diff --git a/api/v1alpha1/bootconfiguration_types.go b/api/v1alpha1/bootconfiguration_types.go index d3ad5a5..8529043 100644 --- a/api/v1alpha1/bootconfiguration_types.go +++ b/api/v1alpha1/bootconfiguration_types.go @@ -34,6 +34,7 @@ const ( // +kubebuilder:object:root=true // +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Cluster // +kubebuilder:printcolumn:name="State",type=string,JSONPath=`.status.state` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` // +genclient diff --git a/api/v1alpha1/machine_types.go b/api/v1alpha1/machine_types.go index f958951..ef6a8ea 100644 --- a/api/v1alpha1/machine_types.go +++ b/api/v1alpha1/machine_types.go @@ -31,6 +31,9 @@ type MachineSpec struct { // +optional LoopbackAddressRef *v1.LocalObjectReference `json:"loopbackAddressRef,omitempty"` + // +optional + BootConfigurationRef *v1.LocalObjectReference `json:"bootConfigurationRef,omitempty"` + // +optional ASN string `json:"asn,omitempty"` diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 3f2b381..d159cf4 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -847,6 +847,11 @@ func (in *MachineSpec) DeepCopyInto(out *MachineSpec) { *out = new(v1.LocalObjectReference) **out = **in } + if in.BootConfigurationRef != nil { + in, out := &in.BootConfigurationRef, &out.BootConfigurationRef + *out = new(v1.LocalObjectReference) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MachineSpec. diff --git a/client/applyconfiguration/api/v1alpha1/machinespec.go b/client/applyconfiguration/api/v1alpha1/machinespec.go index 6109e34..09a64bc 100644 --- a/client/applyconfiguration/api/v1alpha1/machinespec.go +++ b/client/applyconfiguration/api/v1alpha1/machinespec.go @@ -13,16 +13,17 @@ import ( // MachineSpecApplyConfiguration represents an declarative configuration of the MachineSpec type for use // with apply. type MachineSpecApplyConfiguration struct { - UUID *string `json:"uuid,omitempty"` - OOBRef *v1.LocalObjectReference `json:"oobRef,omitempty"` - InventoryRef *v1.LocalObjectReference `json:"inventoryRef,omitempty"` - MachineClaimRef *v1.ObjectReference `json:"machineClaimRef,omitempty"` - LoopbackAddressRef *v1.LocalObjectReference `json:"loopbackAddressRef,omitempty"` - ASN *string `json:"asn,omitempty"` - Power *v1alpha1.Power `json:"power,omitempty"` - LocatorLED *v1alpha1.LED `json:"locatorLED,omitempty"` - CleanupRequired *bool `json:"cleanupRequired,omitempty"` - Maintenance *bool `json:"maintenance,omitempty"` + UUID *string `json:"uuid,omitempty"` + OOBRef *v1.LocalObjectReference `json:"oobRef,omitempty"` + InventoryRef *v1.LocalObjectReference `json:"inventoryRef,omitempty"` + MachineClaimRef *v1.ObjectReference `json:"machineClaimRef,omitempty"` + LoopbackAddressRef *v1.LocalObjectReference `json:"loopbackAddressRef,omitempty"` + BootConfigurationRef *v1.LocalObjectReference `json:"bootConfigurationRef,omitempty"` + ASN *string `json:"asn,omitempty"` + Power *v1alpha1.Power `json:"power,omitempty"` + LocatorLED *v1alpha1.LED `json:"locatorLED,omitempty"` + CleanupRequired *bool `json:"cleanupRequired,omitempty"` + Maintenance *bool `json:"maintenance,omitempty"` } // MachineSpecApplyConfiguration constructs an declarative configuration of the MachineSpec type for use with @@ -71,6 +72,14 @@ func (b *MachineSpecApplyConfiguration) WithLoopbackAddressRef(value v1.LocalObj return b } +// WithBootConfigurationRef sets the BootConfigurationRef field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the BootConfigurationRef field is set to the value of the last call. +func (b *MachineSpecApplyConfiguration) WithBootConfigurationRef(value v1.LocalObjectReference) *MachineSpecApplyConfiguration { + b.BootConfigurationRef = &value + return b +} + // WithASN sets the ASN field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the ASN field is set to the value of the last call. diff --git a/client/applyconfiguration/internal/internal.go b/client/applyconfiguration/internal/internal.go index 636b815..5fb1378 100644 --- a/client/applyconfiguration/internal/internal.go +++ b/client/applyconfiguration/internal/internal.go @@ -578,6 +578,9 @@ var schemaYAML = typed.YAMLObject(`types: - name: asn type: scalar: string + - name: bootConfigurationRef + type: + namedType: io.k8s.api.core.v1.LocalObjectReference - name: cleanupRequired type: scalar: boolean diff --git a/client/openapi/zz_generated.openapi.go b/client/openapi/zz_generated.openapi.go index 25bd031..701b659 100644 --- a/client/openapi/zz_generated.openapi.go +++ b/client/openapi/zz_generated.openapi.go @@ -1904,6 +1904,11 @@ func schema_ironcore_dev_metal_api_v1alpha1_MachineSpec(ref common.ReferenceCall Ref: ref("k8s.io/api/core/v1.LocalObjectReference"), }, }, + "bootConfigurationRef": { + SchemaProps: spec.SchemaProps{ + Ref: ref("k8s.io/api/core/v1.LocalObjectReference"), + }, + }, "asn": { SchemaProps: spec.SchemaProps{ Type: []string{"string"}, diff --git a/cmd/main.go b/cmd/main.go index 972036c..8e1912f 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -60,7 +60,6 @@ type params struct { oobUsernamePrefix string oobTemporaryPasswordSecret string machineInventoryBootImage string - bootConfigurationNamespace string } func parseCmdLine() params { @@ -90,7 +89,6 @@ func parseCmdLine() params { pflag.String("oob-username-prefix", "metal-", "OOB: Use a prefix when creating BMC users. Cannot be empty.") pflag.String("oob-temporary-password-secret", "bmc-temporary-password", "OOB: Secret to store a temporary password in. Will be generated if it does not exist.") pflag.String("machine-inventory-boot-image", "ghcr.io/gardenlinux/gardenlinux:latest", "Machine: boot image to run inventory.") - pflag.String("boot-configuration-namespace", "default", "Boot configuration namespace.") var help bool pflag.BoolVarP(&help, "help", "h", false, "Show this help message.") @@ -131,7 +129,6 @@ func parseCmdLine() params { oobUsernamePrefix: viper.GetString("oob-username-prefix"), oobTemporaryPasswordSecret: viper.GetString("oob-temporary-password-secret"), machineInventoryBootImage: viper.GetString("machine-inventory-boot-image"), - bootConfigurationNamespace: viper.GetString("boot-configuration-namespace"), } } @@ -306,7 +303,7 @@ func main() { if p.enableMachineController { var machineReconciler *controller.MachineReconciler - machineReconciler, err = controller.NewMachineReconciler(p.machineInventoryBootImage, p.bootConfigurationNamespace) + machineReconciler, err = controller.NewMachineReconciler(p.machineInventoryBootImage) if err != nil { log.Error(ctx, fmt.Errorf("cannot create controller: %w", err), "controller", "Machine") exitCode = 1 diff --git a/config/crd/bases/metal.ironcore.dev_bootconfigurations.yaml b/config/crd/bases/metal.ironcore.dev_bootconfigurations.yaml index 02b8a36..88d362c 100644 --- a/config/crd/bases/metal.ironcore.dev_bootconfigurations.yaml +++ b/config/crd/bases/metal.ironcore.dev_bootconfigurations.yaml @@ -12,7 +12,7 @@ spec: listKind: BootConfigurationList plural: bootconfigurations singular: bootconfiguration - scope: Namespaced + scope: Cluster versions: - additionalPrinterColumns: - jsonPath: .status.state diff --git a/config/crd/bases/metal.ironcore.dev_machines.yaml b/config/crd/bases/metal.ironcore.dev_machines.yaml index 2a2d4c4..56e570d 100644 --- a/config/crd/bases/metal.ironcore.dev_machines.yaml +++ b/config/crd/bases/metal.ironcore.dev_machines.yaml @@ -72,6 +72,24 @@ spec: properties: asn: type: string + bootConfigurationRef: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + TODO: Add other useful fields. apiVersion, kind, uid? + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896. + type: string + type: object + x-kubernetes-map-type: atomic cleanupRequired: type: boolean inventoryRef: diff --git a/config/rbac/bootconfiguration_editor_role.yaml b/config/rbac/bootconfiguration_editor_role.yaml new file mode 100644 index 0000000..a6441bd --- /dev/null +++ b/config/rbac/bootconfiguration_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit inventories. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: bootconfiguration-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: metal + app.kubernetes.io/part-of: metal + app.kubernetes.io/managed-by: kustomize + name: bootconfiguration-editor-role +rules: +- apiGroups: + - metal.ironcore.dev + resources: + - bootconfigurations + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - metal.ironcore.dev + resources: + - bootconfigurations/status + verbs: + - get diff --git a/config/rbac/bootconfiguration_viewer_role.yaml b/config/rbac/bootconfiguration_viewer_role.yaml new file mode 100644 index 0000000..7a22c27 --- /dev/null +++ b/config/rbac/bootconfiguration_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view inventories. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: bootconfiguration-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: metal + app.kubernetes.io/part-of: metal + app.kubernetes.io/managed-by: kustomize + name: bootconfiguration-viewer-role +rules: +- apiGroups: + - metal.ironcore.dev + resources: + - bootconfigurations + verbs: + - get + - list + - watch +- apiGroups: + - metal.ironcore.dev + resources: + - bootconfigurations/status + verbs: + - get diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 00fb308..e3fc8ea 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -57,6 +57,24 @@ rules: - get - patch - update +- apiGroups: + - metal.ironcore.dev + resources: + - bootconfigurations + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - metal.ironcore.dev + resources: + - bootconfigurations/status + verbs: + - get - apiGroups: - metal.ironcore.dev resources: diff --git a/internal/controller/inventory_controller.go b/internal/controller/inventory_controller.go index d4be7bc..8612885 100644 --- a/internal/controller/inventory_controller.go +++ b/internal/controller/inventory_controller.go @@ -77,7 +77,8 @@ func (r *InventoryReconciler) reconcile(ctx context.Context, inventory metalv1al if machine.Spec.InventoryRef == nil { machineSpecApply := metalv1alpha1apply.MachineSpec(). WithPower(metalv1alpha1.PowerOff). - WithInventoryRef(corev1.LocalObjectReference{Name: inventory.Name}) + WithInventoryRef(corev1.LocalObjectReference{Name: inventory.Name}). + WithBootConfigurationRef(corev1.LocalObjectReference{}) machineApply = machineApply.WithSpec(machineSpecApply) return r.Patch( ctx, machine, ssa.Apply(machineApply), client.FieldOwner(InventoryFieldManager), client.ForceOwnership) diff --git a/internal/controller/machine_controller.go b/internal/controller/machine_controller.go index 8f91458..56aa5b0 100644 --- a/internal/controller/machine_controller.go +++ b/internal/controller/machine_controller.go @@ -31,6 +31,8 @@ import ( // +kubebuilder:rbac:groups=metal.ironcore.dev,resources=machines/finalizers,verbs=update // +kubebuilder:rbac:groups=metal.ironcore.dev,resources=inventories,verbs=get;list;watch // +kubebuilder:rbac:groups=metal.ironcore.dev,resources=inventories/status,verbs=get +// +kubebuilder:rbac:groups=metal.ironcore.dev,resources=bootconfigurations,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=metal.ironcore.dev,resources=bootconfigurations/status,verbs=get const ( MachineFieldManager string = "metal.ironcore.dev/machine" @@ -71,13 +73,12 @@ const ( MachineSpecOOBRefName = ".spec.oobRef.Name" ) -func NewMachineReconciler(machineInventoryBootImage, bootOperatorNamespace string) (*MachineReconciler, error) { +func NewMachineReconciler(machineInventoryBootImage string) (*MachineReconciler, error) { if machineInventoryBootImage == "" { return nil, fmt.Errorf("no machine inventory boot image provided") } return &MachineReconciler{ - machineInventoryBootImage: machineInventoryBootImage, - bootConfigurationNamespace: bootOperatorNamespace, + machineInventoryBootImage: machineInventoryBootImage, }, nil } @@ -85,8 +86,7 @@ func NewMachineReconciler(machineInventoryBootImage, bootOperatorNamespace strin type MachineReconciler struct { client.Client - machineInventoryBootImage string - bootConfigurationNamespace string + machineInventoryBootImage string } // Reconcile is part of the main kubernetes reconciliation loop which aims to @@ -100,7 +100,7 @@ func (r *MachineReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct return ctrl.Result{}, nil } - machineApply := r.reconcile(ctx, &machine) + machineApply, err := r.reconcile(ctx, &machine) if machineApply.Spec != nil { machineApply.Status = nil return ctrl.Result{}, r.Patch( @@ -111,13 +111,13 @@ func (r *MachineReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct return ctrl.Result{}, r.Status().Patch( ctx, &machine, ssa.Apply(machineApply), client.FieldOwner(MachineFieldManager), client.ForceOwnership) } - return ctrl.Result{}, nil + return ctrl.Result{}, err } func (r *MachineReconciler) reconcile( ctx context.Context, machine *metalv1alpha1.Machine, -) *metalv1alpha1apply.MachineApplyConfiguration { +) (*metalv1alpha1apply.MachineApplyConfiguration, error) { var machineStatusApply *metalv1alpha1apply.MachineStatusApplyConfiguration machineApply := metalv1alpha1apply.Machine(machine.Name, machine.Namespace) @@ -127,12 +127,14 @@ func (r *MachineReconciler) reconcile( r.fillConditions(machine, machineStatusApply) case machine.Spec.InventoryRef == nil && machine.Status.State == metalv1alpha1.MachineStateInitial && conditionTrue(machine.Status.Conditions, MachineInitializedConditionType): - if machine.Spec.Power != metalv1alpha1.PowerOn { - if err := r.createBootConfiguration(ctx, machine); err != nil { - return machineApply - } - return machineApply.WithSpec(metalv1alpha1apply.MachineSpec().WithPower(metalv1alpha1.PowerOn)) + machineSpecApply, err := r.evaluateInitialBoot(ctx, machine) + if err != nil { + return machineApply, err + } + if machineSpecApply == nil { + return machineApply, nil } + return machineApply.WithSpec(machineSpecApply), nil default: machineStatusApply = metalv1alpha1apply.MachineStatus() r.fillConditions(machine, machineStatusApply) @@ -145,7 +147,42 @@ func (r *MachineReconciler) reconcile( if machineStatusApply != nil { machineApply = machineApply.WithStatus(machineStatusApply) } - return machineApply + return machineApply, nil +} + +func (r *MachineReconciler) evaluateInitialBoot( + ctx context.Context, + machine *metalv1alpha1.Machine, +) (*metalv1alpha1apply.MachineSpecApplyConfiguration, error) { + if machine.Spec.Power == metalv1alpha1.PowerOn { + return nil, nil + } + if machine.Spec.BootConfigurationRef == nil || machine.Spec.BootConfigurationRef.Name == "" { + if err := r.createBootConfiguration(ctx, machine); err != nil { + log.Error(ctx, fmt.Errorf("failed to create boot configuration: %w", err)) + return nil, err + } + machineSpecApply := metalv1alpha1apply.MachineSpec() + return machineSpecApply. + WithBootConfigurationRef(corev1.LocalObjectReference{Name: machine.Name}), nil + } + + bootConfig := &metalv1alpha1.BootConfiguration{} + if err := r.Get(ctx, types.NamespacedName{ + Namespace: "", + Name: machine.Spec.BootConfigurationRef.Name, + }, bootConfig); err != nil { + log.Error(ctx, fmt.Errorf("failed to get boot configuration: %w", err)) + return nil, err + } + if bootConfig.Status.State != metalv1alpha1.BootConfigurationStateReady { + return nil, nil + } + log.Info(ctx, "Boot configuration is ready") + machineSpecApply := metalv1alpha1apply.MachineSpec() + return machineSpecApply. + WithPower(metalv1alpha1.PowerOn). + WithBootConfigurationRef(corev1.LocalObjectReference{Name: machine.Name}), nil } func (r *MachineReconciler) evaluateConditions( @@ -568,8 +605,7 @@ func (r *MachineReconciler) createBootConfiguration(ctx context.Context, machine log.Info(ctx, "Creating boot configuration") bootConfig := &metalv1alpha1.BootConfiguration{ ObjectMeta: metav1.ObjectMeta{ - Name: machine.Name, - Namespace: r.bootConfigurationNamespace, + Name: machine.Name, }, Spec: metalv1alpha1.BootConfigurationSpec{ MachineRef: &corev1.LocalObjectReference{Name: machine.Name}, @@ -588,7 +624,7 @@ func (r *MachineReconciler) createBootConfiguration(ctx context.Context, machine WithUID(existing.UID). WithController(*existing.Controller). WithBlockOwnerDeletion(*existing.BlockOwnerDeletion) - bootConfigApply := metalv1alpha1apply.BootConfiguration(bootConfig.Name, bootConfig.Namespace). + bootConfigApply := metalv1alpha1apply.BootConfiguration(bootConfig.Name, ""). WithOwnerReferences(owner) bootConfigSpecApply := metalv1alpha1apply.BootConfigurationSpec(). WithImage(r.machineInventoryBootImage). @@ -603,8 +639,7 @@ func (r *MachineReconciler) deleteBootConfiguration(ctx context.Context, machine log.Info(ctx, "Deleting boot configuration") bootConfiguration := &metalv1alpha1.BootConfiguration{ ObjectMeta: metav1.ObjectMeta{ - Name: machine.Name, - Namespace: r.bootConfigurationNamespace, + Name: machine.Name, }} return client.IgnoreNotFound(r.Delete(ctx, bootConfiguration)) } @@ -617,6 +652,8 @@ func (r *MachineReconciler) SetupWithManager(mgr ctrl.Manager) error { For(&metalv1alpha1.Machine{}). Watches(&metalv1alpha1.Inventory{}, handler.EnqueueRequestForOwner( mgr.GetScheme(), mgr.GetRESTMapper(), &metalv1alpha1.Machine{}, handler.OnlyControllerOwner())). + Watches(&metalv1alpha1.BootConfiguration{}, handler.EnqueueRequestForOwner( + mgr.GetScheme(), mgr.GetRESTMapper(), &metalv1alpha1.Machine{}, handler.OnlyControllerOwner())). Watches(&metalv1alpha1.OOB{}, handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, object client.Object) []reconcile.Request { requests := make([]reconcile.Request, 0) source, ok := object.(*metalv1alpha1.OOB) diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go index 2495853..edad824 100644 --- a/internal/controller/suite_test.go +++ b/internal/controller/suite_test.go @@ -38,7 +38,6 @@ const ( manufacturer = "Sample" serialNumber = "1234" inventoryImage = "fake" - bootOperatorNs = "boot-operator-system" ) var ( @@ -130,7 +129,7 @@ var _ = BeforeSuite(func() { Expect(inventoryReconciler.SetupWithManager(mgr)).To(Succeed()) var machineReconciler *MachineReconciler - machineReconciler, err = NewMachineReconciler(inventoryImage, bootOperatorNs) + machineReconciler, err = NewMachineReconciler(inventoryImage) Expect(err).NotTo(HaveOccurred()) Expect(machineReconciler).NotTo(BeNil()) Expect(machineReconciler.SetupWithManager(mgr)).To(Succeed())