diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index da677b8..73f936e 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -17,4 +17,4 @@ jobs: - name: golangci-lint uses: golangci/golangci-lint-action@v6 with: - version: v1.59 + version: v1.60 diff --git a/Dockerfile b/Dockerfile index a1331b2..f88b0a3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -33,14 +33,14 @@ RUN --mount=type=cache,target=/root/.cache/go-build \ # Use distroless as minimal base image to package the manager binary # Refer to https://github.com/GoogleContainerTools/distroless for more details -FROM gcr.io/distroless/static:nonroot as manager +FROM gcr.io/distroless/static:nonroot AS manager WORKDIR / COPY --from=builder /workspace/manager . USER 65532:65532 ENTRYPOINT ["/manager"] -FROM gcr.io/distroless/static:nonroot as probe +FROM gcr.io/distroless/static:nonroot AS probe WORKDIR / COPY --from=builder /workspace/metalprobe . USER 65532:65532 diff --git a/Makefile b/Makefile index 9d39785..c0ac157 100644 --- a/Makefile +++ b/Makefile @@ -194,7 +194,7 @@ GEN_CRD_API_REFERENCE_DOCS ?= $(LOCALBIN)/gen-crd-api-reference-docs-$(GEN_CRD_A KUSTOMIZE_VERSION ?= v5.3.0 CONTROLLER_TOOLS_VERSION ?= v0.15.0 ENVTEST_VERSION ?= latest -GOLANGCI_LINT_VERSION ?= v1.59.1 +GOLANGCI_LINT_VERSION ?= v1.60.1 GOIMPORTS_VERSION ?= v0.22.0 GEN_CRD_API_REFERENCE_DOCS_VERSION ?= v0.3.0 diff --git a/bmc/redfish_local.go b/bmc/redfish_local.go index 2ac3af9..da9a6bb 100644 --- a/bmc/redfish_local.go +++ b/bmc/redfish_local.go @@ -31,41 +31,27 @@ func NewRedfishLocalBMCClient( } func (r RedfishLocalBMC) PowerOn(systemUUID string) error { - service := r.client.GetService() - systems, err := service.Systems() + system, err := r.getSystemByUUID(systemUUID) if err != nil { - return fmt.Errorf("failed to get systems: %w", err) + return fmt.Errorf("failed to get system: %w", err) } - - for _, system := range systems { - if system.UUID == systemUUID { - system.PowerState = redfish.OnPowerState - systemURI := fmt.Sprintf("/redfish/v1/Systems/%s", system.ID) - if err := system.Patch(systemURI, system); err != nil { - return fmt.Errorf("failed to set power state %s for system %s: %w", redfish.OnPowerState, systemUUID, err) - } - break - } + system.PowerState = redfish.OnPowerState + systemURI := fmt.Sprintf("/redfish/v1/Systems/%s", system.ID) + if err := system.Patch(systemURI, system); err != nil { + return fmt.Errorf("failed to set power state %s for system %s: %w", redfish.OnPowerState, systemUUID, err) } return nil } func (r RedfishLocalBMC) PowerOff(systemUUID string) error { - service := r.client.GetService() - systems, err := service.Systems() + system, err := r.getSystemByUUID(systemUUID) if err != nil { - return fmt.Errorf("failed to get systems: %w", err) + return fmt.Errorf("failed to get system: %w", err) } - - for _, system := range systems { - if system.UUID == systemUUID { - system.PowerState = redfish.OffPowerState - systemURI := fmt.Sprintf("/redfish/v1/Systems/%s", system.ID) - if err := system.Patch(systemURI, system); err != nil { - return fmt.Errorf("failed to set power state %s for system %s: %w", redfish.OffPowerState, systemUUID, err) - } - break - } + system.PowerState = redfish.OffPowerState + systemURI := fmt.Sprintf("/redfish/v1/Systems/%s", system.ID) + if err := system.Patch(systemURI, system); err != nil { + return fmt.Errorf("failed to set power state %s for system %s: %w", redfish.OffPowerState, systemUUID, err) } return nil } diff --git a/cmd/manager/main.go b/cmd/manager/main.go index ac1abe0..3a16cab 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -58,6 +58,7 @@ func main() { var registryURL string var requeueInterval time.Duration var webhookPort int + var enforceFirstBoot bool flag.DurationVar(&requeueInterval, "requeue-interval", 10*time.Second, "Reconciler requeue interval.") flag.StringVar(®istryURL, "registry-url", "", "The URL of the registry.") @@ -70,6 +71,8 @@ func main() { flag.StringVar(&macPrefixesFile, "mac-prefixes-file", "", "Location of the MAC prefixes file.") flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") + flag.BoolVar(&enforceFirstBoot, "enforce-first-boot", false, + "Enforce the first boot probing of a Server even if it is powered on in the Initial state.") flag.IntVar(&webhookPort, "webhook-port", 9443, "The port to use for webhook server.") flag.BoolVar(&enableLeaderElection, "leader-elect", false, "Enable leader election for controller manager. "+ @@ -198,6 +201,7 @@ func main() { ProbeOSImage: probeOSImage, RegistryURL: registryURL, RequeueInterval: requeueInterval, + EnforceFirstBoot: enforceFirstBoot, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Server") os.Exit(1) diff --git a/internal/controller/bmc_controller_test.go b/internal/controller/bmc_controller_test.go index 7f48e4d..7f33c46 100644 --- a/internal/controller/bmc_controller_test.go +++ b/internal/controller/bmc_controller_test.go @@ -33,6 +33,30 @@ var _ = Describe("BMC Controller", func() { } Expect(k8sClient.Create(ctx, endpoint)).To(Succeed()) DeferCleanup(k8sClient.Delete, endpoint) + + By("Ensuring that the BMC will be removed") + bmc := &metalv1alpha1.BMC{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("bmc-%s", endpoint.Name), + }, + } + DeferCleanup(k8sClient.Delete, bmc) + + By("Ensuring that the BMCSecret will be removed") + bmcSecret := &metalv1alpha1.BMCSecret{ + ObjectMeta: metav1.ObjectMeta{ + Name: bmc.Name, + }, + } + DeferCleanup(k8sClient.Delete, bmcSecret) + + By("Ensuring that the Server resource will be removed") + server := &metalv1alpha1.Server{ + ObjectMeta: metav1.ObjectMeta{ + Name: GetServerNameFromBMCandIndex(0, bmc), + }, + } + DeferCleanup(k8sClient.Delete, server) }) It("Should successfully reconcile the a BMC resource", func(ctx SpecContext) { diff --git a/internal/controller/endpoint_controller.go b/internal/controller/endpoint_controller.go index 0f4d09a..78d237b 100644 --- a/internal/controller/endpoint_controller.go +++ b/internal/controller/endpoint_controller.go @@ -121,7 +121,7 @@ func (r *EndpointReconciler) reconcile(ctx context.Context, log logr.Logger, end if err := r.applyBMC(ctx, log, endpoint, bmcSecret, m); err != nil { return ctrl.Result{}, fmt.Errorf("failed to apply BMC object: %w", err) } - log.V(1).Info("Applied local test BMC object for endpoint") + log.V(1).Info("Applied BMC object for Endpoint") } // TODO: other types like Switches can be handled here later } diff --git a/internal/controller/endpoint_controller_test.go b/internal/controller/endpoint_controller_test.go index a17bd38..850037a 100644 --- a/internal/controller/endpoint_controller_test.go +++ b/internal/controller/endpoint_controller_test.go @@ -51,6 +51,7 @@ var _ = Describe("Endpoints Controller", func() { "username": []byte(base64.StdEncoding.EncodeToString([]byte("foo"))), "password": []byte(base64.StdEncoding.EncodeToString([]byte("bar"))), })))) + DeferCleanup(k8sClient.Delete, bmcSecret) By("By ensuring that the BMC object has been created") bmc := &metalv1alpha1.BMC{ diff --git a/internal/controller/server_controller.go b/internal/controller/server_controller.go index 9aab354..4729989 100644 --- a/internal/controller/server_controller.go +++ b/internal/controller/server_controller.go @@ -57,6 +57,7 @@ type ServerReconciler struct { RegistryURL string ProbeOSImage string RequeueInterval time.Duration + EnforceFirstBoot bool } //+kubebuilder:rbac:groups=metal.ironcore.dev,resources=bmcs,verbs=get;list;watch @@ -152,12 +153,12 @@ func (r *ServerReconciler) reconcile(ctx context.Context, log logr.Logger, serve if err := r.applyBiosSettings(ctx, log, server); err != nil { return ctrl.Result{}, fmt.Errorf("failed to update server bios settings: %w", err) } - log.V(1).Info("Updated Server bios settings") + log.V(1).Info("Updated Server BIOS settings") if err := r.applyBootOrder(ctx, log, server); err != nil { return ctrl.Result{}, fmt.Errorf("failed to update server bios boot order: %w", err) } - log.V(1).Info("Updated Server bios boot order") + log.V(1).Info("Updated Server BIOS boot order") requeue, err := r.ensureServerStateTransition(ctx, log, server) if requeue && err == nil { @@ -215,6 +216,16 @@ func (r *ServerReconciler) ensureServerStateTransition(ctx context.Context, log } func (r *ServerReconciler) handleInitialState(ctx context.Context, log logr.Logger, server *metalv1alpha1.Server) (bool, error) { + if requeue, err := r.ensureInitialConditions(ctx, log, server); err != nil || requeue { + return requeue, err + } + log.V(1).Info("Initial conditions for Server met") + + if err := r.ensureServerPowerState(ctx, log, server); err != nil { + return false, fmt.Errorf("failed to ensure server power state: %w", err) + } + log.V(1).Info("Ensured power state for Server") + if err := r.applyBootConfigurationAndIgnitionForDiscovery(ctx, log, server); err != nil { return false, fmt.Errorf("failed to apply server boot configuration: %w", err) } @@ -232,6 +243,12 @@ func (r *ServerReconciler) handleInitialState(ctx context.Context, log logr.Logg } func (r *ServerReconciler) handleDiscoveryState(ctx context.Context, log logr.Logger, server *metalv1alpha1.Server) (bool, error) { + if ready, err := r.serverBootConfigurationIsReady(ctx, server); err != nil || !ready { + log.V(1).Info("Server boot configuration is not ready. Retrying ...") + return true, err + } + log.V(1).Info("Server boot configuration is ready") + serverBase := server.DeepCopy() server.Spec.Power = metalv1alpha1.PowerOn if err := r.Patch(ctx, server, client.MergeFrom(serverBase)); err != nil { @@ -244,12 +261,6 @@ func (r *ServerReconciler) handleDiscoveryState(ctx context.Context, log logr.Lo } log.V(1).Info("Server state set to power on") - if ready, err := r.serverBootConfigurationIsReady(ctx, server); err != nil || !ready { - log.V(1).Info("Server boot configuration is not ready. Retrying ...") - return true, err - } - log.V(1).Info("Server boot configuration is ready") - ready, err := r.extractServerDetailsFromRegistry(ctx, log, server) if !ready && err == nil { log.V(1).Info("Server agent did not post info to registry") @@ -480,6 +491,47 @@ func (r *ServerReconciler) generateDefaultIgnitionDataForServer(flags string) ([ return ignitionData, nil } +func (r *ServerReconciler) ensureInitialConditions(ctx context.Context, log logr.Logger, server *metalv1alpha1.Server) (bool, error) { + if server.Spec.Power == "" && server.Status.PowerState == metalv1alpha1.ServerOffPowerState { + requeue, err := r.setAndPatchServerPowerState(ctx, log, server, metalv1alpha1.PowerOff) + if err != nil { + return false, fmt.Errorf("failed to set server power state: %w", err) + } + if requeue { + return requeue, nil + } + } + + if server.Status.State == metalv1alpha1.ServerStateInitial && + server.Status.PowerState == metalv1alpha1.ServerOnPowerState && + r.EnforceFirstBoot { + log.V(1).Info("Server in initial state is powered on. Ensure that it is powered off.") + requeue, err := r.setAndPatchServerPowerState(ctx, log, server, metalv1alpha1.PowerOff) + if err != nil { + return false, fmt.Errorf("failed to set server power state: %w", err) + } + if requeue { + return requeue, nil + } + } + return false, nil +} + +func (r *ServerReconciler) setAndPatchServerPowerState(ctx context.Context, log logr.Logger, server *metalv1alpha1.Server, powerState metalv1alpha1.Power) (bool, error) { + op, err := controllerutil.CreateOrPatch(ctx, r.Client, server, func() error { + server.Spec.Power = powerState + return nil + }) + if err != nil { + return false, fmt.Errorf("failed to patch Server: %w", err) + } + if op == controllerutil.OperationResultUpdated { + log.V(1).Info("Server updated to power off state.") + return true, nil + } + return false, nil +} + func (r *ServerReconciler) serverBootConfigurationIsReady(ctx context.Context, server *metalv1alpha1.Server) (bool, error) { if server.Spec.BootConfigurationRef == nil { return false, nil @@ -502,7 +554,11 @@ func (r *ServerReconciler) pxeBootServer(ctx context.Context, log logr.Logger, s } bmcClient, err := GetBMCClientForServer(ctx, r.Client, server, r.Insecure) - defer bmcClient.Logout() + defer func() { + if bmcClient != nil { + bmcClient.Logout() + } + }() if err != nil { return fmt.Errorf("failed to get BMC client: %w", err) @@ -520,6 +576,10 @@ func (r *ServerReconciler) extractServerDetailsFromRegistry(ctx context.Context, return false, nil } + if resp == nil { + return false, fmt.Errorf("failed to find server information in registry") + } + if err != nil { return false, fmt.Errorf("failed to fetch server details: %w", err) } @@ -585,17 +645,21 @@ func (r *ServerReconciler) ensureServerPowerState(ctx context.Context, log logr. } bmcClient, err := GetBMCClientForServer(ctx, r.Client, server, r.Insecure) - defer bmcClient.Logout() + defer func() { + if bmcClient != nil { + bmcClient.Logout() + } + }() if err != nil { return fmt.Errorf("failed to get BMC client: %w", err) } - if powerOp == powerOpOn { + switch powerOp { + case powerOpOn: if err := bmcClient.PowerOn(server.Spec.UUID); err != nil { return fmt.Errorf("failed to power on server: %w", err) } - } - if powerOp == powerOpOff { + case powerOpOff: if err := bmcClient.PowerOff(server.Spec.UUID); err != nil { return fmt.Errorf("failed to power off server: %w", err) } @@ -663,28 +727,6 @@ func (r *ServerReconciler) invalidateRegistryEntryForServer(log logr.Logger, ser return nil } -// SetupWithManager sets up the controller with the Manager. -func (r *ServerReconciler) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&metalv1alpha1.Server{}). - Watches( - &metalv1alpha1.ServerBootConfiguration{}, - r.enqueueServerByServerBootConfiguration(), - ). - Complete(r) -} - -func (r *ServerReconciler) enqueueServerByServerBootConfiguration() handler.EventHandler { - return handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []ctrl.Request { - config := obj.(*metalv1alpha1.ServerBootConfiguration) - return []ctrl.Request{ - { - NamespacedName: types.NamespacedName{Name: config.Spec.ServerRef.Name}, - }, - } - }) -} - func (r *ServerReconciler) applyBootOrder(ctx context.Context, log logr.Logger, server *metalv1alpha1.Server) error { if server.Spec.BMCRef == nil && server.Spec.BMC == nil { log.V(1).Info("Server has no BMC connection configured") @@ -764,8 +806,30 @@ func (r *ServerReconciler) applyBiosSettings(ctx context.Context, log logr.Logge } } if !versionMatch { - log.V(1).Info("none of the Bios versions match") + log.V(1).Info("None of the Bios versions match") return nil } return nil } + +// SetupWithManager sets up the controller with the Manager. +func (r *ServerReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&metalv1alpha1.Server{}). + Watches( + &metalv1alpha1.ServerBootConfiguration{}, + r.enqueueServerByServerBootConfiguration(), + ). + Complete(r) +} + +func (r *ServerReconciler) enqueueServerByServerBootConfiguration() handler.EventHandler { + return handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []ctrl.Request { + config := obj.(*metalv1alpha1.ServerBootConfiguration) + return []ctrl.Request{ + { + NamespacedName: types.NamespacedName{Name: config.Spec.ServerRef.Name}, + }, + } + }) +} diff --git a/internal/controller/server_controller_test.go b/internal/controller/server_controller_test.go index bfc5442..c62d3ba 100644 --- a/internal/controller/server_controller_test.go +++ b/internal/controller/server_controller_test.go @@ -40,6 +40,7 @@ var _ = Describe("Server Controller", func() { }, } Expect(k8sClient.Create(ctx, endpoint)).To(Succeed()) + DeferCleanup(k8sClient.Delete, endpoint) By("Ensuring that the BMC resource has been created for an endpoint") bmc = &metalv1alpha1.BMC{ @@ -48,6 +49,16 @@ var _ = Describe("Server Controller", func() { }, } Eventually(Get(bmc)).Should(Succeed()) + DeferCleanup(k8sClient.Delete, bmc) + + By("Ensuring that the BMCSecret will be removed") + bmcSecret := &metalv1alpha1.BMCSecret{ + ObjectMeta: metav1.ObjectMeta{ + Name: bmc.Name, + }, + } + Eventually(Get(bmcSecret)).Should(Succeed()) + DeferCleanup(k8sClient.Delete, bmcSecret) By("Ensuring that the Server resource has been created") server := &metalv1alpha1.Server{ @@ -66,15 +77,17 @@ var _ = Describe("Server Controller", func() { BlockOwnerDeletion: ptr.To(true), })), HaveField("Spec.UUID", "38947555-7742-3448-3784-823347823834"), - HaveField("Spec.Power", metalv1alpha1.Power("")), + HaveField("Spec.Power", metalv1alpha1.PowerOff), HaveField("Spec.IndicatorLED", metalv1alpha1.IndicatorLED("")), HaveField("Spec.ServerClaimRef", BeNil()), HaveField("Status.Manufacturer", "Contoso"), HaveField("Status.SKU", "8675309"), HaveField("Status.SerialNumber", "437XR1138R2"), HaveField("Status.IndicatorLED", metalv1alpha1.OffIndicatorLED), - HaveField("Status.State", metalv1alpha1.ServerStateInitial), + HaveField("Status.State", metalv1alpha1.ServerStateDiscovery), + HaveField("Status.PowerState", metalv1alpha1.ServerOffPowerState), )) + DeferCleanup(k8sClient.Delete, server) By("Ensuring the boot configuration has been created") bootConfig := &metalv1alpha1.ServerBootConfiguration{ @@ -110,6 +123,11 @@ var _ = Describe("Server Controller", func() { HaveField("Data", HaveKeyWithValue("ignition", MatchYAML(testdata.DefaultIgnition))), )) + By("Patching the boot configuration to a Ready state") + Eventually(UpdateStatus(bootConfig, func() { + bootConfig.Status.State = metalv1alpha1.ServerBootConfigurationStateReady + })).Should(Succeed()) + By("Ensuring that the Server is set to discovery and powered on") Eventually(Object(server)).Should(SatisfyAll( HaveField("Finalizers", ContainElement(ServerFinalizer)), @@ -132,11 +150,6 @@ var _ = Describe("Server Controller", func() { HaveField("Status.State", metalv1alpha1.ServerStateDiscovery), )) - By("Patching the boot configuration to a Ready state") - Eventually(UpdateStatus(bootConfig, func() { - bootConfig.Status.State = metalv1alpha1.ServerBootConfigurationStateReady - })).Should(Succeed()) - By("Starting the probe agent") probeAgent := probe.NewAgent(server.Spec.UUID, registryURL) go func() { @@ -165,7 +178,7 @@ var _ = Describe("Server Controller", func() { By("Creating a BMCSecret") bmcSecret := &metalv1alpha1.BMCSecret{ ObjectMeta: metav1.ObjectMeta{ - GenerateName: "foo-", + GenerateName: "test-", }, Data: map[string][]byte{ "username": []byte(base64.StdEncoding.EncodeToString([]byte("foo"))), @@ -231,6 +244,11 @@ var _ = Describe("Server Controller", func() { HaveField("Data", HaveKeyWithValue("ignition", MatchYAML(testdata.DefaultIgnition))), )) + By("Patching the boot configuration to a Ready state") + Eventually(UpdateStatus(bootConfig, func() { + bootConfig.Status.State = metalv1alpha1.ServerBootConfigurationStateReady + })).Should(Succeed()) + By("Ensuring that the Server resource has been created") Eventually(Object(server)).Should(SatisfyAll( HaveField("Finalizers", ContainElement(ServerFinalizer)), @@ -252,11 +270,6 @@ var _ = Describe("Server Controller", func() { HaveField("Status.State", metalv1alpha1.ServerStateDiscovery), )) - By("Patching the boot configuration to a Ready state") - Eventually(UpdateStatus(bootConfig, func() { - bootConfig.Status.State = metalv1alpha1.ServerBootConfigurationStateReady - })).Should(Succeed()) - By("Starting the probe agent") probeAgent := probe.NewAgent(server.Spec.UUID, registryURL) go func() { @@ -267,6 +280,7 @@ var _ = Describe("Server Controller", func() { By("Ensuring that the server is set to available and powered off") Eventually(Object(server)).Should(SatisfyAll( HaveField("Spec.BootConfigurationRef", BeNil()), + HaveField("Spec.Power", metalv1alpha1.PowerOff), HaveField("Status.State", metalv1alpha1.ServerStateAvailable), HaveField("Status.PowerState", metalv1alpha1.ServerOffPowerState), HaveField("Status.NetworkInterfaces", Not(BeEmpty())), diff --git a/internal/controller/serverclaim_controller.go b/internal/controller/serverclaim_controller.go index 2d2fae1..846597a 100644 --- a/internal/controller/serverclaim_controller.go +++ b/internal/controller/serverclaim_controller.go @@ -153,36 +153,72 @@ func (r *ServerClaimReconciler) reconcile(ctx context.Context, log logr.Logger, return ctrl.Result{}, nil } - if modified, err := r.patchServerRef(ctx, claim, server.Name); err != nil || modified { + if modified, err := r.patchServerRef(ctx, claim, server); err != nil || modified { return ctrl.Result{}, err } + log.V(1).Info("Patched ServerRef in Claim") if err := r.applyBootConfiguration(ctx, log, server, claim); err != nil { return ctrl.Result{}, fmt.Errorf("failed to apply boot configuration: %w", err) } + log.V(1).Info("Applied BootConfiguration for ServerClaim") - serverBase := server.DeepCopy() - server.Spec.ServerClaimRef = &v1.ObjectReference{ - APIVersion: "metal.ironcore.dev/v1alpha1", - Kind: "ServerClaim", - Namespace: claim.Namespace, - Name: claim.Name, - UID: claim.UID, - } - server.Spec.Power = claim.Spec.Power - if err := r.Patch(ctx, server, client.MergeFrom(serverBase)); err != nil { - return ctrl.Result{}, fmt.Errorf("failed to patch claim ref for server: %w", err) + if modified, err := r.ensureObjectRefForServer(ctx, log, claim, server); err != nil || modified { + return ctrl.Result{}, err } - log.V(1).Info("Patched server claim reference", "Server", server.Name, "ServerClaimRef", claim.Name) + log.V(1).Info("Ensured ObjectRef for Server", "Server", server.Name) if modified, err := r.patchServerClaimPhase(ctx, claim, metalv1alpha1.PhaseBound); err != nil || modified { return ctrl.Result{}, err } + log.V(1).Info("Patched ServerClaim phase", "Phase", claim.Status.Phase) + + if modified, err := r.ensurePowerStateForServer(ctx, log, claim, server); err != nil || modified { + return ctrl.Result{}, err + } + log.V(1).Info("Ensured PowerState for Server", "Server", server.Name) log.V(1).Info("Reconciled server claim") return ctrl.Result{}, nil } +func (r *ServerClaimReconciler) ensureObjectRefForServer(ctx context.Context, log logr.Logger, claim *metalv1alpha1.ServerClaim, server *metalv1alpha1.Server) (bool, error) { + if server.Spec.ServerClaimRef != nil { + return false, nil + } + + if server.Spec.ServerClaimRef == nil { + serverBase := server.DeepCopy() + server.Spec.ServerClaimRef = &v1.ObjectReference{ + APIVersion: "metal.ironcore.dev/v1alpha1", + Kind: "ServerClaim", + Namespace: claim.Namespace, + Name: claim.Name, + UID: claim.UID, + } + if err := r.Patch(ctx, server, client.MergeFrom(serverBase)); err != nil { + return false, fmt.Errorf("failed to patch claim ref for server: %w", err) + } + log.V(1).Info("Patched ServerClaim reference on Server", "Server", server.Name, "ServerClaimRef", claim.Name) + } + return true, nil +} + +func (r *ServerClaimReconciler) ensurePowerStateForServer(ctx context.Context, log logr.Logger, claim *metalv1alpha1.ServerClaim, server *metalv1alpha1.Server) (bool, error) { + if server.Spec.Power == claim.Spec.Power { + return false, nil + } + if server.Spec.ServerClaimRef != nil { + serverBase := server.DeepCopy() + server.Spec.Power = claim.Spec.Power + if err := r.Patch(ctx, server, client.MergeFrom(serverBase)); err != nil { + return false, fmt.Errorf("failed to patch power for server: %w", err) + } + log.V(1).Info("Patched desired Power of the claimed Server", "Server", server.Name) + } + return true, nil +} + func (r *ServerClaimReconciler) patchServerClaimPhase(ctx context.Context, claim *metalv1alpha1.ServerClaim, phase metalv1alpha1.Phase) (bool, error) { if claim.Status.Phase == phase { return false, nil @@ -195,17 +231,17 @@ func (r *ServerClaimReconciler) patchServerClaimPhase(ctx context.Context, claim return true, nil } -func (r *ServerClaimReconciler) patchServerRef(ctx context.Context, claim *metalv1alpha1.ServerClaim, server string) (bool, error) { +func (r *ServerClaimReconciler) patchServerRef(ctx context.Context, claim *metalv1alpha1.ServerClaim, server *metalv1alpha1.Server) (bool, error) { if claim.Spec.ServerRef == nil { claimBase := claim.DeepCopy() - claim.Spec.ServerRef = &v1.LocalObjectReference{Name: server} + claim.Spec.ServerRef = &v1.LocalObjectReference{Name: server.Name} if err := r.Patch(ctx, claim, client.MergeFrom(claimBase)); err != nil { return false, err } return true, nil } - if claim.Spec.ServerRef != nil && claim.Spec.ServerRef.Name == server { + if claim.Spec.ServerRef != nil && claim.Spec.ServerRef.Name == server.Name { return false, nil } diff --git a/internal/controller/serverclaim_controller_test.go b/internal/controller/serverclaim_controller_test.go index 48a89ea..d5bf556 100644 --- a/internal/controller/serverclaim_controller_test.go +++ b/internal/controller/serverclaim_controller_test.go @@ -4,7 +4,7 @@ package controller import ( - "fmt" + "encoding/base64" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -20,41 +20,43 @@ import ( var _ = Describe("ServerClaim Controller", func() { ns := SetupTest() - var server metalv1alpha1.Server + var server *metalv1alpha1.Server BeforeEach(func(ctx SpecContext) { - By("Creating an Endpoints object") - endpoint := &metalv1alpha1.Endpoint{ + By("Creating a BMCSecret") + bmcSecret := &metalv1alpha1.BMCSecret{ ObjectMeta: metav1.ObjectMeta{ GenerateName: "test-", }, - Spec: metalv1alpha1.EndpointSpec{ - // emulator BMC mac address - MACAddress: "23:11:8A:33:CF:EA", - IP: metalv1alpha1.MustParseIP("127.0.0.1"), + Data: map[string][]byte{ + "username": []byte(base64.StdEncoding.EncodeToString([]byte("foo"))), + "password": []byte(base64.StdEncoding.EncodeToString([]byte("bar"))), }, } - Expect(k8sClient.Create(ctx, endpoint)).To(Succeed()) - DeferCleanup(k8sClient.Delete, endpoint) + Expect(k8sClient.Create(ctx, bmcSecret)).To(Succeed()) + DeferCleanup(k8sClient.Delete, bmcSecret) - By("Ensuring that the BMC resource has been created for an endpoint") - bmc := &metalv1alpha1.BMC{ + By("Creating a Server") + server = &metalv1alpha1.Server{ ObjectMeta: metav1.ObjectMeta{ - Name: fmt.Sprintf("bmc-%s", endpoint.Name), + GenerateName: "test-", }, - } - Eventually(Get(bmc)).Should(Succeed()) - DeferCleanup(k8sClient.Delete, bmc) - - By("Creating a Server object") - By("Ensuring that the Server resource has been created") - server = metalv1alpha1.Server{ - ObjectMeta: metav1.ObjectMeta{ - Name: GetServerNameFromBMCandIndex(0, bmc), + Spec: metalv1alpha1.ServerSpec{ + UUID: "38947555-7742-3448-3784-823347823834", + BMC: &metalv1alpha1.BMCAccess{ + Protocol: metalv1alpha1.Protocol{ + Name: metalv1alpha1.ProtocolRedfishLocal, + Port: 8000, + }, + Endpoint: "127.0.0.1", + BMCSecretRef: v1.LocalObjectReference{ + Name: bmcSecret.Name, + }, + }, }, } - Eventually(Get(&server)).Should(Succeed()) - DeferCleanup(k8sClient.Delete, &server) + Expect(k8sClient.Create(ctx, server)).Should(Succeed()) + DeferCleanup(k8sClient.Delete, server) }) It("should successfully claim a server in available state", func(ctx SpecContext) { @@ -87,12 +89,12 @@ var _ = Describe("ServerClaim Controller", func() { Expect(k8sClient.Create(ctx, claim)).To(Succeed()) By("Patching the Server to available state") - Eventually(UpdateStatus(&server, func() { + Eventually(UpdateStatus(server, func() { server.Status.State = metalv1alpha1.ServerStateAvailable })).Should(Succeed()) By("Ensuring that the Server has the correct claim ref") - Eventually(Object(&server)).Should(SatisfyAll( + Eventually(Object(server)).Should(SatisfyAll( HaveField("Spec.ServerClaimRef.Name", claim.Name), HaveField("Spec.Power", metalv1alpha1.PowerOn), HaveField("Status.State", metalv1alpha1.ServerStateReserved), @@ -128,7 +130,7 @@ var _ = Describe("ServerClaim Controller", func() { )) By("Ensuring that the server has a correct boot configuration ref") - Eventually(Object(&server)).Should(SatisfyAll( + Eventually(Object(server)).Should(SatisfyAll( HaveField("Spec.BootConfigurationRef", &v1.ObjectReference{ APIVersion: "metal.ironcore.dev/v1alpha1", Kind: "ServerBootConfiguration", @@ -142,7 +144,7 @@ var _ = Describe("ServerClaim Controller", func() { Expect(k8sClient.Delete(ctx, claim)).To(Succeed()) By("Ensuring that the Server is available") - Eventually(Object(&server)).Should(SatisfyAll( + Eventually(Object(server)).Should(SatisfyAll( HaveField("Spec.ServerClaimRef", BeNil()), HaveField("Spec.BootConfigurationRef", BeNil()), HaveField("Spec.Power", metalv1alpha1.PowerOff), @@ -152,7 +154,7 @@ var _ = Describe("ServerClaim Controller", func() { It("Should successfully claim a server by reference and label selector", func(ctx SpecContext) { By("Patching Server labels") - Eventually(Update(&server, func() { + Eventually(Update(server, func() { server.Labels = map[string]string{ "type": "storage", "env": "staging", @@ -182,12 +184,12 @@ var _ = Describe("ServerClaim Controller", func() { Expect(k8sClient.Create(ctx, claim)).To(Succeed()) By("Patching the Server to available state") - Eventually(UpdateStatus(&server, func() { + Eventually(UpdateStatus(server, func() { server.Status.State = metalv1alpha1.ServerStateAvailable })).Should(Succeed()) By("Ensuring that the Server has the correct claim ref") - Eventually(Object(&server)).Should(SatisfyAll( + Eventually(Object(server)).Should(SatisfyAll( HaveField("Spec.ServerClaimRef.Name", claim.Name), HaveField("Spec.Power", metalv1alpha1.PowerOff), HaveField("Status.State", metalv1alpha1.ServerStateReserved), @@ -205,7 +207,7 @@ var _ = Describe("ServerClaim Controller", func() { Expect(k8sClient.Delete(ctx, claim)).To(Succeed()) By("Ensuring that the Server is available") - Eventually(Object(&server)).Should(SatisfyAll( + Eventually(Object(server)).Should(SatisfyAll( HaveField("Spec.ServerClaimRef", BeNil()), HaveField("Spec.BootConfigurationRef", BeNil()), HaveField("Spec.Power", metalv1alpha1.PowerOff), @@ -215,13 +217,23 @@ var _ = Describe("ServerClaim Controller", func() { It("should successfully claim a server by label selector", func(ctx SpecContext) { By("Patching Server labels") - Eventually(Update(&server, func() { + Eventually(Update(server, func() { server.Labels = map[string]string{ "type": "storage", "env": "prod", } })).Should(Succeed()) + By("Patching the Server to available state") + Eventually(UpdateStatus(server, func() { + server.Status.State = metalv1alpha1.ServerStateAvailable + })).Should(Succeed()) + + Eventually(Object(server)).Should(SatisfyAll( + HaveField("Spec.ServerClaimRef", BeNil()), + HaveField("Status.State", metalv1alpha1.ServerStateAvailable), + )) + By("Creating a ServerClaim") claim := &metalv1alpha1.ServerClaim{ ObjectMeta: metav1.ObjectMeta{ @@ -234,8 +246,8 @@ var _ = Describe("ServerClaim Controller", func() { MatchLabels: map[string]string{"type": "storage"}, MatchExpressions: []metav1.LabelSelectorRequirement{{ Key: "env", - Operator: metav1.LabelSelectorOpNotIn, - Values: []string{"test", "staging"}, + Operator: metav1.LabelSelectorOpIn, + Values: []string{"prod"}, }}, }, Image: "foo:bar", @@ -243,39 +255,46 @@ var _ = Describe("ServerClaim Controller", func() { } Expect(k8sClient.Create(ctx, claim)).To(Succeed()) - By("Patching the Server to available state") - Eventually(UpdateStatus(&server, func() { - server.Status.State = metalv1alpha1.ServerStateAvailable - })).Should(Succeed()) - - By("Ensuring that the Server has the correct claim ref") - Eventually(Object(&server)).Should(SatisfyAll( - HaveField("Spec.ServerClaimRef.Name", claim.Name), - HaveField("Spec.Power", metalv1alpha1.PowerOff), - HaveField("Status.State", metalv1alpha1.ServerStateReserved), - )) - By("Ensuring that the ServerClaim is bound") Eventually(Object(claim)).Should(SatisfyAll( HaveField("Finalizers", ContainElement(ServerClaimFinalizer)), + HaveField("Spec.ServerRef", Equal(&v1.LocalObjectReference{Name: server.Name})), HaveField("Status.Phase", metalv1alpha1.PhaseBound), - HaveField("Spec.ServerRef", Not(BeNil())), - HaveField("Spec.ServerRef.Name", server.Name), + )) + + By("Ensuring that the Server has the correct claim ref") + Eventually(Object(server)).Should(SatisfyAll( + HaveField("Spec.ServerClaimRef", &v1.ObjectReference{ + APIVersion: "metal.ironcore.dev/v1alpha1", + Kind: "ServerClaim", + Name: claim.Name, + Namespace: claim.Namespace, + UID: claim.UID, + }), + HaveField("Spec.Power", metalv1alpha1.PowerOff), + HaveField("Status.State", metalv1alpha1.ServerStateReserved), + HaveField("Status.PowerState", metalv1alpha1.ServerOffPowerState), )) By("Deleting the ServerClaim") Expect(k8sClient.Delete(ctx, claim)).To(Succeed()) By("Ensuring that the Server is available") - Eventually(Object(&server)).Should(SatisfyAll( + Eventually(Object(server)).Should(SatisfyAll( HaveField("Spec.ServerClaimRef", BeNil()), HaveField("Spec.BootConfigurationRef", BeNil()), HaveField("Spec.Power", metalv1alpha1.PowerOff), HaveField("Status.State", metalv1alpha1.ServerStateAvailable), + HaveField("Status.PowerState", metalv1alpha1.ServerOffPowerState), )) }) It("should not claim a server in a non-available state", func(ctx SpecContext) { + By("Patching the Server to available state") + Eventually(UpdateStatus(server, func() { + server.Status.State = metalv1alpha1.ServerStateInitial + })).Should(Succeed()) + By("Creating a ServerClaim") claim := &metalv1alpha1.ServerClaim{ ObjectMeta: metav1.ObjectMeta{ @@ -291,13 +310,8 @@ var _ = Describe("ServerClaim Controller", func() { Expect(k8sClient.Create(ctx, claim)).To(Succeed()) DeferCleanup(k8sClient.Delete, claim) - By("Patching the Server to available state") - Eventually(UpdateStatus(&server, func() { - server.Status.State = metalv1alpha1.ServerStateInitial - })).Should(Succeed()) - By("Ensuring that the Server has no claim ref") - Eventually(Object(&server)).Should(SatisfyAll( + Eventually(Object(server)).Should(SatisfyAll( HaveField("Spec.ServerClaimRef", BeNil()), HaveField("Status.State", metalv1alpha1.ServerStateInitial), )) @@ -320,7 +334,7 @@ var _ = Describe("ServerClaim Controller", func() { It("should not claim a server with set claim ref", func(ctx SpecContext) { By("Patching the Server to available state") - Eventually(Update(&server, func() { + Eventually(Update(server, func() { server.Spec.ServerClaimRef = &v1.ObjectReference{ APIVersion: "metal.ironcore.dev/v1alpha1", Kind: "ServerClaim", @@ -346,7 +360,7 @@ var _ = Describe("ServerClaim Controller", func() { DeferCleanup(k8sClient.Delete, claim) By("Ensuring that the Server has no claim ref") - Eventually(Object(&server)).Should(SatisfyAll( + Eventually(Object(server)).Should(SatisfyAll( HaveField("Spec.ServerClaimRef", &v1.ObjectReference{ APIVersion: "metal.ironcore.dev/v1alpha1", Kind: "ServerClaim", @@ -398,12 +412,12 @@ var _ = Describe("ServerClaim Controller", func() { DeferCleanup(k8sClient.Delete, claim) By("Patching the Server to available state") - Eventually(UpdateStatus(&server, func() { + Eventually(UpdateStatus(server, func() { server.Status.State = metalv1alpha1.ServerStateAvailable })).Should(Succeed()) By("Ensuring that the Server has no claim ref") - Eventually(Object(&server)).Should(SatisfyAll( + Eventually(Object(server)).Should(SatisfyAll( HaveField("Spec.ServerClaimRef", BeNil()), )) diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go index 0aaba69..b83c0c4 100644 --- a/internal/controller/suite_test.go +++ b/internal/controller/suite_test.go @@ -32,16 +32,16 @@ import ( ) const ( - pollingInterval = 100 * time.Millisecond + pollingInterval = 50 * time.Millisecond eventuallyTimeout = 3 * time.Second - consistentlyDuration = 3 * time.Second + consistentlyDuration = 1 * time.Second ) var ( cfg *rest.Config k8sClient client.Client testEnv *envtest.Environment - registryURL = "http://localhost:12345" + registryURL = "http://localhost:30000" ) func TestControllers(t *testing.T) { @@ -97,7 +97,7 @@ var _ = BeforeSuite(func() { var mgrCtx context.Context mgrCtx, cancel := context.WithCancel(context.Background()) DeferCleanup(cancel) - registryServer := registry.NewServer(":12345") + registryServer := registry.NewServer(":30000") go func() { defer GinkgoRecover() Expect(registryServer.Start(mgrCtx)).To(Succeed(), "failed to start registry server") @@ -170,6 +170,7 @@ func SetupTest() *corev1.Namespace { ProbeOSImage: "fooOS:latest", RegistryURL: registryURL, RequeueInterval: 50 * time.Millisecond, + EnforceFirstBoot: true, }).SetupWithManager(k8sManager)).To(Succeed()) Expect((&ServerClaimReconciler{ diff --git a/internal/controller/testdata/ignition.go b/internal/controller/testdata/ignition.go index 6b246c1..89a928e 100644 --- a/internal/controller/testdata/ignition.go +++ b/internal/controller/testdata/ignition.go @@ -36,7 +36,7 @@ systemd: ExecStartPre=-/usr/bin/docker stop metalprobe ExecStartPre=-/usr/bin/docker rm metalprobe ExecStartPre=/usr/bin/docker pull foo:latest - ExecStart=/usr/bin/docker run --network host --privileged --name metalprobe foo:latest --registry-url=http://localhost:12345 --server-uuid=38947555-7742-3448-3784-823347823834 + ExecStart=/usr/bin/docker run --network host --privileged --name metalprobe foo:latest --registry-url=http://localhost:30000 --server-uuid=38947555-7742-3448-3784-823347823834 ExecStop=/usr/bin/docker stop metalprobe [Install] WantedBy=multi-user.target diff --git a/internal/probe/probe_suite_test.go b/internal/probe/probe_suite_test.go index 3190e19..7eeb40d 100644 --- a/internal/probe/probe_suite_test.go +++ b/internal/probe/probe_suite_test.go @@ -19,8 +19,8 @@ var ( probeAgent *probe.Agent registryServer *registry.Server - registryAddr = ":5432" - registryURL = "http://localhost:5432" + registryAddr = ":30001" + registryURL = "http://localhost:30001" systemUUID = "1234-5678" ) diff --git a/internal/registry/registry_suite_test.go b/internal/registry/registry_suite_test.go index ed3c5a6..be1fcbb 100644 --- a/internal/registry/registry_suite_test.go +++ b/internal/registry/registry_suite_test.go @@ -16,8 +16,8 @@ import ( var ( server *registry.Server - testServerURL = "http://localhost:54321" - testServerAddr = ":54321" + testServerURL = "http://localhost:30002" + testServerAddr = ":30002" ) func TestRegistry(t *testing.T) {