diff --git a/bundle/manifests/kepler-operator.clusterserviceversion.yaml b/bundle/manifests/kepler-operator.clusterserviceversion.yaml index 20437377..3e763a11 100644 --- a/bundle/manifests/kepler-operator.clusterserviceversion.yaml +++ b/bundle/manifests/kepler-operator.clusterserviceversion.yaml @@ -27,7 +27,7 @@ metadata: capabilities: Basic Install categories: Monitoring containerImage: quay.io/sustainable_computing_io/kepler-operator:0.10.0 - createdAt: "2024-02-15T04:47:16Z" + createdAt: "2024-03-01T19:29:40Z" description: 'Deploys and Manages Kepler on Kubernetes ' operators.operatorframework.io/builder: operator-sdk-v1.27.0 operators.operatorframework.io/internal-objects: |- @@ -117,6 +117,7 @@ spec: resources: - daemonsets - deployments + - secrets verbs: - create - delete @@ -155,6 +156,7 @@ spec: - nodes/metrics - nodes/proxy - nodes/stats + - secrets verbs: - get - list diff --git a/bundle/manifests/kepler.system.sustainable.computing.io_keplerinternals.yaml b/bundle/manifests/kepler.system.sustainable.computing.io_keplerinternals.yaml index 3829f4ad..5ed8c66e 100644 --- a/bundle/manifests/kepler.system.sustainable.computing.io_keplerinternals.yaml +++ b/bundle/manifests/kepler.system.sustainable.computing.io_keplerinternals.yaml @@ -221,6 +221,25 @@ spec: - image - namespace type: object + redfish: + description: RedfishSpec for connecting to Redfish API + properties: + probeInterval: + description: ProbeInterval controls how frequently power info + is queried from Redfish + type: string + secretRef: + description: SecretRef refers to the name of secret which + contains credentials to initialize RedfishClient + type: string + skipSSLVerify: + default: false + description: SkipSSLVerify controls if RedfishClient will + skip verifying server + type: boolean + required: + - secretRef + type: object required: - deployment type: object diff --git a/bundle/manifests/kepler.system.sustainable.computing.io_keplers.yaml b/bundle/manifests/kepler.system.sustainable.computing.io_keplers.yaml index 4204d599..7dc887e9 100644 --- a/bundle/manifests/kepler.system.sustainable.computing.io_keplers.yaml +++ b/bundle/manifests/kepler.system.sustainable.computing.io_keplers.yaml @@ -139,6 +139,25 @@ spec: type: object type: array type: object + redfish: + description: RedfishSpec for connecting to Redfish API + properties: + probeInterval: + description: ProbeInterval controls how frequently power info + is queried from Redfish + type: string + secretRef: + description: SecretRef refers to the name of secret which + contains credentials to initialize RedfishClient + type: string + skipSSLVerify: + default: false + description: SkipSSLVerify controls if RedfishClient will + skip verifying server + type: boolean + required: + - secretRef + type: object type: object type: object status: diff --git a/config/crd/bases/kepler.system.sustainable.computing.io_keplerinternals.yaml b/config/crd/bases/kepler.system.sustainable.computing.io_keplerinternals.yaml index dcdaccc7..151582f0 100644 --- a/config/crd/bases/kepler.system.sustainable.computing.io_keplerinternals.yaml +++ b/config/crd/bases/kepler.system.sustainable.computing.io_keplerinternals.yaml @@ -221,6 +221,25 @@ spec: - image - namespace type: object + redfish: + description: RedfishSpec for connecting to Redfish API + properties: + probeInterval: + description: ProbeInterval controls how frequently power info + is queried from Redfish + type: string + secretRef: + description: SecretRef refers to the name of secret which + contains credentials to initialize RedfishClient + type: string + skipSSLVerify: + default: false + description: SkipSSLVerify controls if RedfishClient will + skip verifying server + type: boolean + required: + - secretRef + type: object required: - deployment type: object diff --git a/config/crd/bases/kepler.system.sustainable.computing.io_keplers.yaml b/config/crd/bases/kepler.system.sustainable.computing.io_keplers.yaml index 7816020e..0822f2dd 100644 --- a/config/crd/bases/kepler.system.sustainable.computing.io_keplers.yaml +++ b/config/crd/bases/kepler.system.sustainable.computing.io_keplers.yaml @@ -129,6 +129,25 @@ spec: type: object type: array type: object + redfish: + description: RedfishSpec for connecting to Redfish API + properties: + probeInterval: + description: ProbeInterval controls how frequently power info + is queried from Redfish + type: string + secretRef: + description: SecretRef refers to the name of secret which + contains credentials to initialize RedfishClient + type: string + skipSSLVerify: + default: false + description: SkipSSLVerify controls if RedfishClient will + skip verifying server + type: boolean + required: + - secretRef + type: object type: object type: object status: diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 667509dc..6187064a 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -9,6 +9,7 @@ rules: resources: - daemonsets - deployments + - secrets verbs: - create - delete @@ -47,6 +48,7 @@ rules: - nodes/metrics - nodes/proxy - nodes/stats + - secrets verbs: - get - list diff --git a/docs/api.md b/docs/api.md index 23544960..11519698 100644 --- a/docs/api.md +++ b/docs/api.md @@ -140,6 +140,13 @@ KeplerInternalSpec defines the desired state of KeplerInternal
true + + redfish + object + + RedfishSpec for connecting to Redfish API
+ + false @@ -265,6 +272,49 @@ The pod this Toleration is attached to tolerates any taint that matches the trip +### KeplerInternal.spec.exporter.redfish +[↩ Parent](#keplerinternalspecexporter) + + + +RedfishSpec for connecting to Redfish API + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
secretRefstring + SecretRef refers to the name of secret which contains credentials to initialize RedfishClient
+
true
probeIntervalstring + ProbeInterval controls how frequently power info is queried from Redfish
+
false
skipSSLVerifyboolean + SkipSSLVerify controls if RedfishClient will skip verifying server
+
+ Default: false
+
false
+ + ### KeplerInternal.spec.estimator [↩ Parent](#keplerinternalspec) @@ -1518,6 +1568,13 @@ KeplerSpec defines the desired state of Kepler
false + + redfish + object + + RedfishSpec for connecting to Redfish API
+ + false @@ -1629,6 +1686,49 @@ The pod this Toleration is attached to tolerates any taint that matches the trip +### Kepler.spec.exporter.redfish +[↩ Parent](#keplerspecexporter) + + + +RedfishSpec for connecting to Redfish API + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
secretRefstring + SecretRef refers to the name of secret which contains credentials to initialize RedfishClient
+
true
probeIntervalstring + ProbeInterval controls how frequently power info is queried from Redfish
+
false
skipSSLVerifyboolean + SkipSSLVerify controls if RedfishClient will skip verifying server
+
+ Default: false
+
false
+ + ### Kepler.status [↩ Parent](#kepler) diff --git a/pkg/api/v1alpha1/kepler_internal_types.go b/pkg/api/v1alpha1/kepler_internal_types.go index 94074533..771f598b 100644 --- a/pkg/api/v1alpha1/kepler_internal_types.go +++ b/pkg/api/v1alpha1/kepler_internal_types.go @@ -39,6 +39,8 @@ type InternalExporterDeploymentSpec struct { type InternalExporterSpec struct { // +kubebuilder:validation:Required Deployment InternalExporterDeploymentSpec `json:"deployment"` + + Redfish *RedfishSpec `json:"redfish,omitempty"` } type DashboardSpec struct { diff --git a/pkg/api/v1alpha1/kepler_types.go b/pkg/api/v1alpha1/kepler_types.go index 41ec8e3a..a23facba 100644 --- a/pkg/api/v1alpha1/kepler_types.go +++ b/pkg/api/v1alpha1/kepler_types.go @@ -38,8 +38,25 @@ type ExporterDeploymentSpec struct { Tolerations []corev1.Toleration `json:"tolerations,omitempty"` } +// RedfishSpec for connecting to Redfish API +type RedfishSpec struct { + // SecretRef refers to the name of secret which contains credentials to initialize RedfishClient + SecretRef string `json:"secretRef"` + + // ProbeInterval controls how frequently power info is queried from Redfish + // +optional + ProbeInterval metav1.Duration `json:"probeInterval,omitempty"` + + // SkipSSLVerify controls if RedfishClient will skip verifying server + // +optional + // +kubebuilder:validation:Required + // +kubebuilder:default:=false + SkipSSLVerify bool `json:"skipSSLVerify,omitempty"` +} + type ExporterSpec struct { Deployment ExporterDeploymentSpec `json:"deployment,omitempty"` + Redfish *RedfishSpec `json:"redfish,omitempty"` } // KeplerSpec defines the desired state of Kepler diff --git a/pkg/api/v1alpha1/zz_generated.deepcopy.go b/pkg/api/v1alpha1/zz_generated.deepcopy.go index 821d301f..621fce09 100644 --- a/pkg/api/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/api/v1alpha1/zz_generated.deepcopy.go @@ -150,6 +150,11 @@ func (in *ExporterDeploymentSpec) DeepCopy() *ExporterDeploymentSpec { func (in *ExporterSpec) DeepCopyInto(out *ExporterSpec) { *out = *in in.Deployment.DeepCopyInto(&out.Deployment) + if in.Redfish != nil { + in, out := &in.Redfish, &out.Redfish + *out = new(RedfishSpec) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExporterSpec. @@ -221,6 +226,11 @@ func (in *InternalExporterDeploymentSpec) DeepCopy() *InternalExporterDeployment func (in *InternalExporterSpec) DeepCopyInto(out *InternalExporterSpec) { *out = *in in.Deployment.DeepCopyInto(&out.Deployment) + if in.Redfish != nil { + in, out := &in.Redfish, &out.Redfish + *out = new(RedfishSpec) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InternalExporterSpec. @@ -509,3 +519,19 @@ func (in *OpenShiftSpec) DeepCopy() *OpenShiftSpec { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedfishSpec) DeepCopyInto(out *RedfishSpec) { + *out = *in + out.ProbeInterval = in.ProbeInterval +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedfishSpec. +func (in *RedfishSpec) DeepCopy() *RedfishSpec { + if in == nil { + return nil + } + out := new(RedfishSpec) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/components/exporter/exporter.go b/pkg/components/exporter/exporter.go index b72e3fb3..89fa4525 100644 --- a/pkg/components/exporter/exporter.go +++ b/pkg/components/exporter/exporter.go @@ -45,6 +45,11 @@ const ( overviewDashboardName = "power-monitoring-overview" nsInfoDashboardName = "power-monitoring-by-ns" DashboardNs = "openshift-config-managed" + REDFISH_ARGS = "-redfish-cred-file-path=/etc/redfish/redfish.csv" + REDFISH_CSV = "redfish.csv" + REDFISH_ANNOTATION = "kepler.system.sustainable.computing.io/redfish-secret-ref" + + IdxKeplerContainer = 0 ) var ( @@ -132,6 +137,19 @@ func NewDaemonSet(detail components.Detail, k *v1alpha1.KeplerInternal) *appsv1. } } +func MountRedfishSecretToDaemonSet(ds *appsv1.DaemonSet, secret *corev1.Secret) { + spec := ds.Spec.Template.Spec + spec.Containers[IdxKeplerContainer].Command = append(spec.Containers[IdxKeplerContainer].Command, REDFISH_ARGS) + spec.Containers[IdxKeplerContainer].VolumeMounts = append(spec.Containers[IdxKeplerContainer].VolumeMounts, + corev1.VolumeMount{Name: "redfish-cred", MountPath: "/etc/redfish", ReadOnly: true}) + spec.Volumes = append(spec.Volumes, + k8s.VolumeFromSecret("redfish-cred", secret.ObjectMeta.Name)) + ds.Spec.Template.Spec = spec + ds.Spec.Template.Annotations = map[string]string{ + REDFISH_ANNOTATION: secret.ResourceVersion, + } +} + func openshiftDashboardObjectMeta(name string) metav1.ObjectMeta { return metav1.ObjectMeta{ Name: name, @@ -377,6 +395,7 @@ func NewSCC(d components.Detail, ki *v1alpha1.KeplerInternal) *secv1.SecurityCon Users: []string{ki.FQServiceAccountName()}, Volumes: []secv1.FSType{ secv1.FSType("configMap"), + secv1.FSType("secret"), secv1.FSType("projected"), secv1.FSType("emptyDir"), secv1.FSType("hostPath")}, @@ -596,7 +615,6 @@ func newExporterContainer(kiName, dsName string, deployment v1alpha1.InternalExp "-enable-gpu=$(ENABLE_GPU)", "-v=$(KEPLER_LOG_LEVEL)", "-kernel-source-dir=/usr/share/kepler/kernel_sources", - "-redfish-cred-file-path=/etc/redfish/redfish.csv", }, Ports: []corev1.ContainerPort{{ ContainerPort: int32(deployment.Port), diff --git a/pkg/components/exporter/exporter_test.go b/pkg/components/exporter/exporter_test.go index f9425683..b0295357 100644 --- a/pkg/components/exporter/exporter_test.go +++ b/pkg/components/exporter/exporter_test.go @@ -93,41 +93,36 @@ func TestTolerations(t *testing.T) { } } -func TestHostPID(t *testing.T) { +func TestDaemonSet(t *testing.T) { tt := []struct { - spec v1alpha1.InternalExporterSpec - hostPID bool - scenario string + spec v1alpha1.InternalExporterSpec + hostPID bool + exporterCommand []string + volumeMounts []corev1.VolumeMount + volumes []corev1.Volume + scenario string + addRedfish bool + redfishSecret *corev1.Secret + annotation map[string]string }{ { - spec: v1alpha1.InternalExporterSpec{}, - hostPID: true, - scenario: "default case", - }, - } - - for _, tc := range tt { - tc := tc - t.Run(tc.scenario, func(t *testing.T) { - t.Parallel() - k := v1alpha1.KeplerInternal{ - Spec: v1alpha1.KeplerInternalSpec{ - Exporter: tc.spec, + spec: v1alpha1.InternalExporterSpec{ + Deployment: v1alpha1.InternalExporterDeploymentSpec{ + ExporterDeploymentSpec: v1alpha1.ExporterDeploymentSpec{ + Port: 9103, + }, }, - } - actual := k8s.HostPIDFromDS(NewDaemonSet(components.Full, &k)) - assert.Equal(t, actual, tc.hostPID) - }) - } -} -func TestVolumeMounts(t *testing.T) { - tt := []struct { - spec v1alpha1.InternalExporterSpec - volumeMounts []corev1.VolumeMount - scenario string - }{ - { - spec: v1alpha1.InternalExporterSpec{}, + }, + hostPID: true, + exporterCommand: []string{ + "/usr/bin/kepler", + "-address", + "0.0.0.0:9103", + "-enable-cgroup-id=true", + "-enable-gpu=$(ENABLE_GPU)", + "-v=$(KEPLER_LOG_LEVEL)", + "-kernel-source-dir=/usr/share/kepler/kernel_sources", + }, volumeMounts: []corev1.VolumeMount{ {Name: "lib-modules", MountPath: "/lib/modules", ReadOnly: true}, {Name: "tracing", MountPath: "/sys", ReadOnly: true}, @@ -135,40 +130,61 @@ func TestVolumeMounts(t *testing.T) { {Name: "proc", MountPath: "/proc"}, {Name: "cfm", MountPath: "/etc/kepler/kepler.config"}, }, + volumes: []corev1.Volume{ + k8s.VolumeFromHost("lib-modules", "/lib/modules"), + k8s.VolumeFromHost("tracing", "/sys"), + k8s.VolumeFromHost("proc", "/proc"), + k8s.VolumeFromHost("kernel-src", "/usr/src/kernels"), + k8s.VolumeFromConfigMap("cfm", "kepler-internal"), + }, scenario: "default case", }, - } - - for _, tc := range tt { - tc := tc - t.Run(tc.scenario, func(t *testing.T) { - t.Parallel() - k := v1alpha1.KeplerInternal{ - Spec: v1alpha1.KeplerInternalSpec{ - Exporter: tc.spec, - }, - } - actual := k8s.VolumeMountsFromDS(NewDaemonSet(components.Full, &k)) - assert.Equal(t, actual, tc.volumeMounts) - }) - } -} -func TestVolumes(t *testing.T) { - tt := []struct { - spec v1alpha1.InternalExporterSpec - volumes []corev1.Volume - scenario string - }{ { - spec: v1alpha1.InternalExporterSpec{}, + spec: v1alpha1.InternalExporterSpec{ + Deployment: v1alpha1.InternalExporterDeploymentSpec{ + ExporterDeploymentSpec: v1alpha1.ExporterDeploymentSpec{ + Port: 9103, + }, + }, + }, + hostPID: true, + exporterCommand: []string{ + "/usr/bin/kepler", + "-address", + "0.0.0.0:9103", + "-enable-cgroup-id=true", + "-enable-gpu=$(ENABLE_GPU)", + "-v=$(KEPLER_LOG_LEVEL)", + "-kernel-source-dir=/usr/share/kepler/kernel_sources", + "-redfish-cred-file-path=/etc/redfish/redfish.csv", + }, + volumeMounts: []corev1.VolumeMount{ + {Name: "lib-modules", MountPath: "/lib/modules", ReadOnly: true}, + {Name: "tracing", MountPath: "/sys", ReadOnly: true}, + {Name: "kernel-src", MountPath: "/usr/src/kernels", ReadOnly: true}, + {Name: "proc", MountPath: "/proc"}, + {Name: "cfm", MountPath: "/etc/kepler/kepler.config"}, + {Name: "redfish-cred", MountPath: "/etc/redfish", ReadOnly: true}, + }, volumes: []corev1.Volume{ k8s.VolumeFromHost("lib-modules", "/lib/modules"), k8s.VolumeFromHost("tracing", "/sys"), k8s.VolumeFromHost("proc", "/proc"), k8s.VolumeFromHost("kernel-src", "/usr/src/kernels"), k8s.VolumeFromConfigMap("cfm", "kepler-internal"), + k8s.VolumeFromSecret("redfish-cred", "my-redfish-secret"), }, - scenario: "default case", + addRedfish: true, + redfishSecret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-redfish-secret", + ResourceVersion: "123", + }, + }, + annotation: map[string]string{ + "kepler.system.sustainable.computing.io/redfish-secret-ref": "123", + }, + scenario: "redfish case", }, } @@ -184,8 +200,25 @@ func TestVolumes(t *testing.T) { Exporter: tc.spec, }, } - actual := k8s.VolumesFromDS(NewDaemonSet(components.Full, &k)) - assert.Equal(t, actual, tc.volumes) + ds := NewDaemonSet(components.Full, &k) + if tc.addRedfish { + MountRedfishSecretToDaemonSet(ds, tc.redfishSecret) + } + + actual_hostPID := k8s.HostPIDFromDS(ds) + assert.Equal(t, actual_hostPID, tc.hostPID) + + actual_exporterCommand := k8s.CommandFromDS(ds, IdxKeplerContainer) + assert.Equal(t, actual_exporterCommand, tc.exporterCommand) + + actual_volumeMounts := k8s.VolumeMountsFromDS(ds, IdxKeplerContainer) + assert.Equal(t, actual_volumeMounts, tc.volumeMounts) + + actual_Volumes := k8s.VolumesFromDS(ds) + assert.Equal(t, actual_Volumes, tc.volumes) + + actual_Annotation := k8s.AnnotationFromDS(ds) + assert.Equal(t, actual_Annotation, tc.annotation) }) } } diff --git a/pkg/controllers/kepler.go b/pkg/controllers/kepler.go index da9f4b86..649e567c 100644 --- a/pkg/controllers/kepler.go +++ b/pkg/controllers/kepler.go @@ -269,6 +269,7 @@ func newKeplerInternal(d components.Detail, k *v1alpha1.Kepler) *v1alpha1.Kepler Image: Config.Image, Namespace: KeplerDeploymentNS, }, + Redfish: k.Spec.Exporter.Redfish, }, OpenShift: v1alpha1.OpenShiftSpec{ Enabled: isOpenShift, diff --git a/pkg/controllers/kepler_internal.go b/pkg/controllers/kepler_internal.go index 137b04c0..b8371c3d 100644 --- a/pkg/controllers/kepler_internal.go +++ b/pkg/controllers/kepler_internal.go @@ -8,8 +8,10 @@ import ( "github.com/go-logr/logr" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/sustainable.computing.io/kepler-operator/pkg/api/v1alpha1" "github.com/sustainable.computing.io/kepler-operator/pkg/components" @@ -46,12 +48,12 @@ type KeplerInternalReconciler struct { //+kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=*,verbs=* // RBAC for running Kepler exporter -//+kubebuilder:rbac:groups=apps,resources=daemonsets;deployments,verbs=list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=apps,resources=daemonsets;deployments;secrets,verbs=list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=security.openshift.io,resources=securitycontextconstraints,verbs=list;watch;create;update;patch;delete;use //+kubebuilder:rbac:groups=monitoring.coreos.com,resources=servicemonitors;prometheusrules,verbs=list;watch;create;update;patch;delete // RBAC required by Kepler exporter -//+kubebuilder:rbac:groups=core,resources=nodes/metrics;nodes/proxy;nodes/stats,verbs=get;list;watch +//+kubebuilder:rbac:groups=core,resources=nodes/metrics;nodes/proxy;nodes/stats;secrets,verbs=get;list;watch // SetupWithManager sets up the controller with the Manager. func (r *KeplerInternalReconciler) SetupWithManager(mgr ctrl.Manager) error { @@ -74,12 +76,40 @@ func (r *KeplerInternalReconciler) SetupWithManager(mgr ctrl.Manager) error { Owns(&rbacv1.ClusterRoleBinding{}, genChanged). Owns(&rbacv1.ClusterRole{}, genChanged) + c = c.Watches(&corev1.Secret{}, + handler.EnqueueRequestsFromMapFunc(r.mapSecretToRequests), + builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}), + ) + if Config.Cluster == k8s.OpenShift { c = c.Owns(&secv1.SecurityContextConstraints{}, genChanged) } return c.Complete(r) } +// mapSecretToRequests returns the reconcile requests for kepler-internal objects for which an associated redfish secret has been changed. +func (r *KeplerInternalReconciler) mapSecretToRequests(ctx context.Context, object client.Object) []reconcile.Request { + + secret, _ := object.(*corev1.Secret) + + ks := v1alpha1.KeplerInternalList{} + if err := r.List(ctx, &ks); err != nil { + return nil + } + + requests := []reconcile.Request{} + for _, ki := range ks.Items { + ex := ki.Spec.Exporter + if ex.Redfish.SecretRef == secret.GetName() && + ex.Deployment.Namespace == secret.GetNamespace() { + requests = append(requests, reconcile.Request{ + NamespacedName: types.NamespacedName{Name: ki.ObjectMeta.Name, Namespace: ki.ObjectMeta.Namespace}, + }) + } + } + return requests +} + // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. // TODO(user): Modify the Reconcile function to compare the state specified by @@ -93,7 +123,7 @@ func (r *KeplerInternalReconciler) Reconcile(ctx context.Context, req ctrl.Reque logger := log.FromContext(ctx) r.logger = logger - logger.Info("Start of reconcile") + logger.Info("Start of reconcile") defer logger.Info("End of reconcile") ki, err := r.getInternal(ctx, req) @@ -123,7 +153,7 @@ func (r *KeplerInternalReconciler) Reconcile(ctx context.Context, req ctrl.Reque func (r KeplerInternalReconciler) runReconcilers(ctx context.Context, ki *v1alpha1.KeplerInternal) (ctrl.Result, error) { reconcilers := r.reconcilersForInternal(ki) - r.logger.V(6).Info("renconcilers ...", "count", len(reconcilers)) + r.logger.V(6).Info("reconcilers ...", "count", len(reconcilers)) return reconciler.Runner{ Reconcilers: reconcilers, @@ -414,33 +444,33 @@ func availableCondition(dset *appsv1.DaemonSet) v1alpha1.Condition { return c } -func (r KeplerInternalReconciler) reconcilersForInternal(k *v1alpha1.KeplerInternal) []reconciler.Reconciler { +func (r KeplerInternalReconciler) reconcilersForInternal(ki *v1alpha1.KeplerInternal) []reconciler.Reconciler { rs := []reconciler.Reconciler{} - cleanup := !k.DeletionTimestamp.IsZero() + cleanup := !ki.DeletionTimestamp.IsZero() if !cleanup { // NOTE: create namespace first and for deletion, reverse the order rs = append(rs, reconciler.Updater{ - Owner: k, - Resource: components.NewNamespace(k.Namespace()), + Owner: ki, + Resource: components.NewNamespace(ki.Namespace()), OnError: reconciler.Requeue, Logger: r.logger, }) } - if k.Spec.Estimator != nil { - if k.Spec.Estimator.Image == "" { - k.Spec.Estimator.Image = InternalConfig.EstimatorImage + if ki.Spec.Estimator != nil { + if ki.Spec.Estimator.Image == "" { + ki.Spec.Estimator.Image = InternalConfig.EstimatorImage } } - rs = append(rs, exporterReconcilers(k, Config.Cluster)...) + rs = append(rs, exporterReconcilers(ki, Config.Cluster)...) - if k.Spec.ModelServer != nil && k.Spec.ModelServer.Enabled { - if k.Spec.ModelServer.Image == "" { - k.Spec.ModelServer.Image = InternalConfig.ModelServerImage + if ki.Spec.ModelServer != nil && ki.Spec.ModelServer.Enabled { + if ki.Spec.ModelServer.Image == "" { + ki.Spec.ModelServer.Image = InternalConfig.ModelServerImage } - reconcilers, err := modelServerInternalReconcilers(k) + reconcilers, err := modelServerInternalReconcilers(ki) if err != nil { r.logger.Info(fmt.Sprintf("cannot init model server reconciler from config: %v", err)) } else { @@ -451,7 +481,7 @@ func (r KeplerInternalReconciler) reconcilersForInternal(k *v1alpha1.KeplerInter if cleanup { rs = append(rs, reconciler.Deleter{ OnError: reconciler.Requeue, - Resource: components.NewNamespace(k.Namespace()), + Resource: components.NewNamespace(ki.Namespace()), WaitTimeout: 2 * time.Minute, }) } @@ -459,7 +489,7 @@ func (r KeplerInternalReconciler) reconcilersForInternal(k *v1alpha1.KeplerInter // WARN: only run finalizer if theren't any errors // this bug 🐛 must be FIXED rs = append(rs, reconciler.Finalizer{ - Resource: k, + Resource: ki, Finalizer: Finalizer, Logger: r.logger, }) @@ -490,12 +520,27 @@ func exporterReconcilers(ki *v1alpha1.KeplerInternal, cluster k8s.Cluster) []rec // namespace scoped rs = append(rs, resourceReconcilers(updateResource, exporter.NewServiceAccount(ki), - exporter.NewConfigMap(components.Full, ki), - exporter.NewDaemonSet(components.Full, ki), exporter.NewService(ki), exporter.NewServiceMonitor(ki), exporter.NewPrometheusRule(ki), )...) + if ki.Spec.Exporter.Redfish == nil { + rs = append(rs, resourceReconcilers(updateResource, + exporter.NewDaemonSet(components.Full, ki), + exporter.NewConfigMap(components.Full, ki), + )...) + } else { + rs = append(rs, + reconciler.KeplerDaemonSetReconciler{ + Ki: *ki, + Ds: exporter.NewDaemonSet(components.Full, ki), + }, + reconciler.KeplerConfigMapReconciler{ + Ki: *ki, + Cfm: exporter.NewConfigMap(components.Full, ki), + }, + ) + } rs = append(rs, resourceReconcilers(updateResource, openshiftNamespacedResources(ki, cluster)...)...) return rs } diff --git a/pkg/reconciler/reconciler.go b/pkg/reconciler/reconciler.go index 81de3d00..8622bd21 100644 --- a/pkg/reconciler/reconciler.go +++ b/pkg/reconciler/reconciler.go @@ -18,8 +18,16 @@ package reconciler import ( "context" + "fmt" + "strconv" + "github.com/sustainable.computing.io/kepler-operator/pkg/api/v1alpha1" + "github.com/sustainable.computing.io/kepler-operator/pkg/components/exporter" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -44,3 +52,52 @@ type Result struct { type Reconciler interface { Reconcile(context.Context, client.Client, *runtime.Scheme) Result } + +type KeplerDaemonSetReconciler struct { + Ki v1alpha1.KeplerInternal + Ds *appsv1.DaemonSet +} + +func (r KeplerDaemonSetReconciler) Reconcile(ctx context.Context, cli client.Client, s *runtime.Scheme) Result { + + secretRef := r.Ki.Spec.Exporter.Redfish.SecretRef + secret, err := r.getRedfishSecret(ctx, cli, secretRef) + + if err != nil { + return Result{Action: Stop, Error: fmt.Errorf("Error occured while getting secret %w", err)} + } + if secret == nil { + return Result{Action: Stop, Error: fmt.Errorf("Redfish secret configured, but secret %q not found", secretRef)} + } + if _, ok := secret.Data[exporter.REDFISH_CSV]; !ok { + return Result{Action: Stop, Error: fmt.Errorf("Redfish secret does not contain \"redfish.csv\"")} + } + + exporter.MountRedfishSecretToDaemonSet(r.Ds, secret) + + return Updater{Owner: &r.Ki, Resource: r.Ds}.Reconcile(ctx, cli, s) +} + +func (r KeplerDaemonSetReconciler) getRedfishSecret(ctx context.Context, cli client.Client, secretName string) (*corev1.Secret, error) { + ns := r.Ki.Spec.Exporter.Deployment.Namespace + redfishSecret := corev1.Secret{} + if err := cli.Get(ctx, types.NamespacedName{Namespace: ns, Name: secretName}, &redfishSecret); err != nil { + return nil, client.IgnoreNotFound(err) + } + return &redfishSecret, nil +} + +type KeplerConfigMapReconciler struct { + Ki v1alpha1.KeplerInternal + Cfm *corev1.ConfigMap +} + +func (r KeplerConfigMapReconciler) Reconcile(ctx context.Context, cli client.Client, s *runtime.Scheme) Result { + rf := r.Ki.Spec.Exporter.Redfish + zero := metav1.Duration{} + if rf.ProbeInterval != zero { + r.Cfm.Data["REDFISH_PROBE_INTERVAL_IN_SECONDS"] = fmt.Sprintf("%f", rf.ProbeInterval.Duration.Seconds()) + } + r.Cfm.Data["REDFISH_SKIP_SSL_VERIFY"] = strconv.FormatBool(rf.SkipSSLVerify) + return Updater{Owner: &r.Ki, Resource: r.Cfm}.Reconcile(ctx, cli, s) +} diff --git a/pkg/utils/k8s/k8s.go b/pkg/utils/k8s/k8s.go index 56f68771..5160f6c8 100644 --- a/pkg/utils/k8s/k8s.go +++ b/pkg/utils/k8s/k8s.go @@ -23,6 +23,7 @@ import ( "github.com/sustainable.computing.io/kepler-operator/pkg/api/v1alpha1" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + "k8s.io/utils/pointer" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -157,14 +158,35 @@ func HostPIDFromDS(ds *appsv1.DaemonSet) bool { return ds.Spec.Template.Spec.HostPID } -func VolumeMountsFromDS(ds *appsv1.DaemonSet) []corev1.VolumeMount { - return ds.Spec.Template.Spec.Containers[0].VolumeMounts +func CommandFromDS(ds *appsv1.DaemonSet, index int) []string { + return ds.Spec.Template.Spec.Containers[index].Command +} + +func AnnotationFromDS(ds *appsv1.DaemonSet) map[string]string { + return ds.Spec.Template.Annotations +} + +func VolumeMountsFromDS(ds *appsv1.DaemonSet, index int) []corev1.VolumeMount { + return ds.Spec.Template.Spec.Containers[index].VolumeMounts } func VolumesFromDS(ds *appsv1.DaemonSet) []corev1.Volume { return ds.Spec.Template.Spec.Volumes } +func VolumeFromSecret(name, secretName string) corev1.Volume { + return corev1.Volume{ + Name: name, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: secretName, + Optional: pointer.Bool(true), + DefaultMode: pointer.Int32(420), + }, + }, + } +} + func AllowsFromSCC(SCC *secv1.SecurityContextConstraints) SCCAllows { return SCCAllows{ AllowPrivilegedContainer: SCC.AllowPrivilegedContainer, diff --git a/pkg/utils/test/framework.go b/pkg/utils/test/framework.go index 3a5f8195..6c5aa3cf 100644 --- a/pkg/utils/test/framework.go +++ b/pkg/utils/test/framework.go @@ -193,6 +193,12 @@ func (f Framework) DeleteKepler(name string) { }) } +func (f Framework) GetKeplerInternal(name string) *v1alpha1.KeplerInternal { + kepler := v1alpha1.KeplerInternal{} + f.AssertResourceExists(name, "", &kepler) + return &kepler +} + func (f Framework) CreateInternal(name string, fns ...internalFn) *v1alpha1.KeplerInternal { ki := v1alpha1.KeplerInternal{ TypeMeta: metav1.TypeMeta{ diff --git a/pkg/utils/test/kepler_internal_builder.go b/pkg/utils/test/kepler_internal_builder.go index 6c208293..c1883568 100644 --- a/pkg/utils/test/kepler_internal_builder.go +++ b/pkg/utils/test/kepler_internal_builder.go @@ -72,3 +72,11 @@ func (InternalBuilder) WithCluster(c k8s.Cluster) func(k *v1alpha1.KeplerInterna } } } + +func (InternalBuilder) WithRedfish(c k8s.Cluster, secretName string) func(k *v1alpha1.KeplerInternal) { + return func(k *v1alpha1.KeplerInternal) { + k.Spec.Exporter.Redfish = &v1alpha1.RedfishSpec{ + SecretRef: secretName, + } + } +} diff --git a/tests/e2e/kepler_internal_test.go b/tests/e2e/kepler_internal_test.go index d3739a4e..63f6c08e 100644 --- a/tests/e2e/kepler_internal_test.go +++ b/tests/e2e/kepler_internal_test.go @@ -16,15 +16,22 @@ limitations under the License. package e2e import ( + "context" + "fmt" "testing" "time" "github.com/stretchr/testify/assert" "github.com/sustainable.computing.io/kepler-operator/pkg/api/v1alpha1" + "github.com/sustainable.computing.io/kepler-operator/pkg/components/exporter" "github.com/sustainable.computing.io/kepler-operator/pkg/controllers" + "github.com/sustainable.computing.io/kepler-operator/pkg/utils/k8s" "github.com/sustainable.computing.io/kepler-operator/pkg/utils/test" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" ) const ( @@ -63,6 +70,89 @@ func TestKeplerInternal_Reconciliation(t *testing.T) { f.AssertInternalStatus(ki.Name, test.Timeout(5*time.Minute)) } +func TestKeplerInternal_ReconciliationWithRedfish(t *testing.T) { + f := test.NewFramework(t) + name := "e2e-ki-redfish" + secretName := "my-redfish-secret" + // test namespace must be the deployment namespace for controller + // to watch the deployments / daemonsets etc + testNs := controllers.KeplerDeploymentNS + + // pre-condition + f.AssertNoResourceExists(name, "", &v1alpha1.KeplerInternal{}, test.NoWait()) + + // when + b := test.InternalBuilder{} + ki := f.CreateInternal(name, + b.WithNamespace(testNs), + b.WithExporterImage(keplerImage), + b.WithExporterPort(9108), + b.WithCluster(Cluster), + b.WithRedfish(Cluster, secretName), + ) + + // then the following resources will be created + f.AssertResourceExists(testNs, "", &corev1.Namespace{}) + + ds := appsv1.DaemonSet{} + f.AssertNoResourceExists(ki.Name, testNs, &ds) + + cond := f.GetKeplerInternal(name).Status.Exporter.Conditions + assert.True(t, len(cond) > 1) + assert.Equal(t, fmt.Sprintf("Redfish secret configured, but secret %q not found", secretName), cond[0].Message) + + // create redfish secret + redfishSecret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: controllers.KeplerDeploymentNS, + }, + Data: map[string][]byte{ + "redfish.csv": []byte("dummy"), + }, + } + err := f.Client().Create(context.TODO(), &redfishSecret) + assert.NoError(t, err) + + // wait for DaemonSet to be created + f.AssertResourceExists(ki.Name, testNs, &ds) + cond = f.GetKeplerInternal(name).Status.Exporter.Conditions + assert.True(t, len(cond) > 1) + + containers := ds.Spec.Template.Spec.Containers + assert.Equal(t, 1, len(containers)) + exp := containers[exporter.IdxKeplerContainer] + assert.Contains(t, exp.Command, exporter.REDFISH_ARGS) + assert.Contains(t, exp.VolumeMounts, + corev1.VolumeMount{Name: "redfish-cred", MountPath: "/etc/redfish", ReadOnly: true}) + assert.Contains(t, ds.Spec.Template.Spec.Volumes, + k8s.VolumeFromSecret("redfish-cred", redfishSecret.Name)) + assert.Contains(t, ds.Spec.Template.Annotations, exporter.REDFISH_ANNOTATION) + + og := ds.Status.ObservedGeneration + assert.Equal(t, og, int64(1)) + + redfishSecret.Data["redfish.csv"] = []byte("dummy2") + err = f.Client().Update(context.TODO(), &redfishSecret) + assert.NoError(t, err) + + // wait for DaemonSet to restart + ds = appsv1.DaemonSet{} + f.WaitUntil("Daemonset to restart", func() (bool, error) { + err := f.Client().Get(context.TODO(), + client.ObjectKey{Namespace: controllers.KeplerDeploymentNS, Name: ki.Name}, &ds) + if errors.IsNotFound(err) { + return false, nil + } else if err != nil { + return false, err + } + return ds.Status.ObservedGeneration == og+1, nil + }) + + // test expected status + f.AssertInternalStatus(ki.Name) +} + func TestKeplerInternal_WithEstimator(t *testing.T) { f := test.NewFramework(t) name := "e2e-ki-with-estimator" diff --git a/tests/run-e2e.sh b/tests/run-e2e.sh index 0f52c0af..2338e948 100755 --- a/tests/run-e2e.sh +++ b/tests/run-e2e.sh @@ -11,7 +11,7 @@ source "$PROJECT_ROOT/hack/utils.bash" declare -r LOCAL_BIN="$PROJECT_ROOT/tmp/bin" declare -r OPERATOR="kepler-operator" declare -r OLM_CATALOG="kepler-operator-catalog" -declare -r VERSION="0.0.0-e2e" +declare -r VERSION=${VERSION:-"0.0.0-e2e"} declare -r OPERATOR_DEPLOY_YAML="config/manager/manager.yaml" declare -r OPERATOR_CSV="bundle/manifests/$OPERATOR.clusterserviceversion.yaml" declare -r OPERATOR_DEPLOY_NAME="kepler-operator-controller" @@ -302,7 +302,7 @@ restart_operator() { info "scale down Operator" run kubectl scale -n "$OPERATORS_NS" --replicas=0 "$deployment" run kubectl wait -n "$OPERATORS_NS" --for=delete \ - pods -l app.kubernetes.io/component=operator --timeout=60s + pods -l app.kubernetes.io/component=manager --timeout=60s update_crds