diff --git a/api/v1alpha1/clusterconfig_types.go b/api/v1alpha1/clusterconfig_types.go index 903ab2a47..df17b8dfa 100644 --- a/api/v1alpha1/clusterconfig_types.go +++ b/api/v1alpha1/clusterconfig_types.go @@ -40,7 +40,7 @@ type ClusterConfigSpec struct { ExtraAPIServerCertSANs ExtraAPIServerCertSANs `json:"extraAPIServerCertSANs,omitempty"` // +optional - CNI *CNI `json:"cni,omitempty"` + Addons *Addons `json:"addons,omitempty"` } func (ClusterConfigSpec) VariableSchema() clusterv1.VariableSchema { @@ -49,7 +49,7 @@ func (ClusterConfigSpec) VariableSchema() clusterv1.VariableSchema { Description: "Cluster configuration", Type: "object", Properties: map[string]clusterv1.JSONSchemaProps{ - "cni": CNI{}.VariableSchema().OpenAPIV3Schema, + "addons": Addons{}.VariableSchema().OpenAPIV3Schema, "etcd": Etcd{}.VariableSchema().OpenAPIV3Schema, "extraAPIServerCertSANs": ExtraAPIServerCertSANs{}.VariableSchema().OpenAPIV3Schema, "proxy": HTTPProxy{}.VariableSchema().OpenAPIV3Schema, @@ -186,6 +186,27 @@ func (ExtraAPIServerCertSANs) VariableSchema() clusterv1.VariableSchema { } } +type Addons struct { + // +optional + CNI *CNI `json:"cni,omitempty"` + + // +optional + NFD *NFD `json:"nfd,omitempty"` +} + +func (Addons) VariableSchema() clusterv1.VariableSchema { + return clusterv1.VariableSchema{ + OpenAPIV3Schema: clusterv1.JSONSchemaProps{ + Description: "Cluster configuration", + Type: "object", + Properties: map[string]clusterv1.JSONSchemaProps{ + "cni": CNI{}.VariableSchema().OpenAPIV3Schema, + "nfd": NFD{}.VariableSchema().OpenAPIV3Schema, + }, + }, + } +} + // CNI required for providing CNI configuration. type CNI struct { Provider string `json:"provider,omitempty"` @@ -209,6 +230,18 @@ func (CNI) VariableSchema() clusterv1.VariableSchema { Enum: cniProviderEnumVals, }, }, + Required: []string{"provider"}, + }, + } +} + +// NFD tells us to enable or disable the node feature discovery addon. +type NFD struct{} + +func (NFD) VariableSchema() clusterv1.VariableSchema { + return clusterv1.VariableSchema{ + OpenAPIV3Schema: clusterv1.JSONSchemaProps{ + Type: "object", }, } } diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index de81e05c9..d613aaa71 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -11,6 +11,31 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Addons) DeepCopyInto(out *Addons) { + *out = *in + if in.CNI != nil { + in, out := &in.CNI, &out.CNI + *out = new(CNI) + **out = **in + } + if in.NFD != nil { + in, out := &in.NFD, &out.NFD + *out = new(NFD) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Addons. +func (in *Addons) DeepCopy() *Addons { + if in == nil { + return nil + } + out := new(Addons) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CNI) DeepCopyInto(out *CNI) { *out = *in @@ -75,10 +100,10 @@ func (in *ClusterConfigSpec) DeepCopyInto(out *ClusterConfigSpec) { *out = make(ExtraAPIServerCertSANs, len(*in)) copy(*out, *in) } - if in.CNI != nil { - in, out := &in.CNI, &out.CNI - *out = new(CNI) - **out = **in + if in.Addons != nil { + in, out := &in.Addons, &out.Addons + *out = new(Addons) + (*in).DeepCopyInto(*out) } } @@ -166,6 +191,21 @@ func (in *Image) DeepCopy() *Image { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NFD) DeepCopyInto(out *NFD) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NFD. +func (in *NFD) DeepCopy() *NFD { + if in == nil { + return nil + } + out := new(NFD) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ObjectMeta) DeepCopyInto(out *ObjectMeta) { *out = *in diff --git a/charts/capi-runtime-extensions/templates/nfd/manifests/node-feature-discovery-configmap.yaml b/charts/capi-runtime-extensions/templates/nfd/manifests/node-feature-discovery-configmap.yaml new file mode 100644 index 000000000..df154fcc6 --- /dev/null +++ b/charts/capi-runtime-extensions/templates/nfd/manifests/node-feature-discovery-configmap.yaml @@ -0,0 +1,924 @@ +# Copyright 2023 D2iQ, Inc. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +#================================================================= +# DO NOT EDIT THIS FILE +# IT HAS BEEN GENERATED BY /hack/addons/update-node-feature-discovery-manifests.sh +#================================================================= +apiVersion: v1 +data: + node-feature-discovery.yaml: | + apiVersion: v1 + kind: Namespace + metadata: + name: node-feature-discovery + --- + apiVersion: apiextensions.k8s.io/v1 + kind: CustomResourceDefinition + metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.12.1 + name: nodefeatures.nfd.k8s-sigs.io + spec: + group: nfd.k8s-sigs.io + names: + kind: NodeFeature + listKind: NodeFeatureList + plural: nodefeatures + singular: nodefeature + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: NodeFeature resource holds the features discovered for one node + in the cluster. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: NodeFeatureSpec describes a NodeFeature object. + properties: + features: + description: Features is the full "raw" features data that has been + discovered. + properties: + attributes: + additionalProperties: + description: AttributeFeatureSet is a set of features having + string value. + properties: + elements: + additionalProperties: + type: string + type: object + required: + - elements + type: object + description: Attributes contains all the attribute-type features + of the node. + type: object + flags: + additionalProperties: + description: FlagFeatureSet is a set of simple features only + containing names without values. + properties: + elements: + additionalProperties: + description: Nil is a dummy empty struct for protobuf + compatibility + type: object + type: object + required: + - elements + type: object + description: Flags contains all the flag-type features of the + node. + type: object + instances: + additionalProperties: + description: InstanceFeatureSet is a set of features each of + which is an instance having multiple attributes. + properties: + elements: + items: + description: InstanceFeature represents one instance of + a complex features, e.g. a device. + properties: + attributes: + additionalProperties: + type: string + type: object + required: + - attributes + type: object + type: array + required: + - elements + type: object + description: Instances contains all the instance-type features + of the node. + type: object + type: object + labels: + additionalProperties: + type: string + description: Labels is the set of node labels that are requested to + be created. + type: object + type: object + required: + - spec + type: object + served: true + storage: true + --- + apiVersion: apiextensions.k8s.io/v1 + kind: CustomResourceDefinition + metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.12.1 + name: nodefeaturerules.nfd.k8s-sigs.io + spec: + group: nfd.k8s-sigs.io + names: + kind: NodeFeatureRule + listKind: NodeFeatureRuleList + plural: nodefeaturerules + shortNames: + - nfr + singular: nodefeaturerule + scope: Cluster + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: NodeFeatureRule resource specifies a configuration for feature-based + customization of node objects, such as node labeling. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: NodeFeatureRuleSpec describes a NodeFeatureRule. + properties: + rules: + description: Rules is a list of node customization rules. + items: + description: Rule defines a rule for node customization such as + labeling. + properties: + extendedResources: + additionalProperties: + type: string + description: ExtendedResources to create if the rule matches. + type: object + labels: + additionalProperties: + type: string + description: Labels to create if the rule matches. + type: object + labelsTemplate: + description: LabelsTemplate specifies a template to expand for + dynamically generating multiple labels. Data (after template + expansion) must be keys with an optional value ([=]) + separated by newlines. + type: string + matchAny: + description: MatchAny specifies a list of matchers one of which + must match. + items: + description: MatchAnyElem specifies one sub-matcher of MatchAny. + properties: + matchFeatures: + description: MatchFeatures specifies a set of matcher + terms all of which must match. + items: + description: FeatureMatcherTerm defines requirements + against one feature set. All requirements (specified + as MatchExpressions) are evaluated against each element + in the feature set. + properties: + feature: + type: string + matchExpressions: + additionalProperties: + description: "MatchExpression specifies an expression + to evaluate against a set of input values. It + contains an operator that is applied when matching + the input and an array of values that the operator + evaluates the input against. \n NB: CreateMatchExpression + or MustCreateMatchExpression() should be used + for creating new instances. \n NB: Validate() + must be called if Op or Value fields are modified + or if a new instance is created from scratch + without using the helper functions." + properties: + op: + description: Op is the operator to be applied. + enum: + - In + - NotIn + - InRegexp + - Exists + - DoesNotExist + - Gt + - Lt + - GtLt + - IsTrue + - IsFalse + type: string + value: + description: Value is the list of values that + the operand evaluates the input against. + Value should be empty if the operator is + Exists, DoesNotExist, IsTrue or IsFalse. + Value should contain exactly one element + if the operator is Gt or Lt and exactly + two elements if the operator is GtLt. In + other cases Value should contain at least + one element. + items: + type: string + type: array + required: + - op + type: object + description: MatchExpressionSet contains a set of + MatchExpressions, each of which is evaluated against + a set of input values. + type: object + required: + - feature + - matchExpressions + type: object + type: array + required: + - matchFeatures + type: object + type: array + matchFeatures: + description: MatchFeatures specifies a set of matcher terms + all of which must match. + items: + description: FeatureMatcherTerm defines requirements against + one feature set. All requirements (specified as MatchExpressions) + are evaluated against each element in the feature set. + properties: + feature: + type: string + matchExpressions: + additionalProperties: + description: "MatchExpression specifies an expression + to evaluate against a set of input values. It contains + an operator that is applied when matching the input + and an array of values that the operator evaluates + the input against. \n NB: CreateMatchExpression or + MustCreateMatchExpression() should be used for creating + new instances. \n NB: Validate() must be called if + Op or Value fields are modified or if a new instance + is created from scratch without using the helper functions." + properties: + op: + description: Op is the operator to be applied. + enum: + - In + - NotIn + - InRegexp + - Exists + - DoesNotExist + - Gt + - Lt + - GtLt + - IsTrue + - IsFalse + type: string + value: + description: Value is the list of values that the + operand evaluates the input against. Value should + be empty if the operator is Exists, DoesNotExist, + IsTrue or IsFalse. Value should contain exactly + one element if the operator is Gt or Lt and exactly + two elements if the operator is GtLt. In other + cases Value should contain at least one element. + items: + type: string + type: array + required: + - op + type: object + description: MatchExpressionSet contains a set of MatchExpressions, + each of which is evaluated against a set of input values. + type: object + required: + - feature + - matchExpressions + type: object + type: array + name: + description: Name of the rule. + type: string + taints: + description: Taints to create if the rule matches. + items: + description: The node this Taint is attached to has the "effect" + on any pod that does not tolerate the Taint. + properties: + effect: + description: Required. The effect of the taint on pods + that do not tolerate the taint. Valid effects are NoSchedule, + PreferNoSchedule and NoExecute. + type: string + key: + description: Required. The taint key to be applied to + a node. + type: string + timeAdded: + description: TimeAdded represents the time at which the + taint was added. It is only written for NoExecute taints. + format: date-time + type: string + value: + description: The taint value corresponding to the taint + key. + type: string + required: + - effect + - key + type: object + type: array + vars: + additionalProperties: + type: string + description: Vars is the variables to store if the rule matches. + Variables do not directly inflict any changes in the node + object. However, they can be referenced from other rules enabling + more complex rule hierarchies, without exposing intermediary + output values as labels. + type: object + varsTemplate: + description: VarsTemplate specifies a template to expand for + dynamically generating multiple variables. Data (after template + expansion) must be keys with an optional value ([=]) + separated by newlines. + type: string + required: + - name + type: object + type: array + required: + - rules + type: object + required: + - spec + type: object + served: true + storage: true + --- + apiVersion: v1 + kind: ServiceAccount + metadata: + labels: + app.kubernetes.io/instance: node-feature-discovery + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: node-feature-discovery + app.kubernetes.io/version: v0.14.1 + helm.sh/chart: node-feature-discovery-0.14.1 + name: node-feature-discovery + namespace: node-feature-discovery + --- + apiVersion: v1 + kind: ServiceAccount + metadata: + labels: + app.kubernetes.io/instance: node-feature-discovery + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: node-feature-discovery + app.kubernetes.io/version: v0.14.1 + helm.sh/chart: node-feature-discovery-0.14.1 + name: nfd-gc + namespace: node-feature-discovery + --- + apiVersion: v1 + kind: ServiceAccount + metadata: + labels: + app.kubernetes.io/instance: node-feature-discovery + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: node-feature-discovery + app.kubernetes.io/version: v0.14.1 + helm.sh/chart: node-feature-discovery-0.14.1 + name: node-feature-discovery-worker + namespace: node-feature-discovery + --- + apiVersion: v1 + data: + nfd-master.conf: "null" + kind: ConfigMap + metadata: + labels: + app.kubernetes.io/instance: node-feature-discovery + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: node-feature-discovery + app.kubernetes.io/version: v0.14.1 + helm.sh/chart: node-feature-discovery-0.14.1 + name: node-feature-discovery-master-conf + namespace: node-feature-discovery + --- + apiVersion: v1 + data: + nfd-topology-updater.conf: "null" + kind: ConfigMap + metadata: + labels: + app.kubernetes.io/instance: node-feature-discovery + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: node-feature-discovery + app.kubernetes.io/version: v0.14.1 + helm.sh/chart: node-feature-discovery-0.14.1 + name: node-feature-discovery-topology-updater-conf + namespace: node-feature-discovery + --- + apiVersion: v1 + data: + nfd-worker.conf: |- + sources: + pci: + deviceLabelFields: + - class + - vendor + kind: ConfigMap + metadata: + labels: + app.kubernetes.io/instance: node-feature-discovery + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: node-feature-discovery + app.kubernetes.io/version: v0.14.1 + helm.sh/chart: node-feature-discovery-0.14.1 + name: node-feature-discovery-worker-conf + namespace: node-feature-discovery + --- + apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRole + metadata: + labels: + app.kubernetes.io/instance: node-feature-discovery + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: node-feature-discovery + app.kubernetes.io/version: v0.14.1 + helm.sh/chart: node-feature-discovery-0.14.1 + name: node-feature-discovery + rules: + - apiGroups: + - "" + resources: + - nodes + - nodes/status + verbs: + - get + - patch + - update + - list + - apiGroups: + - nfd.k8s-sigs.io + resources: + - nodefeatures + - nodefeaturerules + verbs: + - get + - list + - watch + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - create + - apiGroups: + - coordination.k8s.io + resourceNames: + - nfd-master.nfd.kubernetes.io + resources: + - leases + verbs: + - get + - update + --- + apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRole + metadata: + labels: + app.kubernetes.io/instance: node-feature-discovery + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: node-feature-discovery + app.kubernetes.io/version: v0.14.1 + helm.sh/chart: node-feature-discovery-0.14.1 + name: node-feature-discovery-gc + rules: + - apiGroups: + - "" + resources: + - nodes + verbs: + - list + - watch + - apiGroups: + - "" + resources: + - nodes/proxy + verbs: + - get + - apiGroups: + - topology.node.k8s.io + resources: + - noderesourcetopologies + verbs: + - delete + - list + - apiGroups: + - nfd.k8s-sigs.io + resources: + - nodefeatures + verbs: + - delete + - list + --- + apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRoleBinding + metadata: + labels: + app.kubernetes.io/instance: node-feature-discovery + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: node-feature-discovery + app.kubernetes.io/version: v0.14.1 + helm.sh/chart: node-feature-discovery-0.14.1 + name: node-feature-discovery + roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: node-feature-discovery + subjects: + - kind: ServiceAccount + name: node-feature-discovery + namespace: node-feature-discovery + --- + apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRoleBinding + metadata: + labels: + app.kubernetes.io/instance: node-feature-discovery + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: node-feature-discovery + app.kubernetes.io/version: v0.14.1 + helm.sh/chart: node-feature-discovery-0.14.1 + name: node-feature-discovery-gc + roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: node-feature-discovery-gc + subjects: + - kind: ServiceAccount + name: nfd-gc + namespace: node-feature-discovery + --- + apiVersion: rbac.authorization.k8s.io/v1 + kind: Role + metadata: + labels: + app.kubernetes.io/instance: node-feature-discovery + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: node-feature-discovery + app.kubernetes.io/version: v0.14.1 + helm.sh/chart: node-feature-discovery-0.14.1 + name: node-feature-discovery-worker + namespace: node-feature-discovery + rules: + - apiGroups: + - nfd.k8s-sigs.io + resources: + - nodefeatures + verbs: + - create + - get + - update + --- + apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + labels: + app.kubernetes.io/instance: node-feature-discovery + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: node-feature-discovery + app.kubernetes.io/version: v0.14.1 + helm.sh/chart: node-feature-discovery-0.14.1 + name: node-feature-discovery-worker + namespace: node-feature-discovery + roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: node-feature-discovery-worker + subjects: + - kind: ServiceAccount + name: node-feature-discovery-worker + namespace: node-feature-discovery + --- + apiVersion: v1 + kind: Service + metadata: + labels: + app.kubernetes.io/instance: node-feature-discovery + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: node-feature-discovery + app.kubernetes.io/version: v0.14.1 + helm.sh/chart: node-feature-discovery-0.14.1 + role: master + name: node-feature-discovery-master + namespace: node-feature-discovery + spec: + ports: + - name: grpc + port: 8080 + protocol: TCP + targetPort: grpc + selector: + app.kubernetes.io/instance: node-feature-discovery + app.kubernetes.io/name: node-feature-discovery + role: master + type: ClusterIP + --- + apiVersion: apps/v1 + kind: DaemonSet + metadata: + labels: + app.kubernetes.io/instance: node-feature-discovery + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: node-feature-discovery + app.kubernetes.io/version: v0.14.1 + helm.sh/chart: node-feature-discovery-0.14.1 + role: worker + name: node-feature-discovery-worker + namespace: node-feature-discovery + spec: + selector: + matchLabels: + app.kubernetes.io/instance: node-feature-discovery + app.kubernetes.io/name: node-feature-discovery + role: worker + template: + metadata: + labels: + app.kubernetes.io/instance: node-feature-discovery + app.kubernetes.io/name: node-feature-discovery + role: worker + spec: + containers: + - args: + - -server=node-feature-discovery-master:8080 + - -metrics=8081 + command: + - nfd-worker + env: + - name: NODE_NAME + valueFrom: + fieldRef: + fieldPath: spec.nodeName + image: registry.k8s.io/nfd/node-feature-discovery:v0.14.1-minimal + imagePullPolicy: IfNotPresent + name: worker + ports: + - containerPort: 8081 + name: metrics + resources: {} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + volumeMounts: + - mountPath: /host-boot + name: host-boot + readOnly: true + - mountPath: /host-etc/os-release + name: host-os-release + readOnly: true + - mountPath: /host-sys + name: host-sys + readOnly: true + - mountPath: /host-usr/lib + name: host-usr-lib + readOnly: true + - mountPath: /host-lib + name: host-lib + readOnly: true + - mountPath: /etc/kubernetes/node-feature-discovery/source.d/ + name: source-d + readOnly: true + - mountPath: /etc/kubernetes/node-feature-discovery/features.d/ + name: features-d + readOnly: true + - mountPath: /etc/kubernetes/node-feature-discovery + name: nfd-worker-conf + readOnly: true + dnsPolicy: ClusterFirstWithHostNet + securityContext: {} + serviceAccountName: node-feature-discovery-worker + tolerations: + - effect: NoSchedule + key: node-role.kubernetes.io/master + - effect: NoSchedule + key: node-role.kubernetes.io/control-plane + volumes: + - hostPath: + path: /boot + name: host-boot + - hostPath: + path: /etc/os-release + name: host-os-release + - hostPath: + path: /sys + name: host-sys + - hostPath: + path: /usr/lib + name: host-usr-lib + - hostPath: + path: /lib + name: host-lib + - hostPath: + path: /etc/kubernetes/node-feature-discovery/source.d/ + name: source-d + - hostPath: + path: /etc/kubernetes/node-feature-discovery/features.d/ + name: features-d + - configMap: + items: + - key: nfd-worker.conf + path: nfd-worker.conf + name: node-feature-discovery-worker-conf + name: nfd-worker-conf + --- + apiVersion: apps/v1 + kind: Deployment + metadata: + labels: + app.kubernetes.io/instance: node-feature-discovery + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: node-feature-discovery + app.kubernetes.io/version: v0.14.1 + helm.sh/chart: node-feature-discovery-0.14.1 + role: master + name: node-feature-discovery-master + namespace: node-feature-discovery + spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: node-feature-discovery + app.kubernetes.io/name: node-feature-discovery + role: master + template: + metadata: + labels: + app.kubernetes.io/instance: node-feature-discovery + app.kubernetes.io/name: node-feature-discovery + role: master + spec: + affinity: + nodeAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - preference: + matchExpressions: + - key: node-role.kubernetes.io/master + operator: In + values: + - "" + weight: 1 + - preference: + matchExpressions: + - key: node-role.kubernetes.io/control-plane + operator: In + values: + - "" + weight: 1 + containers: + - args: + - -port=8080 + - -extra-label-ns=nvidia.com,beta.amd.com,amd.com + - -crd-controller=true + - -metrics=8081 + command: + - nfd-master + env: + - name: NODE_NAME + valueFrom: + fieldRef: + fieldPath: spec.nodeName + image: registry.k8s.io/nfd/node-feature-discovery:v0.14.1-minimal + imagePullPolicy: IfNotPresent + livenessProbe: + exec: + command: + - /usr/bin/grpc_health_probe + - -addr=:8080 + initialDelaySeconds: 10 + periodSeconds: 10 + name: master + ports: + - containerPort: 8080 + name: grpc + - containerPort: 8081 + name: metrics + readinessProbe: + exec: + command: + - /usr/bin/grpc_health_probe + - -addr=:8080 + failureThreshold: 10 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: {} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + volumeMounts: + - mountPath: /etc/kubernetes/node-feature-discovery + name: nfd-master-conf + readOnly: true + enableServiceLinks: false + securityContext: {} + serviceAccountName: node-feature-discovery + tolerations: + - effect: NoSchedule + key: node-role.kubernetes.io/master + operator: Equal + value: "" + - effect: NoSchedule + key: node-role.kubernetes.io/control-plane + operator: Equal + value: "" + volumes: + - configMap: + items: + - key: nfd-master.conf + path: nfd-master.conf + name: node-feature-discovery-master-conf + name: nfd-master-conf + --- + apiVersion: apps/v1 + kind: Deployment + metadata: + labels: + app.kubernetes.io/instance: node-feature-discovery + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: node-feature-discovery + app.kubernetes.io/version: v0.14.1 + helm.sh/chart: node-feature-discovery-0.14.1 + role: gc + name: node-feature-discovery-gc + namespace: node-feature-discovery + spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: node-feature-discovery + app.kubernetes.io/name: node-feature-discovery + role: gc + template: + metadata: + labels: + app.kubernetes.io/instance: node-feature-discovery + app.kubernetes.io/name: node-feature-discovery + role: gc + spec: + containers: + - args: + - -gc-interval=1h + command: + - nfd-gc + env: + - name: NODE_NAME + valueFrom: + fieldRef: + fieldPath: spec.nodeName + image: registry.k8s.io/nfd/node-feature-discovery:v0.14.1-minimal + imagePullPolicy: IfNotPresent + name: gc + resources: {} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + dnsPolicy: ClusterFirstWithHostNet + securityContext: {} + serviceAccountName: nfd-gc +kind: ConfigMap +metadata: + creationTimestamp: null + name: node-feature-discovery diff --git a/cmd/main.go b/cmd/main.go index 4788b9928..ac83efbf3 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -34,6 +34,7 @@ import ( "github.com/d2iq-labs/capi-runtime-extensions/pkg/handlers/extraapiservercertsans" "github.com/d2iq-labs/capi-runtime-extensions/pkg/handlers/httpproxy" "github.com/d2iq-labs/capi-runtime-extensions/pkg/handlers/kubernetesimagerepository" + "github.com/d2iq-labs/capi-runtime-extensions/pkg/handlers/nfd" "github.com/d2iq-labs/capi-runtime-extensions/pkg/handlers/servicelbgc" ) @@ -83,12 +84,14 @@ func main() { "Bind address to expose the pprof profiler (e.g. localhost:6060)") calicoCNIConfig := &calico.CalicoCNIConfig{} + nfdConfig := &nfd.NFDConfig{} runtimeWebhookServerOpts := server.NewServerOptions() // Initialize and parse command line flags. initFlags(pflag.CommandLine) runtimeWebhookServerOpts.AddFlags(pflag.CommandLine) + nfdConfig.AddFlags("nfd", pflag.CommandLine) calicoCNIConfig.AddFlags("calicocni", pflag.CommandLine) pflag.CommandLine.SetNormalizeFunc(cliflag.WordSepNormalizeFunc) pflag.CommandLine.AddGoFlagSet(flag.CommandLine) @@ -147,6 +150,7 @@ func main() { // This Calico handler relies on a variable but does not generate a patch. // Instead it creates other resources in the API. calico.NewMetaHandler(mgr.GetClient(), calicoCNIConfig), + nfd.NewMetaHandler(mgr.GetClient(), nfdConfig), clusterconfig.NewVariable(), mutation.NewMetaGeneratePatchesHandler("clusterConfigPatch", metaPatchHandlers...), } diff --git a/docs/content/calico-cni.md b/docs/content/calico-cni.md index c0c628ad5..e42038df7 100644 --- a/docs/content/calico-cni.md +++ b/docs/content/calico-cni.md @@ -39,8 +39,9 @@ spec: variables: - name: clusterConfig value: - cni: - provider: calico + addons: + cni: + provider: calico ``` As ClusterResourceSets must exist in the same name as the cluster they apply to, the lifecycle hook copies default diff --git a/docs/content/nfd.md b/docs/content/nfd.md new file mode 100644 index 000000000..0184dae5d --- /dev/null +++ b/docs/content/nfd.md @@ -0,0 +1,41 @@ +--- +title: "Node Feature Discovery" +--- + +By leveraging CAPI cluster lifecycle hooks, this handler deploys [Node Feature +Discovery](https://github.com/kubernetes-sigs/node-feature-discovery) (NFD) on the new cluster via +`ClusterResourceSets` at the `AfterControlPlaneInitialized` phase. + +Deployment of NFD is opt-in using the following configuration for the lifecycle hook to perform any actions. The hook +creates a `ClusterResourceSet` to deploy the NFD resources. + +To enable the meta handler enable the `clusterconfigvars` and `clusterconfigpatch` external patches on `ClusterClass`. + +```yaml +apiVersion: cluster.x-k8s.io/v1beta1 +kind: ClusterClass +metadata: + name: +spec: + patches: + - name: cluster-config + external: + generateExtension: "clusterconfigpatch.capi-runtime-extensions" + discoverVariablesExtension: "clusterconfigvars.capi-runtime-extensions" +``` + +On the cluster resource then specify this `nfd` value: + +```yaml +apiVersion: cluster.x-k8s.io/v1beta1 +kind: Cluster +metadata: + name: +spec: + topology: + variables: + - name: clusterConfig + value: + addons: + nfd: {} +``` diff --git a/examples/capi-quick-start/capd-cluster.yaml b/examples/capi-quick-start/capd-cluster.yaml index 4869676cd..7c45a0790 100644 --- a/examples/capi-quick-start/capd-cluster.yaml +++ b/examples/capi-quick-start/capd-cluster.yaml @@ -22,8 +22,10 @@ spec: variables: - name: clusterConfig value: - cni: - provider: calico + addons: + cni: + provider: calico + nfd: {} version: v1.27.5 workers: machineDeployments: diff --git a/hack/addons/kustomize/nfd/kustomization.yaml.tmpl b/hack/addons/kustomize/nfd/kustomization.yaml.tmpl new file mode 100644 index 000000000..4579aea05 --- /dev/null +++ b/hack/addons/kustomize/nfd/kustomization.yaml.tmpl @@ -0,0 +1,24 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +metadata: + name: node-feature-discovery + +sortOptions: + order: fifo + +resources: +- namespace.yaml + +helmCharts: +- name: node-feature-discovery + includeCRDs: true + valuesFile: node-feature-discovery-values.yaml + valuesInline: + image: + tag: "v${NODE_FEATURE_VERSION}-minimal" + releaseName: node-feature-discovery + version: ${NODE_FEATURE_VERSION} + repo: https://kubernetes-sigs.github.io/node-feature-discovery/charts + +namespace: node-feature-discovery diff --git a/hack/addons/kustomize/nfd/namespace.yaml b/hack/addons/kustomize/nfd/namespace.yaml new file mode 100644 index 000000000..edc1897bd --- /dev/null +++ b/hack/addons/kustomize/nfd/namespace.yaml @@ -0,0 +1,7 @@ +# Copyright 2023 D2iQ, Inc. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +apiVersion: v1 +kind: Namespace +metadata: + name: node-feature-discovery diff --git a/hack/addons/kustomize/nfd/node-feature-discovery-values.yaml b/hack/addons/kustomize/nfd/node-feature-discovery-values.yaml new file mode 100644 index 000000000..513f9ffde --- /dev/null +++ b/hack/addons/kustomize/nfd/node-feature-discovery-values.yaml @@ -0,0 +1,23 @@ +# Copyright 2023 D2iQ, Inc. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +--- +master: + extraLabelNs: + - nvidia.com + - beta.amd.com + - amd.com + +worker: ### + config: + sources: + pci: + deviceLabelFields: + - "class" + - "vendor" + tolerations: + - effect: NoSchedule + key: node-role.kubernetes.io/master + - effect: NoSchedule + key: node-role.kubernetes.io/control-plane +### diff --git a/hack/addons/update-node-feature-discovery-manifests.sh b/hack/addons/update-node-feature-discovery-manifests.sh new file mode 100755 index 000000000..85fa1e31d --- /dev/null +++ b/hack/addons/update-node-feature-discovery-manifests.sh @@ -0,0 +1,41 @@ +#!/bin/bash +set -euo pipefail +IFS=$'\n\t' + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +readonly SCRIPT_DIR + +# shellcheck source=hack/common.sh +source "${SCRIPT_DIR}/../common.sh" + +if [ -z "${NODE_FEATURE_VERSION:-}" ]; then + echo "Missing environment variable: NODE_FEATURE_VERSION" + exit 1 +fi + +ASSETS_DIR="$(mktemp -d -p "${TMPDIR:-/tmp}")" +readonly ASSETS_DIR +trap_add "rm -rf ${ASSETS_DIR}" EXIT + +readonly FILE_NAME="node-feature-discovery.yaml" + +readonly KUSTOMIZE_BASE_DIR="${SCRIPT_DIR}/kustomize/nfd/" +envsubst -no-unset <"${KUSTOMIZE_BASE_DIR}/kustomization.yaml.tmpl" >"${ASSETS_DIR}/kustomization.yaml" +cp "${KUSTOMIZE_BASE_DIR}"/*.yaml "${ASSETS_DIR}" +kustomize build --enable-helm "${ASSETS_DIR}" >"${ASSETS_DIR}/${FILE_NAME}" + +kubectl create configmap node-feature-discovery --dry-run=client --output yaml \ + --from-file "${ASSETS_DIR}/${FILE_NAME}" \ + >"${GIT_REPO_ROOT}/charts/capi-runtime-extensions/templates/nfd/manifests/node-feature-discovery-configmap.yaml" + +# add warning not to edit file directly +cat <"${GIT_REPO_ROOT}/charts/capi-runtime-extensions/templates/nfd/manifests/node-feature-discovery-configmap-temp.yaml" +#================================================================= +# DO NOT EDIT THIS FILE +# IT HAS BEEN GENERATED BY /hack/addons/update-node-feature-discovery-manifests.sh +#================================================================= +$(cat "${GIT_REPO_ROOT}/charts/capi-runtime-extensions/templates/nfd/manifests/node-feature-discovery-configmap.yaml") +EOF + +mv "${GIT_REPO_ROOT}/charts/capi-runtime-extensions/templates/nfd/manifests/node-feature-discovery-configmap-temp.yaml" \ + "${GIT_REPO_ROOT}/charts/capi-runtime-extensions/templates/nfd/manifests/node-feature-discovery-configmap.yaml" diff --git a/hack/common.sh b/hack/common.sh index da6a2f130..0103b325f 100644 --- a/hack/common.sh +++ b/hack/common.sh @@ -1,7 +1,11 @@ #!/usr/bin/env bash -# Copyright 2023 D2iQ, Inc. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - GIT_REPO_ROOT="$(git rev-parse --show-toplevel)" export GIT_REPO_ROOT + +trap_add() { + local -r sig="${2:?Signal required}" + local -r hdls="$(trap -p "${sig}" | cut -f2 -d \')" + # shellcheck disable=SC2064 # Quotes are required here to properly expand when adding the new trap. + trap "${hdls}${hdls:+;}${1:?Handler required}" "${sig}" +} diff --git a/hack/examples/kustomization.yaml.tmpl b/hack/examples/kustomization.yaml.tmpl index e322e8fa4..399e5ec61 100644 --- a/hack/examples/kustomization.yaml.tmpl +++ b/hack/examples/kustomization.yaml.tmpl @@ -87,5 +87,7 @@ patches: value: - name: "clusterConfig" value: - cni: - provider: calico + addons: + cni: + provider: calico + nfd: {} diff --git a/hack/examples/sync.sh b/hack/examples/sync.sh index e0603b39b..fb9e77d89 100755 --- a/hack/examples/sync.sh +++ b/hack/examples/sync.sh @@ -17,7 +17,7 @@ trap 'rm -rf ${CAPD_KUSTOMIZATION_FILE} ${EXAMPLES_KUSTOMIZATION_FILE}' EXIT CLUSTERCTL_VERSION=$(clusterctl version -o short 2>/dev/null) envsubst \ <"${CAPD_KUSTOMIZATION_FILE}.tmpl" >"${CAPD_KUSTOMIZATION_FILE}" # replace the kubernetes version -envsubst <"${EXAMPLES_KUSTOMIZATION_FILE}.tmpl" >"${EXAMPLES_KUSTOMIZATION_FILE}" +envsubst -no-unset <"${EXAMPLES_KUSTOMIZATION_FILE}.tmpl" >"${EXAMPLES_KUSTOMIZATION_FILE}" mkdir -p examples/capi-quick-start # Sync ClusterClass and all Templates diff --git a/hack/kind/create-cluster.sh b/hack/kind/create-cluster.sh index 97c012cd4..e8d322365 100755 --- a/hack/kind/create-cluster.sh +++ b/hack/kind/create-cluster.sh @@ -79,7 +79,7 @@ function run_cmd() { # create/override base config export KINDEST_IMAGE="${kindest_image}" - envsubst <"$base_config" >"$cluster_config" + envsubst -no-unset <"$base_config" >"$cluster_config" kind create cluster --name "$cluster_name" --config "$cluster_config" } diff --git a/make/addons.mk b/make/addons.mk index 2f8a8d828..b205fc630 100644 --- a/make/addons.mk +++ b/make/addons.mk @@ -2,7 +2,12 @@ # SPDX-License-Identifier: Apache-2.0 export CALICO_VERSION := v3.26.1 +export NODE_FEATURE_VERSION := 0.14.1 .PHONY: update-addon.calico update-addon.calico: ; $(info $(M) updating calico manifests) ./hack/addons/update-calico-manifests.sh + +.PHONY: update-addon.nfd +update-addon.nfd: ; $(info $(M) updating node feature discovery manifests) + ./hack/addons/update-node-feature-discovery-manifests.sh diff --git a/make/go.mk b/make/go.mk index 7b93d1d85..00ab8f236 100644 --- a/make/go.mk +++ b/make/go.mk @@ -38,15 +38,15 @@ endef .PHONY: test test: ## Runs go tests for all modules in repository ifneq ($(wildcard $(REPO_ROOT)/go.mod),) -test: test.root +test: go-generate test.root endif ifneq ($(words $(GO_SUBMODULES_NO_DOCS)),0) -test: $(addprefix test.,$(GO_SUBMODULES_NO_DOCS:/go.mod=)) +test: go-generate $(addprefix test.,$(GO_SUBMODULES_NO_DOCS:/go.mod=)) endif .PHONY: test.% test.%: ## Runs go tests for a specific module -test.%: ; $(info $(M) running tests$(if $(GOTEST_RUN), matching "$(GOTEST_RUN)") for $* module) +test.%: go-generate ; $(info $(M) running tests$(if $(GOTEST_RUN), matching "$(GOTEST_RUN)") for $* module) $(if $(filter-out root,$*),cd $* && )$(call go_test) .PHONY: integration-test diff --git a/pkg/handlers/cni/calico/handler.go b/pkg/handlers/cni/calico/handler.go index f82fae26a..0fe36b466 100644 --- a/pkg/handlers/cni/calico/handler.go +++ b/pkg/handlers/cni/calico/handler.go @@ -49,7 +49,7 @@ func (c *CalicoCNIConfig) AddFlags(prefix string, flags *pflag.FlagSet) { &c.defaultsNamespace, prefix+".defaultsNamespace", corev1.NamespaceDefault, - "name of the ConfigMap used to deploy Tigera Operator", + "namespace of the ConfigMap used to deploy Tigera Operator", ) flags.StringVar( @@ -91,7 +91,7 @@ func NewMetaHandler( client: c, config: cfg, variableName: handlers.MetaVariableName, - variablePath: []string{variableName}, + variablePath: []string{"addons", variableName}, } } diff --git a/pkg/handlers/nfd/handler.go b/pkg/handlers/nfd/handler.go new file mode 100644 index 000000000..9191d1fd5 --- /dev/null +++ b/pkg/handlers/nfd/handler.go @@ -0,0 +1,189 @@ +// Copyright 2023 D2iQ, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package nfd + +import ( + "context" + "fmt" + + "github.com/spf13/pflag" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + capiv1 "sigs.k8s.io/cluster-api/api/v1beta1" + crsv1 "sigs.k8s.io/cluster-api/exp/addons/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" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + "github.com/d2iq-labs/capi-runtime-extensions/api/v1alpha1" + "github.com/d2iq-labs/capi-runtime-extensions/common/pkg/capi/clustertopology/variables" + "github.com/d2iq-labs/capi-runtime-extensions/common/pkg/k8s/client" + "github.com/d2iq-labs/capi-runtime-extensions/pkg/handlers" +) + +type NFDConfig struct { + defaultsNamespace string + defaultNFDConfigMap string +} + +type DefaultNFD struct { + client ctrlclient.Client + config *NFDConfig + + variableName string // points to the global config variable + variablePath []string // path of this variable on the global config variable +} + +func (n *NFDConfig) AddFlags(prefix string, flags *pflag.FlagSet) { + flags.StringVar( + &n.defaultsNamespace, + prefix+".defaultsNamespace", + corev1.NamespaceDefault, + "namespace location of ConfigMap used to deploy Node Feature Discovery (NFD).", + ) + flags.StringVar( + &n.defaultNFDConfigMap, + prefix+".default-nfd-configmap-name", + "node-feature-discovery", + "name of the ConfigMap used to deploy Node Feature Discovery (NFD)", + ) +} + +const ( + variableName = "nfd" +) + +func NewMetaHandler( + c ctrlclient.Client, + cfg *NFDConfig, +) *DefaultNFD { + return &DefaultNFD{ + client: c, + config: cfg, + variableName: handlers.MetaVariableName, + variablePath: []string{"addons", variableName}, + } +} + +func (n *DefaultNFD) Name() string { + return "DefaultNFD" +} + +func (n *DefaultNFD) AfterControlPlaneInitialized( + ctx context.Context, + req *runtimehooksv1.AfterControlPlaneInitializedRequest, + resp *runtimehooksv1.AfterControlPlaneInitializedResponse, +) { + clusterKey := ctrlclient.ObjectKeyFromObject(&req.Cluster) + + log := ctrl.LoggerFrom(ctx).WithValues( + "cluster", + clusterKey, + ) + varMap := variables.ClusterVariablesToVariablesMap(req.Cluster.Spec.Topology.Variables) + + _, found, err := variables.Get[v1alpha1.NFD](varMap, n.variableName, n.variablePath...) + if err != nil { + resp.SetStatus(runtimehooksv1.ResponseStatusFailure) + log.Error(err, "failed to get NFD variable") + return + } + // If the variable isn't there or disabled we can ignore it. + if !found { + log.V(4).Info( + "Skipping NFD handler. Not specified in cluster config.", + ) + return + } + + cm, err := n.ensureNFDConfigmapForCluster(ctx, &req.Cluster) + if err != nil { + resp.SetStatus(runtimehooksv1.ResponseStatusFailure) + log.Error(err, "failed to apply NFD ConfigMap for cluster") + return + } + err = n.ensureNFDCRSForCluster(ctx, &req.Cluster, cm) + if err != nil { + resp.SetStatus(runtimehooksv1.ResponseStatusFailure) + log.Error(err, "failed to apply NFD ClusterResourceSet for cluster") + return + } + + resp.SetStatus(runtimehooksv1.ResponseStatusSuccess) +} + +// ensureNFDConfigmapForCluster is a private function that creates a configMap for the cluster. +func (n *DefaultNFD) ensureNFDConfigmapForCluster( + ctx context.Context, + cluster *capiv1.Cluster, +) (*corev1.ConfigMap, error) { + key := ctrlclient.ObjectKey{ + Namespace: n.config.defaultsNamespace, + Name: n.config.defaultNFDConfigMap, + } + cm := &corev1.ConfigMap{} + err := n.client.Get(ctx, key, cm) + if err != nil { + return nil, fmt.Errorf( + "failed to fetch the configmap specified by %v: %w", + n.config, + err, + ) + } + // Base configmap is there now we create one in the cluster namespace if needed. + cmForCluster := &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: cluster.Namespace, + Name: n.config.defaultNFDConfigMap, + }, + Data: cm.Data, + } + err = client.ServerSideApply(ctx, n.client, cmForCluster) + if err != nil { + return nil, fmt.Errorf("failed to apply NFD ConfigMap for cluster: %w", err) + } + return cmForCluster, nil +} + +func (n *DefaultNFD) ensureNFDCRSForCluster( + ctx context.Context, + cluster *capiv1.Cluster, + cm *corev1.ConfigMap, +) error { + crs := &crsv1.ClusterResourceSet{ + TypeMeta: metav1.TypeMeta{ + APIVersion: crsv1.GroupVersion.String(), + Kind: "ClusterResourceSet", + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: cluster.Namespace, + Name: cm.Name + "-" + cluster.Name, + }, + Spec: crsv1.ClusterResourceSetSpec{ + Resources: []crsv1.ResourceRef{{ + Kind: string(crsv1.ConfigMapClusterResourceSetResourceKind), + Name: cm.Name, + }}, + Strategy: string(crsv1.ClusterResourceSetStrategyReconcile), + ClusterSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{capiv1.ClusterNameLabel: cluster.Name}, + }, + }, + } + + if err := controllerutil.SetOwnerReference(cluster, crs, n.client.Scheme()); err != nil { + return fmt.Errorf("failed to set owner reference: %w", err) + } + + err := client.ServerSideApply(ctx, n.client, crs) + if err != nil { + return fmt.Errorf("failed to server side apply %w", err) + } + return nil +}