diff --git a/api/v1alpha1/crds/caren.nutanix.com_awsclusterconfigs.yaml b/api/v1alpha1/crds/caren.nutanix.com_awsclusterconfigs.yaml index e6df30d79..92a358699 100644 --- a/api/v1alpha1/crds/caren.nutanix.com_awsclusterconfigs.yaml +++ b/api/v1alpha1/crds/caren.nutanix.com_awsclusterconfigs.yaml @@ -377,6 +377,23 @@ spec: default: m5.xlarge type: string type: object + nodeRegistration: + default: {} + description: NodeRegistration holds fields that relate to registering the new control-plane node to the cluster. + properties: + ignorePreflightErrors: + default: + - SystemVerification + description: |- + IgnorePreflightErrors specifies a slice of pre-flight errors to be ignored by kubeadm + when the current node is registered. + items: + maxLength: 512 + minLength: 1 + type: string + maxItems: 50 + type: array + type: object taints: description: Taints specifies the taints the Node API object should be registered with. items: diff --git a/api/v1alpha1/crds/caren.nutanix.com_awsworkernodeconfigs.yaml b/api/v1alpha1/crds/caren.nutanix.com_awsworkernodeconfigs.yaml index 78b36f14a..da1027efe 100644 --- a/api/v1alpha1/crds/caren.nutanix.com_awsworkernodeconfigs.yaml +++ b/api/v1alpha1/crds/caren.nutanix.com_awsworkernodeconfigs.yaml @@ -90,6 +90,24 @@ spec: description: The AWS instance type to use for the cluster Machines. type: string type: object + nodeRegistration: + default: {} + description: NodeRegistration holds fields that relate to registering + the new control-plane node to the cluster. + properties: + ignorePreflightErrors: + default: + - SystemVerification + description: |- + IgnorePreflightErrors specifies a slice of pre-flight errors to be ignored by kubeadm + when the current node is registered. + items: + maxLength: 512 + minLength: 1 + type: string + maxItems: 50 + type: array + type: object taints: description: Taints specifies the taints the Node API object should be registered with. diff --git a/api/v1alpha1/crds/caren.nutanix.com_dockerclusterconfigs.yaml b/api/v1alpha1/crds/caren.nutanix.com_dockerclusterconfigs.yaml index 0b6ed7e01..19097a02a 100644 --- a/api/v1alpha1/crds/caren.nutanix.com_dockerclusterconfigs.yaml +++ b/api/v1alpha1/crds/caren.nutanix.com_dockerclusterconfigs.yaml @@ -306,6 +306,23 @@ spec: pattern: ^((?:[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*|\[(?:[a-fA-F0-9:]+)\])(:[0-9]+)?/)?[a-z0-9]+((?:[._]|__|[-]+)[a-z0-9]+)*(/[a-z0-9]+((?:[._]|__|[-]+)[a-z0-9]+)*)*(:[\w][\w.-]{0,127})?(@[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][0-9A-Fa-f]{32,})?$ type: string type: object + nodeRegistration: + default: {} + description: NodeRegistration holds fields that relate to registering the new control-plane node to the cluster. + properties: + ignorePreflightErrors: + default: + - SystemVerification + description: |- + IgnorePreflightErrors specifies a slice of pre-flight errors to be ignored by kubeadm + when the current node is registered. + items: + maxLength: 512 + minLength: 1 + type: string + maxItems: 50 + type: array + type: object taints: description: Taints specifies the taints the Node API object should be registered with. items: diff --git a/api/v1alpha1/crds/caren.nutanix.com_dockerworkernodeconfigs.yaml b/api/v1alpha1/crds/caren.nutanix.com_dockerworkernodeconfigs.yaml index 9bd301c97..5c3caec95 100644 --- a/api/v1alpha1/crds/caren.nutanix.com_dockerworkernodeconfigs.yaml +++ b/api/v1alpha1/crds/caren.nutanix.com_dockerworkernodeconfigs.yaml @@ -65,6 +65,24 @@ spec: pattern: ^((?:[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*|\[(?:[a-fA-F0-9:]+)\])(:[0-9]+)?/)?[a-z0-9]+((?:[._]|__|[-]+)[a-z0-9]+)*(/[a-z0-9]+((?:[._]|__|[-]+)[a-z0-9]+)*)*(:[\w][\w.-]{0,127})?(@[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][0-9A-Fa-f]{32,})?$ type: string type: object + nodeRegistration: + default: {} + description: NodeRegistration holds fields that relate to registering + the new control-plane node to the cluster. + properties: + ignorePreflightErrors: + default: + - SystemVerification + description: |- + IgnorePreflightErrors specifies a slice of pre-flight errors to be ignored by kubeadm + when the current node is registered. + items: + maxLength: 512 + minLength: 1 + type: string + maxItems: 50 + type: array + type: object taints: description: Taints specifies the taints the Node API object should be registered with. diff --git a/api/v1alpha1/crds/caren.nutanix.com_nutanixclusterconfigs.yaml b/api/v1alpha1/crds/caren.nutanix.com_nutanixclusterconfigs.yaml index 89ae1b461..ca863a2b5 100644 --- a/api/v1alpha1/crds/caren.nutanix.com_nutanixclusterconfigs.yaml +++ b/api/v1alpha1/crds/caren.nutanix.com_nutanixclusterconfigs.yaml @@ -299,6 +299,23 @@ spec: required: - daysBeforeExpiry type: object + nodeRegistration: + default: {} + description: NodeRegistration holds fields that relate to registering the new control-plane node to the cluster. + properties: + ignorePreflightErrors: + default: + - SystemVerification + description: |- + IgnorePreflightErrors specifies a slice of pre-flight errors to be ignored by kubeadm + when the current node is registered. + items: + maxLength: 512 + minLength: 1 + type: string + maxItems: 50 + type: array + type: object nutanix: properties: machineDetails: diff --git a/api/v1alpha1/crds/caren.nutanix.com_nutanixworkernodeconfigs.yaml b/api/v1alpha1/crds/caren.nutanix.com_nutanixworkernodeconfigs.yaml index bc96d9833..280382dd1 100644 --- a/api/v1alpha1/crds/caren.nutanix.com_nutanixworkernodeconfigs.yaml +++ b/api/v1alpha1/crds/caren.nutanix.com_nutanixworkernodeconfigs.yaml @@ -41,6 +41,23 @@ spec: spec: description: NutanixWorkerNodeConfigSpec defines the desired state of NutanixNodeSpec. properties: + nodeRegistration: + default: {} + description: NodeRegistration holds fields that relate to registering the new control-plane node to the cluster. + properties: + ignorePreflightErrors: + default: + - SystemVerification + description: |- + IgnorePreflightErrors specifies a slice of pre-flight errors to be ignored by kubeadm + when the current node is registered. + items: + maxLength: 512 + minLength: 1 + type: string + maxItems: 50 + type: array + type: object nutanix: properties: machineDetails: diff --git a/api/v1alpha1/nodeconfig_types.go b/api/v1alpha1/nodeconfig_types.go index 266ddc016..01fc706fb 100644 --- a/api/v1alpha1/nodeconfig_types.go +++ b/api/v1alpha1/nodeconfig_types.go @@ -106,6 +106,11 @@ type GenericNodeSpec struct { // Taints specifies the taints the Node API object should be registered with. // +kubebuilder:validation:Optional Taints []Taint `json:"taints,omitempty"` + + // NodeRegistration holds fields that relate to registering the new control-plane node to the cluster. + // +kubebuilder:validation:Optional + // +kubebuilder:default={} + NodeRegistration *NodeRegistrationOptions `json:"nodeRegistration,omitempty"` } // The node this Taint is attached to has the "effect" on @@ -146,6 +151,19 @@ const ( TaintEffectNoExecute TaintEffect = "NoExecute" ) +// NodeRegistrationOptions holds fields that relate to registering a new control-plane or node to the cluster, +// either via "kubeadm init" or "kubeadm join". +type NodeRegistrationOptions struct { + // IgnorePreflightErrors specifies a slice of pre-flight errors to be ignored by kubeadm + // when the current node is registered. + // +kubebuilder:validation:Optional + // +kubebuilder:default={"SystemVerification"} + // +kubebuilder:validation:MaxItems=50 + // +kubebuilder:validation:items:MinLength=1 + // +kubebuilder:validation:items:MaxLength=512 + IgnorePreflightErrors []string `json:"ignorePreflightErrors,omitempty"` +} + //nolint:gochecknoinits // Idiomatic to use init functions to register APIs with scheme. func init() { SchemeBuilder.Register( diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 24bfd42e6..a860a3bae 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -1220,6 +1220,11 @@ func (in *GenericNodeSpec) DeepCopyInto(out *GenericNodeSpec) { *out = make([]Taint, len(*in)) copy(*out, *in) } + if in.NodeRegistration != nil { + in, out := &in.NodeRegistration, &out.NodeRegistration + *out = new(NodeRegistrationOptions) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GenericNodeSpec. @@ -1342,6 +1347,26 @@ func (in *NFD) DeepCopy() *NFD { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NodeRegistrationOptions) DeepCopyInto(out *NodeRegistrationOptions) { + *out = *in + if in.IgnorePreflightErrors != nil { + in, out := &in.IgnorePreflightErrors, &out.IgnorePreflightErrors + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodeRegistrationOptions. +func (in *NodeRegistrationOptions) DeepCopy() *NodeRegistrationOptions { + if in == nil { + return nil + } + out := new(NodeRegistrationOptions) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NutanixAddons) DeepCopyInto(out *NutanixAddons) { *out = *in diff --git a/docs/content/customization/generic/node-registration.md b/docs/content/customization/generic/node-registration.md new file mode 100644 index 000000000..d244cdf9f --- /dev/null +++ b/docs/content/customization/generic/node-registration.md @@ -0,0 +1,124 @@ ++++ +title = "Node registration configuration" ++++ + +Below is a list of node registration configuration options that can be set for `kubeadm init` and `kubeadm join`. + +This customization will be available when the +[provider-specific cluster configuration patch]({{< ref "..">}}) is included in the `ClusterClass`. + +## Example + +### ignorePreflightErrors + +Kubeadm runs preflight checks to ensure the machine is compatible with Kubernetes and its dependencies. +The `SystemVerification` check is known to result in false positives. +For example, it fails when the Linux Kernel version is not supported by kubeadm, +even if the kernel has all the required features. +For this reason, we skip the check by default. + +#### Control plane + +To configure `ignorePreflightErrors` for the control plane nodes, specify the following configuration: + +```yaml +apiVersion: cluster.x-k8s.io/v1beta1 +kind: Cluster +metadata: + name: +spec: + topology: + variables: + - name: clusterConfig + value: + controlPlane: + nodeRegistration: + ignorePreflightErrors: + - SystemVerification +``` + +Applying this configuration will result in the following value being set: + +- `KubeadmControlPlaneTemplate`: + + - ```yaml + spec: + kubeadmConfigSpec: + initConfiguration: + nodeRegistration: + nodeRegistration: + ignorePreflightErrors: + - SystemVerification + joinConfiguration: + nodeRegistration: + ignorePreflightErrors: + - SystemVerification + ``` + +#### Worker node + +`ignorePreflightErrors` for individual nodepools can be configured similarly: + +```yaml +apiVersion: cluster.x-k8s.io/v1beta1 +kind: Cluster +metadata: + name: +spec: + topology: + workers: + machineDeployments: + - class: default-worker + name: md-0 + variables: + overrides: + - name: workerConfig + value: + nodeRegistration: + ignorePreflightErrors: + - SystemVerification +``` + +Applying this configuration will result in the following value being set: + +- `KubeadmConfigTemplate`: + + - ```yaml + spec: + joinConfiguration: + nodeRegistration: + ignorePreflightErrors: + - SystemVerification + ``` + +By default, the following value will be set for both control plane and worker nodes: + +```yaml + variables: + - name: clusterConfig + value: + controlPlane: + nodeRegistration: + ignorePreflightErrors: + - SystemVerification + - name: workerConfig + value: + nodeRegistration: + ignorePreflightErrors: + - SystemVerification +``` + +This can be enabled by setting `ignorePreflightErrors` to an empty list: + +```yaml + variables: + - name: clusterConfig + value: + controlPlane: + nodeRegistration: + ignorePreflightErrors: [] + - name: workerConfig + value: + nodeRegistration: + ignorePreflightErrors: [] +``` diff --git a/examples/capi-quick-start/docker-cluster-calico-crs.yaml b/examples/capi-quick-start/docker-cluster-calico-crs.yaml index 87d178f12..aaad085ae 100644 --- a/examples/capi-quick-start/docker-cluster-calico-crs.yaml +++ b/examples/capi-quick-start/docker-cluster-calico-crs.yaml @@ -49,6 +49,7 @@ spec: - end: 198.18.1.30 start: 198.18.1.21 provider: MetalLB + controlPlane: {} dns: coreDNS: {} encryptionAtRest: diff --git a/examples/capi-quick-start/docker-cluster-calico-helm-addon.yaml b/examples/capi-quick-start/docker-cluster-calico-helm-addon.yaml index 2b24fcfdd..670912f40 100644 --- a/examples/capi-quick-start/docker-cluster-calico-helm-addon.yaml +++ b/examples/capi-quick-start/docker-cluster-calico-helm-addon.yaml @@ -44,6 +44,7 @@ spec: - end: 198.18.1.30 start: 198.18.1.21 provider: MetalLB + controlPlane: {} dns: coreDNS: {} encryptionAtRest: diff --git a/examples/capi-quick-start/docker-cluster-cilium-crs.yaml b/examples/capi-quick-start/docker-cluster-cilium-crs.yaml index af0a7bb50..a4eccd63c 100644 --- a/examples/capi-quick-start/docker-cluster-cilium-crs.yaml +++ b/examples/capi-quick-start/docker-cluster-cilium-crs.yaml @@ -49,6 +49,7 @@ spec: - end: 198.18.1.30 start: 198.18.1.21 provider: MetalLB + controlPlane: {} dns: coreDNS: {} encryptionAtRest: diff --git a/examples/capi-quick-start/docker-cluster-cilium-helm-addon.yaml b/examples/capi-quick-start/docker-cluster-cilium-helm-addon.yaml index c95472b59..8d9ff171e 100644 --- a/examples/capi-quick-start/docker-cluster-cilium-helm-addon.yaml +++ b/examples/capi-quick-start/docker-cluster-cilium-helm-addon.yaml @@ -44,6 +44,7 @@ spec: - end: 198.18.1.30 start: 198.18.1.21 provider: MetalLB + controlPlane: {} dns: coreDNS: {} encryptionAtRest: diff --git a/hack/examples/patches/initialize-variables.yaml b/hack/examples/patches/initialize-variables.yaml index f24fadddc..793cde678 100644 --- a/hack/examples/patches/initialize-variables.yaml +++ b/hack/examples/patches/initialize-variables.yaml @@ -6,6 +6,7 @@ value: - name: "clusterConfig" value: + controlPlane: {} addons: clusterAutoscaler: {} nfd: {} diff --git a/pkg/handlers/generic/mutation/handlers.go b/pkg/handlers/generic/mutation/handlers.go index f532bf2f5..b6576829c 100644 --- a/pkg/handlers/generic/mutation/handlers.go +++ b/pkg/handlers/generic/mutation/handlers.go @@ -21,6 +21,7 @@ import ( "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/mutation/imageregistries/credentials" "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/mutation/kubernetesimagerepository" "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/mutation/mirrors" + "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/mutation/noderegistration" "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/mutation/taints" "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/mutation/users" ) @@ -59,11 +60,13 @@ func MetaMutators(mgr manager.Manager) []mutation.MetaMutator { func ControlPlaneMetaMutators() []mutation.MetaMutator { return []mutation.MetaMutator{ taints.NewControlPlanePatch(), + noderegistration.NewControlPlanePatch(), } } func WorkerMetaMutators() []mutation.MetaMutator { return []mutation.MetaMutator{ taints.NewWorkerPatch(), + noderegistration.NewWorkerPatch(), } } diff --git a/pkg/handlers/generic/mutation/noderegistration/inject_controlplane.go b/pkg/handlers/generic/mutation/noderegistration/inject_controlplane.go new file mode 100644 index 000000000..b0cc4156e --- /dev/null +++ b/pkg/handlers/generic/mutation/noderegistration/inject_controlplane.go @@ -0,0 +1,117 @@ +// Copyright 2023 Nutanix. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package noderegistration + +import ( + "context" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + bootstrapv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1beta1" + controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1beta1" + runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1" + ctrl "sigs.k8s.io/controller-runtime" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/api/v1alpha1" + "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/common/pkg/capi/clustertopology/handlers/mutation" + "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/common/pkg/capi/clustertopology/patches" + "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/common/pkg/capi/clustertopology/patches/selectors" + "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/common/pkg/capi/clustertopology/variables" +) + +type nodeRegistrationControlPlanePatchHandler struct { + variableName string + variableFieldPath []string +} + +func NewControlPlanePatch() *nodeRegistrationControlPlanePatchHandler { + return newNodeRegistrationControlPlanePatchHandler( + v1alpha1.ClusterConfigVariableName, + v1alpha1.ControlPlaneConfigVariableName, + VariableName, + ) +} + +func newNodeRegistrationControlPlanePatchHandler( + variableName string, + variableFieldPath ...string, +) *nodeRegistrationControlPlanePatchHandler { + return &nodeRegistrationControlPlanePatchHandler{ + variableName: variableName, + variableFieldPath: variableFieldPath, + } +} + +func (h *nodeRegistrationControlPlanePatchHandler) Mutate( + ctx context.Context, + obj *unstructured.Unstructured, + vars map[string]apiextensionsv1.JSON, + holderRef runtimehooksv1.HolderReference, + _ ctrlclient.ObjectKey, + _ mutation.ClusterGetter, +) error { + log := ctrl.LoggerFrom(ctx).WithValues( + "holderRef", holderRef, + ) + + nodeRegistrationVar, err := variables.Get[v1alpha1.NodeRegistrationOptions]( + vars, + h.variableName, + h.variableFieldPath..., + ) + if err != nil { + if variables.IsNotFoundError(err) { + log.V(5).Info("NodeRegistration variable for worker not defined") + return nil + } + return err + } + + log = log.WithValues( + "variableName", + h.variableName, + "variableFieldPath", + h.variableFieldPath, + "variableValue", + nodeRegistrationVar, + ) + + return patches.MutateIfApplicable( + obj, vars, &holderRef, selectors.ControlPlane(), log, + func(obj *controlplanev1.KubeadmControlPlaneTemplate) error { + log.WithValues( + "patchedObjectKind", obj.GetObjectKind().GroupVersionKind().String(), + "patchedObjectName", ctrlclient.ObjectKeyFromObject(obj), + ).Info("adding nodeRegistration to control-plane node kubeadm config template") + + setIgnorePreflightErrorsForControlPlane(obj, nodeRegistrationVar.IgnorePreflightErrors) + + return nil + }) +} + +func setIgnorePreflightErrorsForControlPlane( + obj *controlplanev1.KubeadmControlPlaneTemplate, + ignorePreflightErrors []string, +) { + if len(ignorePreflightErrors) == 0 { + return + } + + if obj.Spec.Template.Spec.KubeadmConfigSpec.InitConfiguration == nil { + obj.Spec.Template.Spec.KubeadmConfigSpec.InitConfiguration = &bootstrapv1.InitConfiguration{} + } + if obj.Spec.Template.Spec.KubeadmConfigSpec.JoinConfiguration == nil { + obj.Spec.Template.Spec.KubeadmConfigSpec.JoinConfiguration = &bootstrapv1.JoinConfiguration{} + } + obj.Spec.Template.Spec.KubeadmConfigSpec.InitConfiguration.NodeRegistration.IgnorePreflightErrors = append( + obj.Spec.Template.Spec.KubeadmConfigSpec.InitConfiguration.NodeRegistration.IgnorePreflightErrors, + ignorePreflightErrors..., + ) + obj.Spec.Template.Spec.KubeadmConfigSpec.JoinConfiguration.NodeRegistration.IgnorePreflightErrors = append( + obj.Spec.Template.Spec.KubeadmConfigSpec.JoinConfiguration.NodeRegistration.IgnorePreflightErrors, + ignorePreflightErrors..., + ) +} diff --git a/pkg/handlers/generic/mutation/noderegistration/inject_controlplane_test.go b/pkg/handlers/generic/mutation/noderegistration/inject_controlplane_test.go new file mode 100644 index 000000000..4ceef7dc2 --- /dev/null +++ b/pkg/handlers/generic/mutation/noderegistration/inject_controlplane_test.go @@ -0,0 +1,69 @@ +// Copyright 2023 Nutanix. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package noderegistration + +import ( + . "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" + runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1" + + "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/api/v1alpha1" + "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/common/pkg/capi/clustertopology/handlers/mutation" + "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/common/pkg/testutils/capitest" + "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/common/pkg/testutils/capitest/request" + "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/test/helpers" +) + +var _ = Describe("Generate NodeRegistration patches for Control Plane", func() { + patchGenerator := func() mutation.GeneratePatches { + return mutation.NewMetaGeneratePatchesHandler( + "", helpers.TestEnv.Client, NewControlPlanePatch(), + ).(mutation.GeneratePatches) + } + + testDefs := []capitest.PatchTestDef{ + { + Name: "unset variable", + }, + { + Name: "IgnorePreflightErrors for control plane set", + Vars: []runtimehooksv1.Variable{ + capitest.VariableWithValue( + v1alpha1.ClusterConfigVariableName, + v1alpha1.NodeRegistrationOptions{ + IgnorePreflightErrors: []string{"all"}, + }, + v1alpha1.ControlPlaneConfigVariableName, + VariableName, + ), + }, + RequestItem: request.NewKubeadmControlPlaneTemplateRequestItem(""), + ExpectedPatchMatchers: []capitest.JSONPatchMatcher{{ + Operation: "add", + Path: "/spec/template/spec/kubeadmConfigSpec/initConfiguration/nodeRegistration/ignorePreflightErrors", + ValueMatcher: gomega.ConsistOf( + []string{"all"}, + ), + }, { + Operation: "add", + Path: "/spec/template/spec/kubeadmConfigSpec/joinConfiguration/nodeRegistration/ignorePreflightErrors", + ValueMatcher: gomega.ConsistOf( + []string{"all"}, + ), + }}, + }, + } + + // create test node for each case + for testIdx := range testDefs { + tt := testDefs[testIdx] + It(tt.Name, func() { + capitest.AssertGeneratePatches( + GinkgoT(), + patchGenerator, + &tt, + ) + }) + } +}) diff --git a/pkg/handlers/generic/mutation/noderegistration/inject_suite_test.go b/pkg/handlers/generic/mutation/noderegistration/inject_suite_test.go new file mode 100644 index 000000000..1dd6c6c6a --- /dev/null +++ b/pkg/handlers/generic/mutation/noderegistration/inject_suite_test.go @@ -0,0 +1,16 @@ +// Copyright 2023 Nutanix. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package noderegistration + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestNodeRegistrationPatch(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "NodeRegistration patches for ControlPlane and Workers suite") +} diff --git a/pkg/handlers/generic/mutation/noderegistration/inject_worker.go b/pkg/handlers/generic/mutation/noderegistration/inject_worker.go new file mode 100644 index 000000000..4f11ccc45 --- /dev/null +++ b/pkg/handlers/generic/mutation/noderegistration/inject_worker.go @@ -0,0 +1,110 @@ +// Copyright 2023 Nutanix. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package noderegistration + +import ( + "context" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + bootstrapv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1beta1" + runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1" + ctrl "sigs.k8s.io/controller-runtime" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/api/v1alpha1" + "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/common/pkg/capi/clustertopology/handlers/mutation" + "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/common/pkg/capi/clustertopology/patches" + "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/common/pkg/capi/clustertopology/patches/selectors" + "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/common/pkg/capi/clustertopology/variables" +) + +const VariableName = "nodeRegistration" + +type nodeRegistrationWorkerPatchHandler struct { + variableName string + variableFieldPath []string +} + +func NewWorkerPatch() *nodeRegistrationWorkerPatchHandler { + return newNodeRegistrationWorkerPatchHandlerWorkerPatchHandler( + v1alpha1.WorkerConfigVariableName, + VariableName, + ) +} + +func newNodeRegistrationWorkerPatchHandlerWorkerPatchHandler( + variableName string, + variableFieldPath ...string, +) *nodeRegistrationWorkerPatchHandler { + return &nodeRegistrationWorkerPatchHandler{ + variableName: variableName, + variableFieldPath: variableFieldPath, + } +} + +func (h *nodeRegistrationWorkerPatchHandler) Mutate( + ctx context.Context, + obj *unstructured.Unstructured, + vars map[string]apiextensionsv1.JSON, + holderRef runtimehooksv1.HolderReference, + _ ctrlclient.ObjectKey, + _ mutation.ClusterGetter, +) error { + log := ctrl.LoggerFrom(ctx).WithValues( + "holderRef", holderRef, + ) + + nodeRegistrationVar, err := variables.Get[v1alpha1.NodeRegistrationOptions]( + vars, + h.variableName, + h.variableFieldPath..., + ) + if err != nil { + if variables.IsNotFoundError(err) { + log.V(5).Info("NodeRegistration variable for worker not defined") + return nil + } + return err + } + + log = log.WithValues( + "variableName", + h.variableName, + "variableFieldPath", + h.variableFieldPath, + "variableValue", + nodeRegistrationVar, + ) + + return patches.MutateIfApplicable( + obj, vars, &holderRef, selectors.WorkersKubeadmConfigTemplateSelector(), log, + func(obj *bootstrapv1.KubeadmConfigTemplate) error { + log.WithValues( + "patchedObjectKind", obj.GetObjectKind().GroupVersionKind().String(), + "patchedObjectName", ctrlclient.ObjectKeyFromObject(obj), + ).Info("adding nodeRegistration options to worker node kubeadm config template") + + setIgnorePreflightErrorsForWorkers(obj, nodeRegistrationVar.IgnorePreflightErrors) + + return nil + }) +} + +func setIgnorePreflightErrorsForWorkers( + obj *bootstrapv1.KubeadmConfigTemplate, + ignorePreflightErrors []string, +) { + if len(ignorePreflightErrors) == 0 { + return + } + + if obj.Spec.Template.Spec.JoinConfiguration == nil { + obj.Spec.Template.Spec.JoinConfiguration = &bootstrapv1.JoinConfiguration{} + } + obj.Spec.Template.Spec.JoinConfiguration.NodeRegistration.IgnorePreflightErrors = append( + obj.Spec.Template.Spec.JoinConfiguration.NodeRegistration.IgnorePreflightErrors, + ignorePreflightErrors..., + ) +} diff --git a/pkg/handlers/generic/mutation/noderegistration/inject_worker_test.go b/pkg/handlers/generic/mutation/noderegistration/inject_worker_test.go new file mode 100644 index 000000000..dac5027cd --- /dev/null +++ b/pkg/handlers/generic/mutation/noderegistration/inject_worker_test.go @@ -0,0 +1,67 @@ +// Copyright 2023 Nutanix. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package noderegistration + +import ( + . "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1" + + "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/api/v1alpha1" + "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/common/pkg/capi/clustertopology/handlers/mutation" + "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/common/pkg/testutils/capitest" + "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/common/pkg/testutils/capitest/request" + "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/test/helpers" +) + +var _ = Describe("Generate NodeRegistration patches for Worker", func() { + patchGenerator := func() mutation.GeneratePatches { + return mutation.NewMetaGeneratePatchesHandler("", helpers.TestEnv.Client, NewWorkerPatch()).(mutation.GeneratePatches) + } + + testDefs := []capitest.PatchTestDef{ + { + Name: "unset variable", + }, + { + Name: "IgnorePreflightErrors for workers set", + Vars: []runtimehooksv1.Variable{ + capitest.VariableWithValue( + v1alpha1.WorkerConfigVariableName, + v1alpha1.NodeRegistrationOptions{ + IgnorePreflightErrors: []string{"all"}, + }, + VariableName, + ), + capitest.VariableWithValue( + "builtin", + apiextensionsv1.JSON{ + Raw: []byte(`{"machineDeployment": {"class": "a-worker"}}`), + }, + ), + }, + RequestItem: request.NewKubeadmConfigTemplateRequestItem(""), + ExpectedPatchMatchers: []capitest.JSONPatchMatcher{{ + Operation: "add", + Path: "/spec/template/spec/joinConfiguration/nodeRegistration/ignorePreflightErrors", + ValueMatcher: gomega.ConsistOf( + []string{"all"}, + ), + }}, + }, + } + + // create test node for each case + for testIdx := range testDefs { + tt := testDefs[testIdx] + It(tt.Name, func() { + capitest.AssertGeneratePatches( + GinkgoT(), + patchGenerator, + &tt, + ) + }) + } +}) diff --git a/pkg/handlers/generic/mutation/noderegistration/variables_test.go b/pkg/handlers/generic/mutation/noderegistration/variables_test.go new file mode 100644 index 000000000..2bae6a84d --- /dev/null +++ b/pkg/handlers/generic/mutation/noderegistration/variables_test.go @@ -0,0 +1,36 @@ +// Copyright 2023 Nutanix. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package noderegistration + +import ( + "testing" + + "k8s.io/utils/ptr" + + "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/api/v1alpha1" + "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/common/pkg/testutils/capitest" + nutanixclusterconfig "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/nutanix/clusterconfig" +) + +func TestVariableValidation(t *testing.T) { + capitest.ValidateDiscoverVariables( + t, + v1alpha1.ClusterConfigVariableName, + ptr.To(v1alpha1.NutanixClusterConfig{}.VariableSchema()), + true, + nutanixclusterconfig.NewVariable, + capitest.VariableTestDef{ + Name: "specified IgnorePreflightErrors", + Vals: v1alpha1.NutanixClusterConfigSpec{ + ControlPlane: &v1alpha1.NutanixControlPlaneSpec{ + GenericNodeSpec: v1alpha1.GenericNodeSpec{ + NodeRegistration: &v1alpha1.NodeRegistrationOptions{ + IgnorePreflightErrors: []string{"all"}, + }, + }, + }, + }, + }, + ) +}