diff --git a/api/v1alpha1/machine_types.go b/api/v1alpha1/machine_types.go index a38bfd30..a66a3ad0 100644 --- a/api/v1alpha1/machine_types.go +++ b/api/v1alpha1/machine_types.go @@ -22,6 +22,7 @@ type MachineSpec struct { OOBRef v1.LocalObjectReference `json:"oobRef"` + // +optional InventoryRef *v1.LocalObjectReference `json:"inventoryRef,omitempty"` // +optional @@ -116,7 +117,7 @@ const ( // +kubebuilder:object:root=true // +kubebuilder:subresource:status // +kubebuilder:resource:scope=Cluster -// +kubebuilder:printcolumn:name="UUID",type=string,JSONPath=`.status.uuid` +// +kubebuilder:printcolumn:name="UUID",type=string,JSONPath=`.spec.uuid` // +kubebuilder:printcolumn:name="Manufacturer",type=string,JSONPath=`.status.manufacturer` // +kubebuilder:printcolumn:name="SKU",type=string,JSONPath=`.status.sku`,priority=100 // +kubebuilder:printcolumn:name="SerialNumber",type=string,JSONPath=`.status.serialNumber`,priority=100 diff --git a/config/crd/bases/metal.ironcore.dev_machines.yaml b/config/crd/bases/metal.ironcore.dev_machines.yaml index ab2a41f8..ff24d77f 100644 --- a/config/crd/bases/metal.ironcore.dev_machines.yaml +++ b/config/crd/bases/metal.ironcore.dev_machines.yaml @@ -15,7 +15,7 @@ spec: scope: Cluster versions: - additionalPrinterColumns: - - jsonPath: .status.uuid + - jsonPath: .spec.uuid name: UUID type: string - jsonPath: .status.manufacturer diff --git a/internal/bmc/fake.go b/internal/bmc/fake.go index a231cab3..d51df9d6 100644 --- a/internal/bmc/fake.go +++ b/internal/bmc/fake.go @@ -83,11 +83,6 @@ func (b *FakeBMC) DeleteUsers(_ context.Context, _ *regexp.Regexp) error { } func (b *FakeBMC) ReadInfo(_ context.Context) (Info, error) { - id, err := uuid.NewRandom() - if err != nil { - return Info{}, fmt.Errorf("cannot generate UUID: %w", err) - } - return Info{ Type: TypeMachine, Manufacturer: "Fake", @@ -95,7 +90,7 @@ func (b *FakeBMC) ReadInfo(_ context.Context) (Info, error) { FirmwareVersion: "1", Machines: []Machine{ { - UUID: id.String(), + UUID: uuid.NewSHA1(uuid.NameSpaceOID, []byte("Fake")).String(), Manufacturer: "Fake", SKU: "Fake-0", SerialNumber: "1", diff --git a/internal/controller/indexes.go b/internal/controller/indexes.go index c755a20d..64898f15 100644 --- a/internal/controller/indexes.go +++ b/internal/controller/indexes.go @@ -15,8 +15,20 @@ import ( func CreateIndexes(ctx context.Context, mgr manager.Manager) error { indexer := mgr.GetFieldIndexer() + var err error - err := indexer.IndexField(ctx, &metalv1alpha1.MachineClaim{}, MachineClaimSpecMachineRef, func(obj client.Object) []string { + err = indexer.IndexField(ctx, &metalv1alpha1.Machine{}, MachineSpecOOBRefName, func(obj client.Object) []string { + machine := obj.(*metalv1alpha1.Machine) + if machine.Spec.OOBRef.Name == "" { + return nil + } + return []string{machine.Spec.OOBRef.Name} + }) + if err != nil { + return fmt.Errorf("cannot index field %s: %w", MachineSpecOOBRefName, err) + } + + err = indexer.IndexField(ctx, &metalv1alpha1.MachineClaim{}, MachineClaimSpecMachineRefName, func(obj client.Object) []string { claim := obj.(*metalv1alpha1.MachineClaim) if claim.Spec.MachineRef == nil || claim.Spec.MachineRef.Name == "" { return nil @@ -24,7 +36,7 @@ func CreateIndexes(ctx context.Context, mgr manager.Manager) error { return []string{claim.Spec.MachineRef.Name} }) if err != nil { - return fmt.Errorf("cannot index field %s: %w", MachineClaimSpecMachineRef, err) + return fmt.Errorf("cannot index field %s: %w", MachineClaimSpecMachineRefName, err) } err = indexer.IndexField(ctx, &metalv1alpha1.OOB{}, OOBSpecMACAddress, func(obj client.Object) []string { diff --git a/internal/controller/machine_controller.go b/internal/controller/machine_controller.go index 1127dfc1..47d34c78 100644 --- a/internal/controller/machine_controller.go +++ b/internal/controller/machine_controller.go @@ -17,6 +17,12 @@ import ( // +kubebuilder:rbac:groups=metal.ironcore.dev,resources=machines/status,verbs=get;update;patch // +kubebuilder:rbac:groups=metal.ironcore.dev,resources=machines/finalizers,verbs=update +const ( + MachineFieldManager = "metal.ironcore.dev/machine" + MachineFinalizer = "metal.ironcore.dev/machine" + MachineSpecOOBRefName = ".spec.oobRef.Name" +) + func NewMachineReconciler() (*MachineReconciler, error) { return &MachineReconciler{}, nil } diff --git a/internal/controller/machineclaim_controller.go b/internal/controller/machineclaim_controller.go index 76d6f872..f618fb6d 100644 --- a/internal/controller/machineclaim_controller.go +++ b/internal/controller/machineclaim_controller.go @@ -31,9 +31,9 @@ import ( // +kubebuilder:rbac:groups=metal.ironcore.dev,resources=machines/finalizers,verbs=update const ( - MachineClaimFieldManager = "metal.ironcore.dev/machineclaim" - MachineClaimFinalizer = "metal.ironcore.dev/machineclaim" - MachineClaimSpecMachineRef = ".spec.machineRef.Name" + MachineClaimFieldManager = "metal.ironcore.dev/machineclaim" + MachineClaimFinalizer = "metal.ironcore.dev/machineclaim" + MachineClaimSpecMachineRefName = ".spec.machineRef.Name" ) func NewMachineClaimReconciler() (*MachineClaimReconciler, error) { @@ -393,7 +393,9 @@ func (r *MachineClaimReconciler) enqueueMachineClaimsFromMachine(ctx context.Con machine := obj.(*metalv1alpha1.Machine) claimList := metalv1alpha1.MachineClaimList{} - err := r.List(ctx, &claimList, client.MatchingFields{MachineClaimSpecMachineRef: machine.Name}) + err := r.List(ctx, &claimList, client.MatchingFields{ + MachineClaimSpecMachineRefName: machine.Name, + }) if err != nil { log.Error(ctx, fmt.Errorf("cannot list MachineClaims: %w", err)) return nil diff --git a/internal/controller/oob_controller.go b/internal/controller/oob_controller.go index 62de9b31..969d6b18 100644 --- a/internal/controller/oob_controller.go +++ b/internal/controller/oob_controller.go @@ -64,6 +64,7 @@ const ( OOBErrorBadEndpoint = "BadEndpoint" OOBErrorBadCredentials = "BadCredentials" OOBErrorBadInfo = "BadInfo" + OOBErrorBadMachines = "BadMachines" ) func NewOOBReconciler(systemNamespace, ipLabelSelector, macDB string, credsRenewalBeforeExpiry time.Duration, usernamePrefix, temporaryPasswordSecret string) (*OOBReconciler, error) { @@ -132,6 +133,7 @@ type access struct { type ctxkOOBHost struct{} type ctxkBMC struct{} +type ctxkInfo struct{} func (r *OOBReconciler) PreStart(ctx context.Context) error { return r.ensureTemporaryPassword(ctx) @@ -168,6 +170,16 @@ func (r *OOBReconciler) finalize(ctx context.Context, oob *metalv1alpha1.OOB) er return err } + err = r.finalizeSecret(ctx, oob) + if err != nil { + return err + } + + err = r.finalizeMachines(ctx, oob) + if err != nil { + return err + } + log.Debug(ctx, "Removing finalizer") var apply *metalv1alpha1apply.OOBApplyConfiguration apply, err = metalv1alpha1apply.ExtractOOB(oob, OOBFieldManager) @@ -250,6 +262,25 @@ func (r *OOBReconciler) finalizeSecret(ctx context.Context, oob *metalv1alpha1.O return nil } +func (r *OOBReconciler) finalizeMachines(ctx context.Context, oob *metalv1alpha1.OOB) error { + var machineList metalv1alpha1.MachineList + err := r.List(ctx, &machineList, client.MatchingFields{ + MachineSpecOOBRefName: oob.Name, + }) + if err != nil { + return fmt.Errorf("cannot list Machines: %w", err) + } + for _, m := range machineList.Items { + log.Info(ctx, "Deleting machine", "machine", m.Name) + err = r.Delete(ctx, &m) + if err != nil { + return fmt.Errorf("cannot delete Machine: %w", err) + } + } + + return nil +} + func (r *OOBReconciler) reconcile(ctx context.Context, oob *metalv1alpha1.OOB) (ctrl.Result, error) { log.Debug(ctx, "Reconciling") @@ -301,6 +332,15 @@ func (r *OOBReconciler) reconcile(ctx context.Context, oob *metalv1alpha1.OOB) ( return ctrl.Result{}, err } + ctx, advance, err = r.runPhase(ctx, oob, oobRecPhase{ + name: "Machines", + run: r.processMachines, + errType: OOBErrorBadMachines, + }) + if !advance { + return ctrl.Result{}, err + } + ctx, advance, err = r.runPhase(ctx, oob, oobRecPhase{ name: "Ready", run: r.processReady, @@ -496,7 +536,7 @@ func (r *OOBReconciler) processEndpoint(ctx context.Context, oob *metalv1alpha1. var ipList ipamv1alpha1.IPList err := r.List(ctx, &ipList, client.MatchingLabelsSelector{Selector: r.ipLabelSelector}, client.MatchingLabels{OOBIPMacLabel: oob.Spec.MACAddress}) if err != nil { - return ctx, nil, nil, fmt.Errorf("cannot list OOBs: %w", err) + return ctx, nil, nil, fmt.Errorf("cannot list IPs: %w", err) } found := false @@ -632,7 +672,9 @@ func (r *OOBReconciler) processCredentials(ctx context.Context, oob *metalv1alph } if oob.Spec.SecretRef == nil { var secretList metalv1alpha1.OOBSecretList - err := r.List(ctx, &secretList, client.MatchingFields{OOBSecretSpecMACAddress: oob.Spec.MACAddress}) + err := r.List(ctx, &secretList, client.MatchingFields{ + OOBSecretSpecMACAddress: oob.Spec.MACAddress, + }) if err != nil { return ctx, nil, nil, fmt.Errorf("cannot list OOBSecrets: %w", err) } @@ -866,7 +908,97 @@ func (r *OOBReconciler) processInfo(ctx context.Context, oob *metalv1alpha1.OOB) WithFirmwareVersion(info.FirmwareVersion) } - return ctx, nil, status, nil + return context.WithValue(ctx, ctxkInfo{}, info), nil, status, nil +} + +func (r *OOBReconciler) processMachines(ctx context.Context, oob *metalv1alpha1.OOB) (context.Context, *metalv1alpha1apply.OOBApplyConfiguration, *metalv1alpha1apply.OOBStatusApplyConfiguration, error) { + info := ctx.Value(ctxkInfo{}).(bmc.Info) + + type minfo struct { + m *metalv1alpha1.Machine + i bmc.Machine + } + machines := make(map[string]minfo, len(info.Machines)) + if oob.Status.Type == metalv1alpha1.OOBTypeMachine { + for _, i := range info.Machines { + machines[i.UUID] = minfo{ + m: nil, + i: i, + } + } + } + + var machineList metalv1alpha1.MachineList + err := r.List(ctx, &machineList, client.MatchingFields{ + MachineSpecOOBRefName: oob.Name, + }) + if err != nil { + return ctx, nil, nil, fmt.Errorf("cannot list Machines: %w", err) + } + for _, m := range machineList.Items { + mi, ok := machines[m.Spec.UUID] + if !ok { + log.Info(ctx, "Deleting orphaned machine", "machine", m.Name) + err = r.Delete(ctx, &m) + if err != nil { + return ctx, nil, nil, fmt.Errorf("cannot delete Machine: %w", err) + } + continue + } + + machine := &m + machines[m.Spec.UUID] = minfo{ + m: machine, + i: mi.i, + } + } + + oobRef := v1.LocalObjectReference{ + Name: oob.Name, + } + + for uuid, mi := range machines { + if mi.m == nil { + mi.m = &metalv1alpha1.Machine{ + ObjectMeta: metav1.ObjectMeta{ + Name: uuid, + }, + } + machineApply := metalv1alpha1apply.Machine(mi.m.Name, "").WithSpec(metalv1alpha1apply.MachineSpec(). + WithUUID(uuid). + WithOOBRef(oobRef)) + log.Info(ctx, "Applying Machine", "machine", mi.m.Name) + err = r.Patch(ctx, mi.m, ssa.Apply(machineApply), client.FieldOwner(OOBFieldManager), client.ForceOwnership) + if err != nil { + return ctx, nil, nil, fmt.Errorf("cannot apply Machine: %w", err) + } + } + + if mi.m.Status.Manufacturer != mi.i.Manufacturer || + mi.m.Status.SKU != mi.i.SKU || + mi.m.Status.SerialNumber != mi.i.SerialNumber || + mi.m.Status.Power != metalv1alpha1.Power(mi.i.Power) || + mi.m.Status.LocatorLED != metalv1alpha1.LED(mi.i.LocatorLED) { + var machineApply *metalv1alpha1apply.MachineApplyConfiguration + machineApply, err = metalv1alpha1apply.ExtractMachineStatus(mi.m, OOBFieldManager) + if err != nil { + return ctx, nil, nil, fmt.Errorf("cannot extract Machine status: %w", err) + } + machineApply = machineApply.WithStatus(util.Ensure(machineApply.Status). + WithManufacturer(mi.i.Manufacturer). + WithSKU(mi.i.SKU). + WithSerialNumber(mi.i.SerialNumber). + WithPower(metalv1alpha1.Power(mi.i.Power)). + WithLocatorLED(metalv1alpha1.LED(mi.i.LocatorLED))) + log.Info(ctx, "Applying Machine status", "machine", mi.m.Name) + err = r.Status().Patch(ctx, mi.m, ssa.Apply(machineApply), client.FieldOwner(OOBFieldManager), client.ForceOwnership) + if err != nil { + return ctx, nil, nil, fmt.Errorf("cannot apply Machine status: %w", err) + } + } + } + + return ctx, nil, nil, nil } func (r *OOBReconciler) processReady(ctx context.Context, oob *metalv1alpha1.OOB) (context.Context, *metalv1alpha1apply.OOBApplyConfiguration, *metalv1alpha1apply.OOBStatusApplyConfiguration, error) { @@ -896,6 +1028,11 @@ func (r *OOBReconciler) SetupWithManager(mgr ctrl.Manager) error { return err } + err = c.Watch(source.Kind(mgr.GetCache(), &metalv1alpha1.Machine{}), handler.EnqueueRequestsFromMapFunc(r.enqueueOOBFromMachine)) + if err != nil { + return err + } + return mgr.Add(c) } @@ -916,8 +1053,10 @@ func (r *OOBReconciler) enqueueOOBFromIP(ctx context.Context, obj client.Object) } ctx = log.WithValues(ctx, "mac", mac) - oobList := metalv1alpha1.OOBList{} - err := r.List(ctx, &oobList, client.MatchingFields{OOBSpecMACAddress: mac}) + var oobList metalv1alpha1.OOBList + err := r.List(ctx, &oobList, client.MatchingFields{ + OOBSpecMACAddress: mac, + }) if err != nil { log.Error(ctx, fmt.Errorf("cannot list OOBs: %w", err)) return nil @@ -994,8 +1133,10 @@ func (r *OOBReconciler) enqueueOOBFromIP(ctx context.Context, obj client.Object) func (r *OOBReconciler) enqueueOOBFromOOBSecret(ctx context.Context, obj client.Object) []reconcile.Request { secret := obj.(*metalv1alpha1.OOBSecret) - oobList := metalv1alpha1.OOBList{} - err := r.List(ctx, &oobList, client.MatchingFields{OOBSpecMACAddress: secret.Spec.MACAddress}) + var oobList metalv1alpha1.OOBList + err := r.List(ctx, &oobList, client.MatchingFields{ + OOBSpecMACAddress: secret.Spec.MACAddress, + }) if err != nil { log.Error(ctx, fmt.Errorf("cannot list OOBs: %w", err)) return nil @@ -1015,6 +1156,18 @@ func (r *OOBReconciler) enqueueOOBFromOOBSecret(ctx context.Context, obj client. return reqs } +func (r *OOBReconciler) enqueueOOBFromMachine(_ context.Context, obj client.Object) []reconcile.Request { + machine := obj.(*metalv1alpha1.Machine) + + return []reconcile.Request{ + { + NamespacedName: types.NamespacedName{ + Name: machine.Spec.OOBRef.Name, + }, + }, + } +} + func (r *OOBReconciler) ensureTemporaryPassword(ctx context.Context) error { secret := v1.Secret{ ObjectMeta: metav1.ObjectMeta{ diff --git a/internal/controller/oob_controller_test.go b/internal/controller/oob_controller_test.go index f28c6f47..bbf6bb49 100644 --- a/internal/controller/oob_controller_test.go +++ b/internal/controller/oob_controller_test.go @@ -422,7 +422,7 @@ var _ = Describe("OOB Controller", Serial, func() { oob := &metalv1alpha1.OOB{ ObjectMeta: metav1.ObjectMeta{ - Name: "aabbccddee00", //fixme + Name: "aabbccddee00", }, } @@ -648,6 +648,75 @@ var _ = Describe("OOB Controller", Serial, func() { WithTransform(readyReason, Equal(metalv1alpha1.OOBConditionReasonReady)), )) }) + + It("should create Machine objects", func(ctx SpecContext) { + oob := &metalv1alpha1.OOB{ + ObjectMeta: metav1.ObjectMeta{ + Name: mac, + }, + } + secret := &metalv1alpha1.OOBSecret{ + ObjectMeta: metav1.ObjectMeta{ + Name: mac, + }, + } + machine := &metalv1alpha1.Machine{} + DeferCleanup(func(ctx SpecContext) { + Expect(k8sClient.Delete(ctx, oob)).To(Succeed()) + Eventually(Get(oob)).Should(Satisfy(errors.IsNotFound)) + Expect(k8sClient.Delete(ctx, secret)).To(Succeed()) + Eventually(Get(secret)).Should(Satisfy(errors.IsNotFound)) + Eventually(Get(machine)).Should(Satisfy(errors.IsNotFound)) + }) + + By("Creating an IP") + ip := &ipamv1alpha1.IP{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "test-", + Namespace: OOBTemporaryNamespaceHack, + Labels: map[string]string{ + OOBIPMacLabel: mac, + "test": "test", + }, + }, + } + Expect(k8sClient.Create(ctx, ip)).To(Succeed()) + DeferCleanup(func(ctx SpecContext) { + Expect(k8sClient.Delete(ctx, ip)).To(Succeed()) + Eventually(Get(ip)).Should(Satisfy(errors.IsNotFound)) + }) + + By("Patching IP reservation and state") + ipAddr, err := ipamv1alpha1.IPAddrFromString("1.2.3.4") + Expect(err).NotTo(HaveOccurred()) + Eventually(UpdateStatus(ip, func() { + ip.Status.Reserved = ipAddr + ip.Status.State = ipamv1alpha1.CFinishedIPState + })).Should(Succeed()) + + By("Expecting the OOB to have the correct info") + Eventually(Object(oob)).Should(SatisfyAll( + HaveField("Status.Type", metalv1alpha1.OOBTypeMachine), + HaveField("Status.State", metalv1alpha1.OOBStateReady), + WithTransform(readyReason, Equal(metalv1alpha1.OOBConditionReasonReady)), + )) + + By("Listing machines") + machines := &metalv1alpha1.MachineList{} + Eventually(ObjectList(machines)).Should(HaveField("Items", HaveLen(1))) + machine = &machines.Items[0] + + By("Expecting Machine to have the correct data") + Eventually(Object(machine)).Should(SatisfyAll( + HaveField("Spec.UUID", machine.Name), + HaveField("Spec.OOBRef.Name", oob.Name), + HaveField("Status.Manufacturer", "Fake"), + HaveField("Status.SKU", "Fake-0"), + HaveField("Status.SerialNumber", "1"), + HaveField("Status.Power", metalv1alpha1.PowerOn), + HaveField("Status.LocatorLED", metalv1alpha1.LEDOff), + )) + }) }) func readyReason(o client.Object) (string, error) {