diff --git a/api/v1beta1/agent_types.go b/api/v1beta1/agent_types.go index 2d773602b81b..104aa6b217a9 100644 --- a/api/v1beta1/agent_types.go +++ b/api/v1beta1/agent_types.go @@ -199,6 +199,8 @@ type AgentSpec struct { IgnitionEndpointHTTPHeaders map[string]string `json:"ignitionEndpointHTTPHeaders,omitempty"` // NodeLabels are the labels to be applied on the node associated with this agent NodeLabels map[string]string `json:"nodeLabels,omitempty"` + // FencingCredentialsSecretRef is a name of a secret in the Agent's namespace that contains fencing credentials + FencingCredentialsSecretRef string `json:"fencingCredentialsSecretRef,omitempty"` } type IgnitionEndpointTokenReference struct { diff --git a/config/crd/bases/agent-install.openshift.io_agents.yaml b/config/crd/bases/agent-install.openshift.io_agents.yaml index 383a2bd6602d..277b15078600 100644 --- a/config/crd/bases/agent-install.openshift.io_agents.yaml +++ b/config/crd/bases/agent-install.openshift.io_agents.yaml @@ -82,6 +82,10 @@ spec: name must be unique. type: string type: object + fencingCredentialsSecretRef: + description: FencingCredentialsSecretRef is a name of a secret in + the Agent's namespace that contains fencing credentials + type: string hostname: type: string ignitionConfigOverrides: diff --git a/config/crd/resources.yaml b/config/crd/resources.yaml index a959d300da7e..2cee8256a346 100644 --- a/config/crd/resources.yaml +++ b/config/crd/resources.yaml @@ -915,6 +915,10 @@ spec: name must be unique. type: string type: object + fencingCredentialsSecretRef: + description: FencingCredentialsSecretRef is a name of a secret in + the Agent's namespace that contains fencing credentials + type: string hostname: type: string ignitionConfigOverrides: diff --git a/deploy/olm-catalog/manifests/agent-install.openshift.io_agents.yaml b/deploy/olm-catalog/manifests/agent-install.openshift.io_agents.yaml index 147a50e636b7..ffcbad1bfa0a 100644 --- a/deploy/olm-catalog/manifests/agent-install.openshift.io_agents.yaml +++ b/deploy/olm-catalog/manifests/agent-install.openshift.io_agents.yaml @@ -92,6 +92,10 @@ spec: name must be unique. type: string type: object + fencingCredentialsSecretRef: + description: FencingCredentialsSecretRef is a name of a secret in + the Agent's namespace that contains fencing credentials + type: string hostname: type: string ignitionConfigOverrides: diff --git a/internal/bminventory/inventory.go b/internal/bminventory/inventory.go index 850d99a56f77..7837aa7f113a 100644 --- a/internal/bminventory/inventory.go +++ b/internal/bminventory/inventory.go @@ -5960,6 +5960,15 @@ func (b *bareMetalInventory) BindHostInternal(ctx context.Context, params instal return nil, common.NewApiError(http.StatusBadRequest, err) } + fencingClustersSupported, err := common.BaseVersionGreaterOrEqual(common.MinimumVersionForTwoNodesWithFencing, cluster.OpenshiftVersion) + if err != nil { + return nil, common.NewApiError(http.StatusInternalServerError, err) + } + if host.FencingCredentials != "" && !fencingClustersSupported { + err = errors.Errorf("Host %s has fencing credentials, it must be bound to a cluster with openshift version %s or newer", host.ID, common.MinimumVersionForTwoNodesWithFencing) + return nil, common.NewApiError(http.StatusBadRequest, err) + } + if err = b.clusterApi.AcceptRegistration(cluster); err != nil { log.WithError(err).Errorf("failed to bind host <%s> to cluster %s due to: %s", params.HostID.String(), *params.BindHostParams.ClusterID, err.Error()) diff --git a/internal/bminventory/inventory_test.go b/internal/bminventory/inventory_test.go index 7716db7b1666..ff6d87c6e3c2 100644 --- a/internal/bminventory/inventory_test.go +++ b/internal/bminventory/inventory_test.go @@ -18165,7 +18165,7 @@ var _ = Describe("BindHost", func() { hostID = strfmt.UUID(uuid.New().String()) infraEnvID = strfmt.UUID(uuid.New().String()) bm = createInventory(db, cfg) - err := db.Create(&common.Cluster{Cluster: models.Cluster{ID: &clusterID, Kind: swag.String(models.ClusterKindCluster)}}).Error + err := db.Create(&common.Cluster{Cluster: models.Cluster{ID: &clusterID, Kind: swag.String(models.ClusterKindCluster), OpenshiftVersion: common.TestDefaultConfig.OpenShiftVersion}}).Error Expect(err).ShouldNot(HaveOccurred()) err = db.Create(&common.InfraEnv{InfraEnv: models.InfraEnv{ID: &infraEnvID}}).Error Expect(err).ShouldNot(HaveOccurred()) @@ -18451,6 +18451,54 @@ var _ = Describe("BindHost", func() { response = bm.V2DeregisterCluster(ctx, deregisterParams) Expect(response).To(BeAssignableToTypeOf(&installer.V2DeregisterClusterNoContent{})) }) + + It("successful bind with fencing credentials", func() { + var clusterObj models.Cluster + Expect(db.First(&clusterObj, "id = ?", clusterID).Error).ShouldNot(HaveOccurred()) + Expect(db.Model(&clusterObj).Update("openshift_version", common.MinimumVersionForTwoNodesWithFencing).Error).ShouldNot(HaveOccurred()) + + var hostObj models.Host + Expect(db.First(&hostObj, "id = ?", hostID).Error).ShouldNot(HaveOccurred()) + Expect(db.Model(&hostObj).Update("fencing_credentials", "credentials").Error).ShouldNot(HaveOccurred()) + + params := installer.BindHostParams{ + HostID: hostID, + InfraEnvID: infraEnvID, + BindHostParams: &models.BindHostParams{ClusterID: &clusterID}, + } + mockEvents.EXPECT().SendHostEvent(gomock.Any(), eventstest.NewEventMatcher( + eventstest.WithNameMatcher(eventgen.HostBindSucceededEventName), + eventstest.WithHostIdMatcher(params.HostID.String()), + eventstest.WithInfraEnvIdMatcher(infraEnvID.String()), + eventstest.WithSeverityMatcher(models.EventSeverityInfo))) + mockClusterApi.EXPECT().AcceptRegistration(gomock.Any()).Return(nil).Times(1) + mockClusterApi.EXPECT().RefreshSchedulableMastersForcedTrue(gomock.Any(), gomock.Any()).Return(nil).Times(1) + mockHostApi.EXPECT().BindHost(ctx, gomock.Any(), clusterID, gomock.Any()) + + response := bm.BindHost(ctx, params) + Expect(response).To(BeAssignableToTypeOf(&installer.BindHostOK{})) + }) + + It("failed bind because openshift version doesn't support fencing credentials", func() { + var hostObj models.Host + Expect(db.First(&hostObj, "id = ?", hostID).Error).ShouldNot(HaveOccurred()) + Expect(db.Model(&hostObj).Update("fencing_credentials", "credentials").Error).ShouldNot(HaveOccurred()) + + params := installer.BindHostParams{ + HostID: hostID, + InfraEnvID: infraEnvID, + BindHostParams: &models.BindHostParams{ClusterID: &clusterID}, + } + mockEvents.EXPECT().SendHostEvent(gomock.Any(), eventstest.NewEventMatcher( + eventstest.WithNameMatcher(eventgen.HostBindFailedEventName), + eventstest.WithHostIdMatcher(params.HostID.String()), + eventstest.WithInfraEnvIdMatcher(infraEnvID.String()), + eventstest.WithSeverityMatcher(models.EventSeverityError))) + mockHostApi.EXPECT().BindHost(ctx, gomock.Any(), clusterID, gomock.Any()).Times(0) + + response := bm.BindHost(ctx, params) + verifyApiErrorString(response, http.StatusBadRequest, "has fencing credentials") + }) }) var _ = Describe("BindHost - with rhsso auth", func() { @@ -18485,9 +18533,10 @@ var _ = Describe("BindHost - with rhsso auth", func() { err := db.Create(&common.Cluster{ Cluster: models.Cluster{ - ID: &clusterID, - Kind: swag.String(models.ClusterKindCluster), - UserName: userName1}}).Error + ID: &clusterID, + Kind: swag.String(models.ClusterKindCluster), + OpenshiftVersion: common.TestDefaultConfig.OpenShiftVersion, + UserName: userName1}}).Error Expect(err).ShouldNot(HaveOccurred()) err = db.Create(&common.InfraEnv{InfraEnv: models.InfraEnv{ID: &infraEnvID}}).Error Expect(err).ShouldNot(HaveOccurred()) diff --git a/internal/controller/controllers/agent_controller.go b/internal/controller/controllers/agent_controller.go index fe43aedc3b3c..38e38f97b4c2 100644 --- a/internal/controller/controllers/agent_controller.go +++ b/internal/controller/controllers/agent_controller.go @@ -1661,6 +1661,60 @@ func (r *AgentReconciler) updateNodeLabels(log logrus.FieldLogger, host *common. return false, nil } +func (r *AgentReconciler) updateHostFencingCredentials(ctx context.Context, log logrus.FieldLogger, host *common.Host, agent *aiv1beta1.Agent, params *installer.V2UpdateHostParams) (bool, error) { + if agent.Spec.FencingCredentialsSecretRef == "" { + return false, nil + } + + secretRef := types.NamespacedName{Namespace: agent.Namespace, Name: agent.Spec.FencingCredentialsSecretRef} + secret, err := getSecret(ctx, r.Client, r.APIReader, secretRef) + if err != nil { + log.WithError(err).Errorf("failed to get fencing credentials secret for host %s infra-env %s", host.ID.String(), host.InfraEnvID.String()) + return false, err + } + + agentFencingCredentials := &models.FencingCredentialsParams{ + Address: swag.String(string(secret.Data["address"])), + Password: swag.String(string(secret.Data["password"])), + Username: swag.String(string(secret.Data["username"])), + } + certificateVerification, ok := secret.Data["certificateVerification"] + if ok { + agentFencingCredentials.CertificateVerification = swag.String(string(certificateVerification)) + } + + fencingCredentials, err := json.Marshal(agentFencingCredentials) + if err != nil { + log.WithError(err).Errorf("failed to marshal fencing credentials for host %s infra-env %s", host.ID.String(), host.InfraEnvID.String()) + return false, err + } + + if host.FencingCredentials != string(fencingCredentials) { + params.HostUpdateParams.FencingCredentials = agentFencingCredentials + return true, nil + } + return false, nil +} + +func (r *AgentReconciler) updateHostIgnitionEndpointToken(ctx context.Context, log logrus.FieldLogger, host *common.Host, agent *aiv1beta1.Agent, params *installer.V2UpdateHostParams) (bool, error) { + if agent.Spec.IgnitionEndpointTokenReference != nil { + token, err := r.getIgnitionToken(ctx, agent.Spec.IgnitionEndpointTokenReference) + if err != nil { + log.WithError(err).Errorf("Failed to get ignition token") + return false, err + } + + if token != host.IgnitionEndpointToken { + params.HostUpdateParams.IgnitionEndpointToken = &token + return true, nil + } + } else if host.IgnitionEndpointToken != "" { + params.HostUpdateParams.IgnitionEndpointToken = swag.String("") + return true, nil + } + return false, nil +} + func (r *AgentReconciler) updateIfNeeded(ctx context.Context, log logrus.FieldLogger, agent *aiv1beta1.Agent, internalHost *common.Host) (*common.Host, error) { spec := agent.Spec var err error @@ -1696,7 +1750,12 @@ func (r *AgentReconciler) updateIfNeeded(ctx context.Context, log logrus.FieldLo return internalHost, err } - hostUpdate = hostUpdate || nodesUpdated + fencingCredentialsUpdated, err := r.updateHostFencingCredentials(ctx, log, internalHost, agent, params) + if err != nil { + return internalHost, err + } + + hostUpdate = hostUpdate || nodesUpdated || fencingCredentialsUpdated if spec.Hostname != "" && spec.Hostname != internalHost.RequestedHostname { hostUpdate = true @@ -1721,24 +1780,11 @@ func (r *AgentReconciler) updateIfNeeded(ctx context.Context, log logrus.FieldLo } } - if spec.IgnitionEndpointTokenReference != nil { - var token string - token, err = r.getIgnitionToken(ctx, agent.Spec.IgnitionEndpointTokenReference) - if err != nil { - log.WithError(err).Errorf("Failed to get ignition token") - return internalHost, err - } - - if token != internalHost.IgnitionEndpointToken { - hostUpdate = true - params.HostUpdateParams.IgnitionEndpointToken = &token - } - } else { - if internalHost.IgnitionEndpointToken != "" { - hostUpdate = true - params.HostUpdateParams.IgnitionEndpointToken = swag.String("") - } + IgnitionEndpointTokenUpdated, err := r.updateHostIgnitionEndpointToken(ctx, log, internalHost, agent, params) + if err != nil { + return internalHost, err } + hostUpdate = hostUpdate || IgnitionEndpointTokenUpdated if agent.Spec.IgnitionEndpointHTTPHeaders != nil { hostIgnitionEndpointHTTPHeaders := make(map[string]string) diff --git a/internal/controller/controllers/agent_controller_test.go b/internal/controller/controllers/agent_controller_test.go index c25e7176d4ba..a23b608b6ae8 100644 --- a/internal/controller/controllers/agent_controller_test.go +++ b/internal/controller/controllers/agent_controller_test.go @@ -715,6 +715,114 @@ var _ = Describe("agent reconcile", func() { }) }) + Context("update host fencing credentials", func() { + var ( + hostId, infraEnvId strfmt.UUID + commonHost *common.Host + host *v1beta1.Agent + clusterDeployment *hivev1.ClusterDeployment + ) + BeforeEach(func() { + hostId = strfmt.UUID(uuid.New().String()) + infraEnvId = strfmt.UUID(uuid.New().String()) + commonHost = &common.Host{ + Host: models.Host{ + ID: &hostId, + ClusterID: &sId, + Inventory: common.GenerateTestDefaultInventory(), + Status: swag.String(models.HostStatusKnown), + StatusInfo: swag.String("Some status info"), + InfraEnvID: infraEnvId, + }, + } + backEndCluster = &common.Cluster{Cluster: models.Cluster{ + ID: &sId, + Hosts: []*models.Host{ + &commonHost.Host, + }}} + + host = newAgent("host", testNamespace, v1beta1.AgentSpec{ClusterDeploymentName: &v1beta1.ClusterReference{Name: "clusterDeployment", Namespace: testNamespace}}) + clusterDeployment = newClusterDeployment("clusterDeployment", testNamespace, getDefaultClusterDeploymentSpec("clusterDeployment-test", "test-cluster-aci", "pull-secret")) + Expect(c.Create(ctx, clusterDeployment)).To(BeNil()) + + mockInstallerInternal.EXPECT().GetHostByKubeKey(gomock.Any()).Return(commonHost, nil).AnyTimes() + mockInstallerInternal.EXPECT().GetClusterByKubeKey(gomock.Any()).Return(backEndCluster, nil).Times(1) + }) + + It("secret doesn't set certificateVerification", func() { + fencingCredentials := &models.FencingCredentialsParams{ + Address: swag.String("https://bmc.example.com"), + Username: swag.String("admin"), + Password: swag.String("password123"), + } + fencingSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fencing-secret", + Namespace: testNamespace, + }, + Data: map[string][]byte{ + "address": []byte(*fencingCredentials.Address), + "username": []byte(*fencingCredentials.Username), + "password": []byte(*fencingCredentials.Password), + }, + } + Expect(c.Create(ctx, fencingSecret)).To(Succeed()) + host.Spec.FencingCredentialsSecretRef = "fencing-secret" + + mockInstallerInternal.EXPECT().V2UpdateHostInternal(gomock.Any(), gomock.Any(), bminventory.NonInteractive).Do( + func(ctx context.Context, params installer.V2UpdateHostParams, interactive bminventory.Interactivity) { + Expect(params.HostUpdateParams.FencingCredentials).To(Equal(fencingCredentials)) + }).Return(commonHost, nil).Times(1) + allowGetInfraEnvInternal(mockInstallerInternal, infraEnvId, "infraEnvName") + Expect(c.Create(ctx, host)).To(BeNil()) + result, err := hr.Reconcile(ctx, newHostRequest(host)) + Expect(err).To(BeNil()) + Expect(result).To(Equal(ctrl.Result{})) + }) + + It("secret sets certificateVerification", func() { + fencingCredentials := &models.FencingCredentialsParams{ + Address: swag.String("https://bmc.example.com"), + Username: swag.String("admin"), + Password: swag.String("password123"), + CertificateVerification: swag.String("Disabled"), + } + fencingSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fencing-secret", + Namespace: testNamespace, + }, + Data: map[string][]byte{ + "address": []byte(*fencingCredentials.Address), + "username": []byte(*fencingCredentials.Username), + "password": []byte(*fencingCredentials.Password), + "certificateVerification": []byte(*fencingCredentials.CertificateVerification), + }, + } + Expect(c.Create(ctx, fencingSecret)).To(Succeed()) + host.Spec.FencingCredentialsSecretRef = "fencing-secret" + + mockInstallerInternal.EXPECT().V2UpdateHostInternal(gomock.Any(), gomock.Any(), bminventory.NonInteractive).Do( + func(ctx context.Context, params installer.V2UpdateHostParams, interactive bminventory.Interactivity) { + Expect(params.HostUpdateParams.FencingCredentials).To(Equal(fencingCredentials)) + }).Return(commonHost, nil).Times(1) + allowGetInfraEnvInternal(mockInstallerInternal, infraEnvId, "infraEnvName") + Expect(c.Create(ctx, host)).To(BeNil()) + result, err := hr.Reconcile(ctx, newHostRequest(host)) + Expect(err).To(BeNil()) + Expect(result).To(Equal(ctrl.Result{})) + }) + + It("secret does not exist", func() { + host.Spec.FencingCredentialsSecretRef = "fencing-secret" + allowGetInfraEnvInternal(mockInstallerInternal, infraEnvId, "infraEnvName") + Expect(c.Create(ctx, host)).To(BeNil()) + result, err := hr.Reconcile(ctx, newHostRequest(host)) + Expect(err).To(BeNil()) + Expect(result).To(Equal(ctrl.Result{RequeueAfter: defaultRequeueAfterOnError})) + }) + }) + It("Agent update empty disk path", func() { newInstallDiskPath := "" hostId := strfmt.UUID(uuid.New().String()) diff --git a/internal/controller/controllers/bmh_agent_controller.go b/internal/controller/controllers/bmh_agent_controller.go index 5262899a5cfa..5b7c274b1864 100644 --- a/internal/controller/controllers/bmh_agent_controller.go +++ b/internal/controller/controllers/bmh_agent_controller.go @@ -28,6 +28,7 @@ import ( "text/template" "time" + "github.com/go-openapi/swag" bmh_v1alpha1 "github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha1" machinev1beta1 "github.com/openshift/api/machine/v1beta1" aiv1beta1 "github.com/openshift/assisted-service/api/v1beta1" @@ -108,6 +109,10 @@ const ( BMH_NODE_DRAIN_TIMEOUT_ANNOTATION = "bmac.agent-install.openshift.io/drain-timeout" // in time.Duration format BMH_NODE_DRAIN_STATUS_ANNOTATION = "bmac.agent-install.openshift.io/drain-status" + BMH_AGENT_FENCING_CREDENTIALS_SECRET_NAME = "bmac.agent-install.openshift.io/fencing-credentials-secret-name" // nolint: gosec + BMH_AGENT_CREATE_FENCING_CREDENTIALS_SECRET = "bmac.agent-install.openshift.io/create-fencing-credentials-secret" // nolint: gosec + AGENT_FENCING_NAME_FORMAT = "%s-fencing-credentials" + drainStatusSuccess = "drain succeeded" drainStatusInProgress = "draining in progress" drainStatusTimeout = "drain timed out" @@ -303,7 +308,7 @@ func (r *BMACReconciler) Reconcile(origCtx context.Context, req ctrl.Request) (c // with the BMH being reconciled. We will call both, reconcileAgentSpec and // reconcileAgentInventory, every time. The logic to decide whether there's // any action to take is implemented in each function respectively. - result = r.reconcileAgentSpec(log, bmh, agent, infraEnv) + result = r.reconcileAgentSpec(ctx, log, bmh, agent, infraEnv) if res := r.handleReconcileResult(ctx, log, result, agent); res != nil { return res.Result() } @@ -421,7 +426,7 @@ func (r *BMACReconciler) handleBMHFinalizer(ctx context.Context, log logrus.Fiel // Unless there are errors, the agent should be `Approved` at the end of this // reconcile and a label should be set on it referencing the BMH. No changes to // the BMH should happen in this reconcile step. -func (r *BMACReconciler) reconcileAgentSpec(log logrus.FieldLogger, bmh *bmh_v1alpha1.BareMetalHost, agent *aiv1beta1.Agent, infraEnv *aiv1beta1.InfraEnv) reconcileResult { +func (r *BMACReconciler) reconcileAgentSpec(ctx context.Context, log logrus.FieldLogger, bmh *bmh_v1alpha1.BareMetalHost, agent *aiv1beta1.Agent, infraEnv *aiv1beta1.InfraEnv) reconcileResult { log.Debugf("Setting agent spec according to BMH") @@ -512,6 +517,15 @@ func (r *BMACReconciler) reconcileAgentSpec(log logrus.FieldLogger, bmh *bmh_v1a dirty = true } + setDirty, err = r.reconcileAgentFencingCredentials(ctx, bmh, agent) + if err != nil { + log.WithError(err).Errorf("failed to reconcile agent fencing credentials %s/%s", bmh.Namespace, bmh.Name) + return reconcileError{err: err} + } + if setDirty { + dirty = true + } + log.Debugf("Agent spec reconcile finished: %v", agent) return reconcileComplete{dirty: dirty} @@ -558,6 +572,83 @@ func (r *BMACReconciler) reconcileAgentLabels(bmh *bmh_v1alpha1.BareMetalHost, a return ret, nil } +func (r *BMACReconciler) reconcileAgentFencingCredentials(ctx context.Context, bmh *bmh_v1alpha1.BareMetalHost, agent *aiv1beta1.Agent) (bool, error) { + if val, ok := bmh.ObjectMeta.Annotations[BMH_AGENT_FENCING_CREDENTIALS_SECRET_NAME]; ok { + if agent.Spec.FencingCredentialsSecretRef != val { + agent.Spec.FencingCredentialsSecretRef = val + return true, nil + } + return false, nil + } + + if _, ok := bmh.ObjectMeta.Annotations[BMH_AGENT_CREATE_FENCING_CREDENTIALS_SECRET]; !ok { + return false, nil + } + + fencingCredentialsSecretName := fmt.Sprintf(AGENT_FENCING_NAME_FORMAT, agent.ObjectMeta.Name) + err := r.reconcileFencingCredentialsSecret(ctx, bmh, agent, fencingCredentialsSecretName) + if err != nil { + return false, err + } + + if agent.Spec.FencingCredentialsSecretRef != fencingCredentialsSecretName { + agent.Spec.FencingCredentialsSecretRef = fencingCredentialsSecretName + return true, nil + } + return false, nil +} + +func (r *BMACReconciler) reconcileFencingCredentialsSecret(ctx context.Context, bmh *bmh_v1alpha1.BareMetalHost, agent *aiv1beta1.Agent, fencingCredentialsSecretName string) error { + bmhCredentialsSecretRef := types.NamespacedName{Namespace: bmh.Namespace, Name: bmh.Spec.BMC.CredentialsName} + bmhCredentialsSecret, err := getSecret(ctx, r.Client, r.APIReader, bmhCredentialsSecretRef) + if err != nil { + return err + } + + fencingCredentials := &models.FencingCredentialsParams{ + Address: swag.String(bmh.Spec.BMC.Address), + Password: swag.String(string(bmhCredentialsSecret.Data["password"])), + Username: swag.String(string(bmhCredentialsSecret.Data["username"])), + } + if bmh.Spec.BMC.DisableCertificateVerification { + fencingCredentials.CertificateVerification = swag.String("Disabled") + } else { + fencingCredentials.CertificateVerification = swag.String("Enabled") + } + + fencingCredentialsData := map[string][]byte{ + "address": []byte(*fencingCredentials.Address), + "username": []byte(*fencingCredentials.Username), + "password": []byte(*fencingCredentials.Password), + "certificateVerification": []byte(*fencingCredentials.CertificateVerification), + } + + fencingCredentialsSecretRef := types.NamespacedName{Namespace: agent.Namespace, Name: fencingCredentialsSecretName} + fencingCredentialsSecret, err := getSecret(ctx, r.Client, r.APIReader, fencingCredentialsSecretRef) + if err != nil { + if !k8serrors.IsNotFound(err) { + return err + } + + fencingCredentialsSecret = &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: fencingCredentialsSecretName, + Namespace: agent.Namespace, + }, + Data: fencingCredentialsData, + } + err = r.Create(ctx, fencingCredentialsSecret) + } else { + fencingCredentialsSecret.Data = fencingCredentialsData + err = r.Update(ctx, fencingCredentialsSecret) + } + if err != nil { + return err + } + + return ensureSecretIsLabelled(ctx, r.Client, fencingCredentialsSecret, fencingCredentialsSecretRef) +} + func (r *BMACReconciler) reconcileClusterReference(bmh *bmh_v1alpha1.BareMetalHost, agent *aiv1beta1.Agent, infraEnv *aiv1beta1.InfraEnv) (bool, error) { clusterReferenceStr, annotationExists := bmh.Annotations[BMH_CLUSTER_REFERENCE] diff --git a/internal/controller/controllers/bmh_agent_controller_test.go b/internal/controller/controllers/bmh_agent_controller_test.go index 08c0746061c8..73c22d52a723 100644 --- a/internal/controller/controllers/bmh_agent_controller_test.go +++ b/internal/controller/controllers/bmh_agent_controller_test.go @@ -12,6 +12,7 @@ import ( "github.com/google/uuid" bmh_v1alpha1 "github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha1" . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" . "github.com/onsi/gomega" configv1 "github.com/openshift/api/config/v1" machinev1beta1 "github.com/openshift/api/machine/v1beta1" @@ -873,6 +874,132 @@ var _ = Describe("bmac reconcile", func() { })) }) }) + Context("reconcile fencing credentials", func() { + It("BMH has set fencing credentials secret annotation", func() { + updatedHost := &bmh_v1alpha1.BareMetalHost{} + err := c.Get(ctx, types.NamespacedName{Name: host.Name, Namespace: testNamespace}, updatedHost) + Expect(err).To(BeNil()) + updatedHost.ObjectMeta.Annotations[BMH_AGENT_FENCING_CREDENTIALS_SECRET_NAME] = "fencing-secret" + Expect(c.Update(ctx, updatedHost)).To(BeNil()) + + result, err := bmhr.Reconcile(ctx, newBMHRequest(updatedHost)) + Expect(err).To(BeNil()) + Expect(result).To(Equal(ctrl.Result{})) + + updatedAgent := &v1beta1.Agent{} + err = c.Get(ctx, types.NamespacedName{Name: agent.Name, Namespace: agent.Namespace}, updatedAgent) + Expect(err).To(BeNil()) + Expect(updatedAgent.Spec.FencingCredentialsSecretRef).To(Equal("fencing-secret")) + }) + It("BMH has set fencing credentials secret annotation and agent has a different value", func() { + updatedHost := &bmh_v1alpha1.BareMetalHost{} + err := c.Get(ctx, types.NamespacedName{Name: host.Name, Namespace: testNamespace}, updatedHost) + Expect(err).To(BeNil()) + updatedHost.ObjectMeta.Annotations[BMH_AGENT_FENCING_CREDENTIALS_SECRET_NAME] = "fencing-secret" + Expect(c.Update(ctx, updatedHost)).To(BeNil()) + updatedAgent := &v1beta1.Agent{} + err = c.Get(ctx, types.NamespacedName{Name: agent.Name, Namespace: agent.Namespace}, updatedAgent) + Expect(err).To(BeNil()) + updatedAgent.Spec.FencingCredentialsSecretRef = "existing-fencing-credentials" + Expect(c.Update(ctx, updatedAgent)).To(BeNil()) + + result, err := bmhr.Reconcile(ctx, newBMHRequest(updatedHost)) + Expect(err).To(BeNil()) + Expect(result).To(Equal(ctrl.Result{})) + + err = c.Get(ctx, types.NamespacedName{Name: agent.Name, Namespace: agent.Namespace}, updatedAgent) + Expect(err).To(BeNil()) + Expect(updatedAgent.Spec.FencingCredentialsSecretRef).To(Equal("fencing-secret")) + }) + It("BMH has set fencing credentials secret annotation and create fencing credentials secret annotation", func() { + updatedHost := &bmh_v1alpha1.BareMetalHost{} + err := c.Get(ctx, types.NamespacedName{Name: host.Name, Namespace: testNamespace}, updatedHost) + Expect(err).To(BeNil()) + updatedHost.ObjectMeta.Annotations[BMH_AGENT_FENCING_CREDENTIALS_SECRET_NAME] = "fencing-secret" + updatedHost.ObjectMeta.Annotations[BMH_AGENT_CREATE_FENCING_CREDENTIALS_SECRET] = "" + Expect(c.Update(ctx, updatedHost)).To(BeNil()) + + result, err := bmhr.Reconcile(ctx, newBMHRequest(updatedHost)) + Expect(err).To(BeNil()) + Expect(result).To(Equal(ctrl.Result{})) + + updatedAgent := &v1beta1.Agent{} + err = c.Get(ctx, types.NamespacedName{Name: agent.Name, Namespace: agent.Namespace}, updatedAgent) + Expect(err).To(BeNil()) + Expect(updatedAgent.Spec.FencingCredentialsSecretRef).To(Equal("fencing-secret")) + }) + It("agent has fencing credentials and BMH does not have any fencing annotation", func() { + updatedAgent := &v1beta1.Agent{} + err := c.Get(ctx, types.NamespacedName{Name: agent.Name, Namespace: agent.Namespace}, updatedAgent) + Expect(err).To(BeNil()) + updatedAgent.Spec.FencingCredentialsSecretRef = "existing-fencing-credentials" + Expect(c.Update(ctx, updatedAgent)).To(BeNil()) + + result, err := bmhr.Reconcile(ctx, newBMHRequest(host)) + Expect(err).To(BeNil()) + Expect(result).To(Equal(ctrl.Result{})) + + err = c.Get(ctx, types.NamespacedName{Name: agent.Name, Namespace: agent.Namespace}, updatedAgent) + Expect(err).To(BeNil()) + Expect(updatedAgent.Spec.FencingCredentialsSecretRef).To(Equal("existing-fencing-credentials")) + }) + DescribeTable("create fencing credentials secret from BMH", func(certificateVerification string) { + fencingCredentials := &models.FencingCredentialsParams{ + Address: swag.String("https://bmc.example.com"), + Username: swag.String("admin"), + Password: swag.String("password123"), + } + bmcSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "bmc-secret", + Namespace: testNamespace, + }, + Data: map[string][]byte{ + "username": []byte(*fencingCredentials.Username), + "password": []byte(*fencingCredentials.Password), + }, + } + Expect(c.Create(ctx, bmcSecret)).ToNot(HaveOccurred()) + updatedHost := &bmh_v1alpha1.BareMetalHost{} + err := c.Get(ctx, types.NamespacedName{Name: host.Name, Namespace: testNamespace}, updatedHost) + Expect(err).To(BeNil()) + updatedHost.ObjectMeta.Annotations[BMH_AGENT_CREATE_FENCING_CREDENTIALS_SECRET] = "" + updatedHost.Spec.BMC = bmh_v1alpha1.BMCDetails{ + Address: *fencingCredentials.Address, + CredentialsName: "bmc-secret", + } + if certificateVerification == "Disabled" { + updatedHost.Spec.BMC.DisableCertificateVerification = true + } + Expect(c.Update(ctx, updatedHost)).To(BeNil()) + updatedAgent := &v1beta1.Agent{} + err = c.Get(ctx, types.NamespacedName{Name: agent.Name, Namespace: agent.Namespace}, updatedAgent) + Expect(err).To(BeNil()) + updatedAgent.Spec.FencingCredentialsSecretRef = "existing-fencing-credentials" + Expect(c.Update(ctx, updatedAgent)).To(BeNil()) + + result, err := bmhr.Reconcile(ctx, newBMHRequest(updatedHost)) + Expect(err).To(BeNil()) + Expect(result).To(Equal(ctrl.Result{})) + + updatedAgent = &v1beta1.Agent{} + err = c.Get(ctx, types.NamespacedName{Name: agent.Name, Namespace: agent.Namespace}, updatedAgent) + Expect(err).To(BeNil()) + fencingCredentialsSecretName := fmt.Sprintf(AGENT_FENCING_NAME_FORMAT, agent.Name) + Expect(updatedAgent.Spec.FencingCredentialsSecretRef).To(Equal(fencingCredentialsSecretName)) + + fencingCredentialsSecret := &corev1.Secret{} + err = c.Get(ctx, types.NamespacedName{Name: fencingCredentialsSecretName, Namespace: agent.Namespace}, fencingCredentialsSecret) + Expect(err).To(BeNil()) + Expect(string(fencingCredentialsSecret.Data["address"])).To(Equal(*fencingCredentials.Address)) + Expect(string(fencingCredentialsSecret.Data["username"])).To(Equal(*fencingCredentials.Username)) + Expect(string(fencingCredentialsSecret.Data["password"])).To(Equal(*fencingCredentials.Password)) + Expect(string(fencingCredentialsSecret.Data["certificateVerification"])).To(Equal(certificateVerification)) + }, + Entry("certificate verification is enabled", "Enabled"), + Entry("certificate verification is disabled", "Disabled"), + ) + }) It("should set invalid InstallationDiskID if RootDeviceHints device name doesn't match", func() { updatedHost := &bmh_v1alpha1.BareMetalHost{} err := c.Get(ctx, types.NamespacedName{Name: host.Name, Namespace: testNamespace}, updatedHost) diff --git a/subsystem/day2_cluster_test.go b/subsystem/day2_cluster_test.go index e8cf50d47b74..f718f44b64fa 100644 --- a/subsystem/day2_cluster_test.go +++ b/subsystem/day2_cluster_test.go @@ -525,6 +525,7 @@ var _ = Describe("Day2 cluster with bind/unbind hosts", func() { Name: swag.String("test-cluster"), APIVipDnsname: swag.String("api.test-cluster.example.com"), OpenshiftClusterID: &openshiftClusterID, + OpenshiftVersion: openshiftVersion, }, }) Expect(err).NotTo(HaveOccurred()) diff --git a/vendor/github.com/openshift/assisted-service/api/v1beta1/agent_types.go b/vendor/github.com/openshift/assisted-service/api/v1beta1/agent_types.go index 2d773602b81b..104aa6b217a9 100644 --- a/vendor/github.com/openshift/assisted-service/api/v1beta1/agent_types.go +++ b/vendor/github.com/openshift/assisted-service/api/v1beta1/agent_types.go @@ -199,6 +199,8 @@ type AgentSpec struct { IgnitionEndpointHTTPHeaders map[string]string `json:"ignitionEndpointHTTPHeaders,omitempty"` // NodeLabels are the labels to be applied on the node associated with this agent NodeLabels map[string]string `json:"nodeLabels,omitempty"` + // FencingCredentialsSecretRef is a name of a secret in the Agent's namespace that contains fencing credentials + FencingCredentialsSecretRef string `json:"fencingCredentialsSecretRef,omitempty"` } type IgnitionEndpointTokenReference struct {