diff --git a/cmd/nfd-master/main.go b/cmd/nfd-master/main.go index b18223563c..40cafd7aeb 100644 --- a/cmd/nfd-master/main.go +++ b/cmd/nfd-master/main.go @@ -94,6 +94,8 @@ func initFlags(flagset *flag.FlagSet) *master.Args { flagset.Var(&args.LabelWhiteList, "label-whitelist", "Regular expression to filter label names to publish to the Kubernetes API server. "+ "NB: the label namespace is omitted i.e. the filter is only applied to the name part after '/'.") + flagset.BoolVar(&args.EnableNodeFeatureApi, "-enable-nodefeature-api", false, + "Enable the NodeFeature CRD API for receiving node features. This will automatically disable the gRPC communication.") flagset.BoolVar(&args.NoPublish, "no-publish", false, "Do not publish feature labels") flagset.BoolVar(&args.EnableTaints, "enable-taints", false, diff --git a/cmd/nfd-worker/main.go b/cmd/nfd-worker/main.go index 90892380f1..30e3290d17 100644 --- a/cmd/nfd-worker/main.go +++ b/cmd/nfd-worker/main.go @@ -101,6 +101,10 @@ func initFlags(flagset *flag.FlagSet) (*worker.Args, *worker.ConfigOverrideArgs) "Config file to use.") flagset.StringVar(&args.KeyFile, "key-file", "", "Private key matching -cert-file") + flagset.BoolVar(&args.EnableNodeFeatureApi, "enable-nodefeature-api", false, + "Enable the NodeFeature CRD API for communicating with nfd-master. This will automatically disable the gRPC communication.") + flagset.StringVar(&args.Kubeconfig, "kubeconfig", "", + "Kubeconfig to use") flagset.BoolVar(&args.Oneshot, "oneshot", false, "Do not publish feature labels") flagset.StringVar(&args.Options, "options", "", @@ -119,7 +123,7 @@ func initFlags(flagset *flag.FlagSet) (*worker.Args, *worker.ConfigOverrideArgs) LabelSources: &utils.StringSliceVal{}, } overrides.NoPublish = flagset.Bool("no-publish", false, - "Do not publish discovered features, disable connection to nfd-master.") + "Do not publish discovered features, disable connection to nfd-master and don't create NodeFeature object.") flagset.Var(overrides.FeatureSources, "feature-sources", "Comma separated list of feature sources. Special value 'all' enables all sources. "+ "Prefix the source name with '-' to disable it.") diff --git a/deployment/base/nfd-crds/kustomization.yaml b/deployment/base/nfd-crds/kustomization.yaml index 73fd8cc08d..b7f95447ee 100644 --- a/deployment/base/nfd-crds/kustomization.yaml +++ b/deployment/base/nfd-crds/kustomization.yaml @@ -2,4 +2,4 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: -- nodefeaturerule-crd.yaml +- nfd-api-crds.yaml diff --git a/deployment/base/nfd-crds/nodefeaturerule-crd.yaml b/deployment/base/nfd-crds/nfd-api-crds.yaml similarity index 74% rename from deployment/base/nfd-crds/nodefeaturerule-crd.yaml rename to deployment/base/nfd-crds/nfd-api-crds.yaml index 9a1fc53c83..ab10f15a9c 100644 --- a/deployment/base/nfd-crds/nodefeaturerule-crd.yaml +++ b/deployment/base/nfd-crds/nfd-api-crds.yaml @@ -1,6 +1,117 @@ --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.9.2 + creationTimestamp: null + 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 + 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 + 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 + type: object + required: + - attributes + - flags + - instances + type: object + labels: + additionalProperties: + type: string + description: Labels is the set of node labels that are requested to + be created. + type: object + required: + - features + 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.9.2 diff --git a/deployment/base/rbac/kustomization.yaml b/deployment/base/rbac/kustomization.yaml index fdadb5675e..6eb2d8a8c7 100644 --- a/deployment/base/rbac/kustomization.yaml +++ b/deployment/base/rbac/kustomization.yaml @@ -7,3 +7,6 @@ resources: - master-serviceaccount.yaml - master-clusterrole.yaml - master-clusterrolebinding.yaml +- worker-serviceaccount.yaml +- worker-role.yaml +- worker-rolebinding.yaml diff --git a/deployment/base/rbac/master-clusterrole.yaml b/deployment/base/rbac/master-clusterrole.yaml index ca4b5705aa..d464e546e2 100644 --- a/deployment/base/rbac/master-clusterrole.yaml +++ b/deployment/base/rbac/master-clusterrole.yaml @@ -15,6 +15,7 @@ rules: - apiGroups: - nfd.k8s-sigs.io resources: + - nodefeatures - nodefeaturerules verbs: - get diff --git a/deployment/base/rbac/worker-role.yaml b/deployment/base/rbac/worker-role.yaml new file mode 100644 index 0000000000..72f261e9e7 --- /dev/null +++ b/deployment/base/rbac/worker-role.yaml @@ -0,0 +1,13 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: nfd-worker +rules: +- apiGroups: + - nfd.k8s-sigs.io + resources: + - nodefeatures + verbs: + - create + - get + - update diff --git a/deployment/base/rbac/worker-rolebinding.yaml b/deployment/base/rbac/worker-rolebinding.yaml new file mode 100644 index 0000000000..707c75fbb6 --- /dev/null +++ b/deployment/base/rbac/worker-rolebinding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: nfd-worker +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: nfd-worker +subjects: +- kind: ServiceAccount + name: nfd-worker + namespace: default diff --git a/deployment/base/rbac/worker-serviceaccount.yaml b/deployment/base/rbac/worker-serviceaccount.yaml new file mode 100644 index 0000000000..442b06c5e2 --- /dev/null +++ b/deployment/base/rbac/worker-serviceaccount.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: nfd-worker diff --git a/deployment/base/worker-daemonset/worker-daemonset.yaml b/deployment/base/worker-daemonset/worker-daemonset.yaml index 8f02fd0e13..f93bf175f7 100644 --- a/deployment/base/worker-daemonset/worker-daemonset.yaml +++ b/deployment/base/worker-daemonset/worker-daemonset.yaml @@ -13,6 +13,7 @@ spec: labels: app: nfd-worker spec: + serviceAccount: nfd-worker dnsPolicy: ClusterFirstWithHostNet containers: - name: nfd-worker diff --git a/deployment/base/worker-job/worker-job.yaml b/deployment/base/worker-job/worker-job.yaml index dd2f671dc4..6cf2c9dbf2 100644 --- a/deployment/base/worker-job/worker-job.yaml +++ b/deployment/base/worker-job/worker-job.yaml @@ -12,6 +12,7 @@ spec: labels: app: nfd-worker spec: + serviceAccount: nfd-worker dnsPolicy: ClusterFirstWithHostNet restartPolicy: Never affinity: diff --git a/deployment/helm/node-feature-discovery/crds/nodefeaturerule-crd.yaml b/deployment/helm/node-feature-discovery/crds/nfd-api-crds.yaml similarity index 74% rename from deployment/helm/node-feature-discovery/crds/nodefeaturerule-crd.yaml rename to deployment/helm/node-feature-discovery/crds/nfd-api-crds.yaml index 9a1fc53c83..ab10f15a9c 100644 --- a/deployment/helm/node-feature-discovery/crds/nodefeaturerule-crd.yaml +++ b/deployment/helm/node-feature-discovery/crds/nfd-api-crds.yaml @@ -1,6 +1,117 @@ --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.9.2 + creationTimestamp: null + 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 + 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 + 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 + type: object + required: + - attributes + - flags + - instances + type: object + labels: + additionalProperties: + type: string + description: Labels is the set of node labels that are requested to + be created. + type: object + required: + - features + 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.9.2 diff --git a/deployment/helm/node-feature-discovery/templates/clusterrole.yaml b/deployment/helm/node-feature-discovery/templates/clusterrole.yaml index f1c5805a13..3dd6f6f3b6 100644 --- a/deployment/helm/node-feature-discovery/templates/clusterrole.yaml +++ b/deployment/helm/node-feature-discovery/templates/clusterrole.yaml @@ -27,6 +27,7 @@ rules: - apiGroups: - nfd.k8s-sigs.io resources: + - nodefeatures - nodefeaturerules verbs: - get diff --git a/deployment/helm/node-feature-discovery/templates/master.yaml b/deployment/helm/node-feature-discovery/templates/master.yaml index 498964f8ae..03e995934b 100644 --- a/deployment/helm/node-feature-discovery/templates/master.yaml +++ b/deployment/helm/node-feature-discovery/templates/master.yaml @@ -78,6 +78,9 @@ spec: {{- if .Values.master.instance | empty | not }} - "--instance={{ .Values.master.instance }}" {{- end }} + {{- if .Values.enableNodeFeatureApi }} + - "-enable-nodefeature-api" + {{- end }} {{- if .Values.master.extraLabelNs | empty | not }} - "--extra-label-ns={{- join "," .Values.master.extraLabelNs }}" {{- end }} diff --git a/deployment/helm/node-feature-discovery/templates/role.yaml b/deployment/helm/node-feature-discovery/templates/role.yaml new file mode 100644 index 0000000000..f63cb8ff4f --- /dev/null +++ b/deployment/helm/node-feature-discovery/templates/role.yaml @@ -0,0 +1,18 @@ +{{- if .Values.worker.rbac.create }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ include "node-feature-discovery.fullname" . }}-worker + labels: + {{- include "node-feature-discovery.labels" . | nindent 4 }} +rules: +- apiGroups: + - nfd.k8s-sigs.io + resources: + - nodefeatures + verbs: + - create + - get + - update +{{- end }} + diff --git a/deployment/helm/node-feature-discovery/templates/rolebinding.yaml b/deployment/helm/node-feature-discovery/templates/rolebinding.yaml new file mode 100644 index 0000000000..30a00381f0 --- /dev/null +++ b/deployment/helm/node-feature-discovery/templates/rolebinding.yaml @@ -0,0 +1,17 @@ +{{- if .Values.worker.rbac.create }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "node-feature-discovery.fullname" . }}-worker + labels: + {{- include "node-feature-discovery.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "node-feature-discovery.fullname" . }}-worker +subjects: +- kind: ServiceAccount + name: {{ include "node-feature-discovery.worker.serviceAccountName" . }} + namespace: {{ include "node-feature-discovery.namespace" . }} +{{- end }} + diff --git a/deployment/helm/node-feature-discovery/templates/worker.yaml b/deployment/helm/node-feature-discovery/templates/worker.yaml index f48aade328..e723cc5cbe 100644 --- a/deployment/helm/node-feature-discovery/templates/worker.yaml +++ b/deployment/helm/node-feature-discovery/templates/worker.yaml @@ -46,6 +46,9 @@ spec: - "nfd-worker" args: - "--server={{ include "node-feature-discovery.fullname" . }}-master:{{ .Values.master.service.port }}" + {{- if .Values.enableNodeFeatureApi }} + - "-enable-nodefeature-api" + {{- end }} {{- if .Values.tls.enable }} - "--ca-file=/etc/kubernetes/node-feature-discovery/certs/ca.crt" - "--key-file=/etc/kubernetes/node-feature-discovery/certs/tls.key" diff --git a/deployment/helm/node-feature-discovery/values.yaml b/deployment/helm/node-feature-discovery/values.yaml index 2b3a5942aa..85a737ce1a 100644 --- a/deployment/helm/node-feature-discovery/values.yaml +++ b/deployment/helm/node-feature-discovery/values.yaml @@ -10,8 +10,11 @@ nameOverride: "" fullnameOverride: "" namespaceOverride: "" +enableNodeFeatureApi: false + master: instance: + featureApi: extraLabelNs: [] resourceLabels: [] featureRulesController: null @@ -338,6 +341,9 @@ worker: # If not set and create is true, a name is generated using the fullname template name: + rbac: + create: true + # Allow users to mount the hostPath /usr/src, useful for RHCOS on s390x # Does not work on systems without /usr/src AND a read-only /usr, such as Talos mountUsrSrc: false diff --git a/docs/deployment/helm.md b/docs/deployment/helm.md index e2f0435cd3..6e032026e1 100644 --- a/docs/deployment/helm.md +++ b/docs/deployment/helm.md @@ -97,6 +97,7 @@ We have introduced the following Chart parameters. | `fullnameOverride` | string | | Override a default fully qualified app name | | `tls.enable` | bool | false | Specifies whether to use TLS for communications between components | | `tls.certManager` | bool | false | If enabled, requires [cert-manager](https://cert-manager.io/docs/) to be installed and will automatically create the required TLS certificates | +| `enableNodeFeatureApi` | bool | false | Enable the NodeFeature CRD API for communicating node features. This will automatically disable the gRPC communication. ### Master pod parameters @@ -134,6 +135,7 @@ We have introduced the following Chart parameters. | `worker.serviceAccount.create` | bool | true | Specifies whether a service account for nfd-worker should be created | `worker.serviceAccount.annotations` | dict | {} | Annotations to add to the service account for nfd-worker | `worker.serviceAccount.name` | string | | The name of the service account to use for nfd-worker. If not set and create is true, a name is generated using the fullname template (suffixed with `-worker`) +| `worker.rbac.create` | bool | true | Specifies whether to create [RBAC][rbac] configuration for nfd-worker | `worker.mountUsrSrc` | bool | false | Specifies whether to allow users to mount the hostpath /user/src. Does not work on systems without /usr/src AND a read-only /usr | | `worker.resources` | dict | {} | NFD worker pod [resources management](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/) | | `worker.nodeSelector` | dict | {} | NFD worker pod [node selector](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#nodeselector) | diff --git a/docs/reference/master-commandline-reference.md b/docs/reference/master-commandline-reference.md index 8542742d10..c7dae1d60d 100644 --- a/docs/reference/master-commandline-reference.md +++ b/docs/reference/master-commandline-reference.md @@ -99,18 +99,6 @@ Example: nfd-master -cert-file=/opt/nfd/master.crt -key-file=/opt/nfd/master.key -ca-file=/opt/nfd/ca.crt ``` -### -enable-taints - -The `-enable-taints` flag enables/disables node tainting feature of NFD. - -Default: *false* - -Example: - -```bash -nfd-master -enable-taints=true -``` - ### -key-file The `-key-file` is one of the three flags (together with `-ca-file` and @@ -149,6 +137,32 @@ nfd-master -verify-node-name -ca-file=/opt/nfd/ca.crt \ -cert-file=/opt/nfd/master.crt -key-file=/opt/nfd/master.key ``` +### -enable-nodefeature-api + +The `-enable-nodefeature-api` flag enables the NodeFeature CRD API for +receiving feature requests. This will also automatically disable the gRPC +interface. + +Default: false + +Example: + +```bash +nfd-master -enable-nodefeature-api +``` + +### -enable-taints + +The `-enable-taints` flag enables/disables node tainting feature of NFD. + +Default: *false* + +Example: + +```bash +nfd-master -enable-taints=true +``` + ### -no-publish The `-no-publish` flag disables updates to the Node objects in the Kubernetes diff --git a/docs/reference/worker-commandline-reference.md b/docs/reference/worker-commandline-reference.md index 886faf4c7e..cf6651b8f2 100644 --- a/docs/reference/worker-commandline-reference.md +++ b/docs/reference/worker-commandline-reference.md @@ -122,6 +122,22 @@ Example: nfd-worker -key-file=/opt/nfd/worker.key -cert-file=/opt/nfd/worker.crt -ca-file=/opt/nfd/ca.crt ``` +### -kubeconfig + +The `-kubeconfig` flag specifies the kubeconfig to use for connecting to the +Kubernetes API server. It is only needed for manipulating NodeFeature +objects, and thus the flag only takes effect when +[`-enable-nodefeature-api`](#-enable-nodefeature-api)) is specified. An empty +value (which is also the default) implies in-cluster kubeconfig. + +Default: *empty* + +Example: + +```bash +nfd-worker -kubeconfig ${HOME}/.kube/config +``` + ### -server-name-override The `-server-name-override` flag specifies the common name (CN) which to @@ -178,11 +194,33 @@ Example: nfd-worker -label-sources=kernel,system,local ``` +### -enable-nodefeature-api + +The `-enable-nodefeature-api` flag enables the experimental NodeFeature CRD API +for communicating with nfd-master. This will also automatically disable the +gRPC communication to nfd-master. When enabled, nfd-worker will create per-node +NodeFeature objects the contain all discovered node features and the set of +feature labels to be created. + +Default: false + +Example: + +```bash +nfd-worker -enable-nodefeature-api +``` + ### -no-publish -The `-no-publish` flag disables all communication with the nfd-master, making -it a "dry-run" flag for nfd-worker. NFD-Worker runs feature detection normally, -but no labeling requests are sent to nfd-master. +The `-no-publish` flag disables all communication with the nfd-master and the +Kubernetes API server. It is effectively a "dry-run" flag for nfd-worker. +NFD-Worker runs feature detection normally, but no labeling requests are sent +to nfd-master and no NodeFeature objects are created or updated in the API +server. + +Note: This flag takes precedence over the +[`core.noPublish`](worker-configuration-reference#corenopublish) +configuration file option. Default: *false* diff --git a/docs/reference/worker-configuration-reference.md b/docs/reference/worker-configuration-reference.md index 5dd239e811..48b13307cb 100644 --- a/docs/reference/worker-configuration-reference.md +++ b/docs/reference/worker-configuration-reference.md @@ -131,10 +131,14 @@ core: ### core.noPublish Setting `core.noPublish` to `true` disables all communication with the -nfd-master. It is effectively a "dry-run" flag: nfd-worker runs feature -detection normally, but no labeling requests are sent to nfd-master. - -Note: Overridden by the `-no-publish` command line flag (if specified). +nfd-master and the Kubernetes API server. It is effectively a "dry-run" option. +NFD-Worker runs feature detection normally, but no labeling requests are sent +to nfd-master and no NodeFeature objects are created or updated in the API +server. + +Note: Overridden by the +[`-no-publish`](worker-commandline-reference#-no-publish) command line flag (if +specified). Default: `false` diff --git a/hack/generate.sh b/hack/generate.sh index 9b3b5ca222..0d5c165d61 100755 --- a/hack/generate.sh +++ b/hack/generate.sh @@ -10,10 +10,10 @@ go generate ./cmd/... ./pkg/... ./source/... rm -rf vendor/ -controller-gen object crd output:crd:stdout paths=./pkg/apis/... > deployment/base/nfd-crds/nodefeaturerule-crd.yaml +controller-gen object crd output:crd:stdout paths=./pkg/apis/... > deployment/base/nfd-crds/nfd-api-crds.yaml mkdir -p deployment/helm/node-feature-discovery/crds -cp deployment/base/nfd-crds/nodefeaturerule-crd.yaml deployment/helm/node-feature-discovery/crds/ +cp deployment/base/nfd-crds/nfd-api-crds.yaml deployment/helm/node-feature-discovery/crds rm -rf sigs.k8s.io diff --git a/pkg/apis/nfd/v1alpha1/annotations_labels.go b/pkg/apis/nfd/v1alpha1/annotations_labels.go index 7e4cbee250..978ba853c4 100644 --- a/pkg/apis/nfd/v1alpha1/annotations_labels.go +++ b/pkg/apis/nfd/v1alpha1/annotations_labels.go @@ -46,4 +46,10 @@ const ( // NodeTaintsAnnotation is the annotation that holds the taints that nfd-master set on the node NodeTaintsAnnotation = AnnotationNs + "/taints" + + // NodeFeatureObjNodeNameLabel is the label that specifies which node the + // NodeFeature object is targeting. Creators of NodeFeature objects must + // set this label and consumers of the objects are supposed to use the + // label for filtering features designated for a certain node. + NodeFeatureObjNodeNameLabel = "nfd.node.kubernetes.io/node-name" ) diff --git a/pkg/apis/nfd/v1alpha1/register.go b/pkg/apis/nfd/v1alpha1/register.go index c87a4852a4..8c1d41f5b6 100644 --- a/pkg/apis/nfd/v1alpha1/register.go +++ b/pkg/apis/nfd/v1alpha1/register.go @@ -40,6 +40,7 @@ func Resource(resource string) schema.GroupResource { func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, + &NodeFeature{}, &NodeFeatureRule{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) diff --git a/pkg/apis/nfd/v1alpha1/types.go b/pkg/apis/nfd/v1alpha1/types.go index c057cddf7e..3565db1cd0 100644 --- a/pkg/apis/nfd/v1alpha1/types.go +++ b/pkg/apis/nfd/v1alpha1/types.go @@ -21,6 +21,37 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +// NodeFeatureList contains a list of NodeFeature objects. +// +kubebuilder:object:root=true +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type NodeFeatureList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata"` + + Items []NodeFeature `json:"items"` +} + +// NodeFeature resource holds the features discovered for one node in the +// cluster. +// +kubebuilder:object:root=true +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +genclient +type NodeFeature struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec NodeFeatureSpec `json:"spec"` +} + +// NodeFeatureSpec describes a NodeFeature object. +type NodeFeatureSpec struct { + // Features is the full "raw" features data that has been discovered. + Features Features `json:"features"` + // Labels is the set of node labels that are requested to be created. + // +optional + Labels map[string]string `json:"labels"` +} + // Features is the collection of all discovered features. // // +protobuf=true diff --git a/pkg/apis/nfd/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/nfd/v1alpha1/zz_generated.deepcopy.go index 47513f1335..07073afac1 100644 --- a/pkg/apis/nfd/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/nfd/v1alpha1/zz_generated.deepcopy.go @@ -342,6 +342,64 @@ func (in *Nil) DeepCopy() *Nil { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NodeFeature) DeepCopyInto(out *NodeFeature) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodeFeature. +func (in *NodeFeature) DeepCopy() *NodeFeature { + if in == nil { + return nil + } + out := new(NodeFeature) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *NodeFeature) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NodeFeatureList) DeepCopyInto(out *NodeFeatureList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]NodeFeature, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodeFeatureList. +func (in *NodeFeatureList) DeepCopy() *NodeFeatureList { + if in == nil { + return nil + } + out := new(NodeFeatureList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *NodeFeatureList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NodeFeatureRule) DeepCopyInto(out *NodeFeatureRule) { *out = *in @@ -422,6 +480,29 @@ func (in *NodeFeatureRuleSpec) DeepCopy() *NodeFeatureRuleSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NodeFeatureSpec) DeepCopyInto(out *NodeFeatureSpec) { + *out = *in + in.Features.DeepCopyInto(&out.Features) + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodeFeatureSpec. +func (in *NodeFeatureSpec) DeepCopy() *NodeFeatureSpec { + if in == nil { + return nil + } + out := new(NodeFeatureSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Rule) DeepCopyInto(out *Rule) { *out = *in diff --git a/pkg/generated/clientset/versioned/typed/nfd/v1alpha1/fake/fake_nfd_client.go b/pkg/generated/clientset/versioned/typed/nfd/v1alpha1/fake/fake_nfd_client.go index 7133268330..4022cd3691 100644 --- a/pkg/generated/clientset/versioned/typed/nfd/v1alpha1/fake/fake_nfd_client.go +++ b/pkg/generated/clientset/versioned/typed/nfd/v1alpha1/fake/fake_nfd_client.go @@ -28,6 +28,10 @@ type FakeNfdV1alpha1 struct { *testing.Fake } +func (c *FakeNfdV1alpha1) NodeFeatures(namespace string) v1alpha1.NodeFeatureInterface { + return &FakeNodeFeatures{c, namespace} +} + func (c *FakeNfdV1alpha1) NodeFeatureRules() v1alpha1.NodeFeatureRuleInterface { return &FakeNodeFeatureRules{c} } diff --git a/pkg/generated/clientset/versioned/typed/nfd/v1alpha1/fake/fake_nodefeature.go b/pkg/generated/clientset/versioned/typed/nfd/v1alpha1/fake/fake_nodefeature.go new file mode 100644 index 0000000000..1986bf6181 --- /dev/null +++ b/pkg/generated/clientset/versioned/typed/nfd/v1alpha1/fake/fake_nodefeature.go @@ -0,0 +1,130 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + schema "k8s.io/apimachinery/pkg/runtime/schema" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" + v1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1" +) + +// FakeNodeFeatures implements NodeFeatureInterface +type FakeNodeFeatures struct { + Fake *FakeNfdV1alpha1 + ns string +} + +var nodefeaturesResource = schema.GroupVersionResource{Group: "nfd.k8s-sigs.io", Version: "v1alpha1", Resource: "nodefeatures"} + +var nodefeaturesKind = schema.GroupVersionKind{Group: "nfd.k8s-sigs.io", Version: "v1alpha1", Kind: "NodeFeature"} + +// Get takes name of the nodeFeature, and returns the corresponding nodeFeature object, and an error if there is any. +func (c *FakeNodeFeatures) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.NodeFeature, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(nodefeaturesResource, c.ns, name), &v1alpha1.NodeFeature{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.NodeFeature), err +} + +// List takes label and field selectors, and returns the list of NodeFeatures that match those selectors. +func (c *FakeNodeFeatures) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.NodeFeatureList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(nodefeaturesResource, nodefeaturesKind, c.ns, opts), &v1alpha1.NodeFeatureList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.NodeFeatureList{ListMeta: obj.(*v1alpha1.NodeFeatureList).ListMeta} + for _, item := range obj.(*v1alpha1.NodeFeatureList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested nodeFeatures. +func (c *FakeNodeFeatures) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(nodefeaturesResource, c.ns, opts)) + +} + +// Create takes the representation of a nodeFeature and creates it. Returns the server's representation of the nodeFeature, and an error, if there is any. +func (c *FakeNodeFeatures) Create(ctx context.Context, nodeFeature *v1alpha1.NodeFeature, opts v1.CreateOptions) (result *v1alpha1.NodeFeature, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(nodefeaturesResource, c.ns, nodeFeature), &v1alpha1.NodeFeature{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.NodeFeature), err +} + +// Update takes the representation of a nodeFeature and updates it. Returns the server's representation of the nodeFeature, and an error, if there is any. +func (c *FakeNodeFeatures) Update(ctx context.Context, nodeFeature *v1alpha1.NodeFeature, opts v1.UpdateOptions) (result *v1alpha1.NodeFeature, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(nodefeaturesResource, c.ns, nodeFeature), &v1alpha1.NodeFeature{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.NodeFeature), err +} + +// Delete takes name of the nodeFeature and deletes it. Returns an error if one occurs. +func (c *FakeNodeFeatures) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteActionWithOptions(nodefeaturesResource, c.ns, name, opts), &v1alpha1.NodeFeature{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeNodeFeatures) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(nodefeaturesResource, c.ns, listOpts) + + _, err := c.Fake.Invokes(action, &v1alpha1.NodeFeatureList{}) + return err +} + +// Patch applies the patch and returns the patched nodeFeature. +func (c *FakeNodeFeatures) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.NodeFeature, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(nodefeaturesResource, c.ns, name, pt, data, subresources...), &v1alpha1.NodeFeature{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.NodeFeature), err +} diff --git a/pkg/generated/clientset/versioned/typed/nfd/v1alpha1/generated_expansion.go b/pkg/generated/clientset/versioned/typed/nfd/v1alpha1/generated_expansion.go index 8807cd64dd..cf392dd100 100644 --- a/pkg/generated/clientset/versioned/typed/nfd/v1alpha1/generated_expansion.go +++ b/pkg/generated/clientset/versioned/typed/nfd/v1alpha1/generated_expansion.go @@ -18,4 +18,6 @@ limitations under the License. package v1alpha1 +type NodeFeatureExpansion interface{} + type NodeFeatureRuleExpansion interface{} diff --git a/pkg/generated/clientset/versioned/typed/nfd/v1alpha1/nfd_client.go b/pkg/generated/clientset/versioned/typed/nfd/v1alpha1/nfd_client.go index 034d5cbe74..8ecaf1998d 100644 --- a/pkg/generated/clientset/versioned/typed/nfd/v1alpha1/nfd_client.go +++ b/pkg/generated/clientset/versioned/typed/nfd/v1alpha1/nfd_client.go @@ -28,6 +28,7 @@ import ( type NfdV1alpha1Interface interface { RESTClient() rest.Interface + NodeFeaturesGetter NodeFeatureRulesGetter } @@ -36,6 +37,10 @@ type NfdV1alpha1Client struct { restClient rest.Interface } +func (c *NfdV1alpha1Client) NodeFeatures(namespace string) NodeFeatureInterface { + return newNodeFeatures(c, namespace) +} + func (c *NfdV1alpha1Client) NodeFeatureRules() NodeFeatureRuleInterface { return newNodeFeatureRules(c) } diff --git a/pkg/generated/clientset/versioned/typed/nfd/v1alpha1/nodefeature.go b/pkg/generated/clientset/versioned/typed/nfd/v1alpha1/nodefeature.go new file mode 100644 index 0000000000..bf6d9b1759 --- /dev/null +++ b/pkg/generated/clientset/versioned/typed/nfd/v1alpha1/nodefeature.go @@ -0,0 +1,178 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + "time" + + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" + v1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1" + scheme "sigs.k8s.io/node-feature-discovery/pkg/generated/clientset/versioned/scheme" +) + +// NodeFeaturesGetter has a method to return a NodeFeatureInterface. +// A group's client should implement this interface. +type NodeFeaturesGetter interface { + NodeFeatures(namespace string) NodeFeatureInterface +} + +// NodeFeatureInterface has methods to work with NodeFeature resources. +type NodeFeatureInterface interface { + Create(ctx context.Context, nodeFeature *v1alpha1.NodeFeature, opts v1.CreateOptions) (*v1alpha1.NodeFeature, error) + Update(ctx context.Context, nodeFeature *v1alpha1.NodeFeature, opts v1.UpdateOptions) (*v1alpha1.NodeFeature, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha1.NodeFeature, error) + List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.NodeFeatureList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.NodeFeature, err error) + NodeFeatureExpansion +} + +// nodeFeatures implements NodeFeatureInterface +type nodeFeatures struct { + client rest.Interface + ns string +} + +// newNodeFeatures returns a NodeFeatures +func newNodeFeatures(c *NfdV1alpha1Client, namespace string) *nodeFeatures { + return &nodeFeatures{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the nodeFeature, and returns the corresponding nodeFeature object, and an error if there is any. +func (c *nodeFeatures) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.NodeFeature, err error) { + result = &v1alpha1.NodeFeature{} + err = c.client.Get(). + Namespace(c.ns). + Resource("nodefeatures"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(ctx). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of NodeFeatures that match those selectors. +func (c *nodeFeatures) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.NodeFeatureList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1alpha1.NodeFeatureList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("nodefeatures"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(ctx). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested nodeFeatures. +func (c *nodeFeatures) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + opts.Watch = true + return c.client.Get(). + Namespace(c.ns). + Resource("nodefeatures"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch(ctx) +} + +// Create takes the representation of a nodeFeature and creates it. Returns the server's representation of the nodeFeature, and an error, if there is any. +func (c *nodeFeatures) Create(ctx context.Context, nodeFeature *v1alpha1.NodeFeature, opts v1.CreateOptions) (result *v1alpha1.NodeFeature, err error) { + result = &v1alpha1.NodeFeature{} + err = c.client.Post(). + Namespace(c.ns). + Resource("nodefeatures"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(nodeFeature). + Do(ctx). + Into(result) + return +} + +// Update takes the representation of a nodeFeature and updates it. Returns the server's representation of the nodeFeature, and an error, if there is any. +func (c *nodeFeatures) Update(ctx context.Context, nodeFeature *v1alpha1.NodeFeature, opts v1.UpdateOptions) (result *v1alpha1.NodeFeature, err error) { + result = &v1alpha1.NodeFeature{} + err = c.client.Put(). + Namespace(c.ns). + Resource("nodefeatures"). + Name(nodeFeature.Name). + VersionedParams(&opts, scheme.ParameterCodec). + Body(nodeFeature). + Do(ctx). + Into(result) + return +} + +// Delete takes name of the nodeFeature and deletes it. Returns an error if one occurs. +func (c *nodeFeatures) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("nodefeatures"). + Name(name). + Body(&opts). + Do(ctx). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *nodeFeatures) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + var timeout time.Duration + if listOpts.TimeoutSeconds != nil { + timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second + } + return c.client.Delete(). + Namespace(c.ns). + Resource("nodefeatures"). + VersionedParams(&listOpts, scheme.ParameterCodec). + Timeout(timeout). + Body(&opts). + Do(ctx). + Error() +} + +// Patch applies the patch and returns the patched nodeFeature. +func (c *nodeFeatures) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.NodeFeature, err error) { + result = &v1alpha1.NodeFeature{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("nodefeatures"). + Name(name). + SubResource(subresources...). + VersionedParams(&opts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} diff --git a/pkg/generated/informers/externalversions/generic.go b/pkg/generated/informers/externalversions/generic.go index 453de7f151..5c2b9eda3b 100644 --- a/pkg/generated/informers/externalversions/generic.go +++ b/pkg/generated/informers/externalversions/generic.go @@ -53,6 +53,8 @@ func (f *genericInformer) Lister() cache.GenericLister { func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) { switch resource { // Group=nfd.k8s-sigs.io, Version=v1alpha1 + case v1alpha1.SchemeGroupVersion.WithResource("nodefeatures"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Nfd().V1alpha1().NodeFeatures().Informer()}, nil case v1alpha1.SchemeGroupVersion.WithResource("nodefeaturerules"): return &genericInformer{resource: resource.GroupResource(), informer: f.Nfd().V1alpha1().NodeFeatureRules().Informer()}, nil diff --git a/pkg/generated/informers/externalversions/nfd/v1alpha1/interface.go b/pkg/generated/informers/externalversions/nfd/v1alpha1/interface.go index 5f476d8873..1cae0cfc24 100644 --- a/pkg/generated/informers/externalversions/nfd/v1alpha1/interface.go +++ b/pkg/generated/informers/externalversions/nfd/v1alpha1/interface.go @@ -24,6 +24,8 @@ import ( // Interface provides access to all the informers in this group version. type Interface interface { + // NodeFeatures returns a NodeFeatureInformer. + NodeFeatures() NodeFeatureInformer // NodeFeatureRules returns a NodeFeatureRuleInformer. NodeFeatureRules() NodeFeatureRuleInformer } @@ -39,6 +41,11 @@ func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakList return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} } +// NodeFeatures returns a NodeFeatureInformer. +func (v *version) NodeFeatures() NodeFeatureInformer { + return &nodeFeatureInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + // NodeFeatureRules returns a NodeFeatureRuleInformer. func (v *version) NodeFeatureRules() NodeFeatureRuleInformer { return &nodeFeatureRuleInformer{factory: v.factory, tweakListOptions: v.tweakListOptions} diff --git a/pkg/generated/informers/externalversions/nfd/v1alpha1/nodefeature.go b/pkg/generated/informers/externalversions/nfd/v1alpha1/nodefeature.go new file mode 100644 index 0000000000..20ea65f561 --- /dev/null +++ b/pkg/generated/informers/externalversions/nfd/v1alpha1/nodefeature.go @@ -0,0 +1,90 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + time "time" + + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" + nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1" + versioned "sigs.k8s.io/node-feature-discovery/pkg/generated/clientset/versioned" + internalinterfaces "sigs.k8s.io/node-feature-discovery/pkg/generated/informers/externalversions/internalinterfaces" + v1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/generated/listers/nfd/v1alpha1" +) + +// NodeFeatureInformer provides access to a shared informer and lister for +// NodeFeatures. +type NodeFeatureInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.NodeFeatureLister +} + +type nodeFeatureInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewNodeFeatureInformer constructs a new informer for NodeFeature type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewNodeFeatureInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredNodeFeatureInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredNodeFeatureInformer constructs a new informer for NodeFeature type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredNodeFeatureInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.NfdV1alpha1().NodeFeatures(namespace).List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.NfdV1alpha1().NodeFeatures(namespace).Watch(context.TODO(), options) + }, + }, + &nfdv1alpha1.NodeFeature{}, + resyncPeriod, + indexers, + ) +} + +func (f *nodeFeatureInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredNodeFeatureInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *nodeFeatureInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&nfdv1alpha1.NodeFeature{}, f.defaultInformer) +} + +func (f *nodeFeatureInformer) Lister() v1alpha1.NodeFeatureLister { + return v1alpha1.NewNodeFeatureLister(f.Informer().GetIndexer()) +} diff --git a/pkg/generated/listers/nfd/v1alpha1/expansion_generated.go b/pkg/generated/listers/nfd/v1alpha1/expansion_generated.go index a072a4f4ae..5e6fc01e4d 100644 --- a/pkg/generated/listers/nfd/v1alpha1/expansion_generated.go +++ b/pkg/generated/listers/nfd/v1alpha1/expansion_generated.go @@ -18,6 +18,14 @@ limitations under the License. package v1alpha1 +// NodeFeatureListerExpansion allows custom methods to be added to +// NodeFeatureLister. +type NodeFeatureListerExpansion interface{} + +// NodeFeatureNamespaceListerExpansion allows custom methods to be added to +// NodeFeatureNamespaceLister. +type NodeFeatureNamespaceListerExpansion interface{} + // NodeFeatureRuleListerExpansion allows custom methods to be added to // NodeFeatureRuleLister. type NodeFeatureRuleListerExpansion interface{} diff --git a/pkg/generated/listers/nfd/v1alpha1/nodefeature.go b/pkg/generated/listers/nfd/v1alpha1/nodefeature.go new file mode 100644 index 0000000000..011dffd559 --- /dev/null +++ b/pkg/generated/listers/nfd/v1alpha1/nodefeature.go @@ -0,0 +1,99 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" + v1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1" +) + +// NodeFeatureLister helps list NodeFeatures. +// All objects returned here must be treated as read-only. +type NodeFeatureLister interface { + // List lists all NodeFeatures in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.NodeFeature, err error) + // NodeFeatures returns an object that can list and get NodeFeatures. + NodeFeatures(namespace string) NodeFeatureNamespaceLister + NodeFeatureListerExpansion +} + +// nodeFeatureLister implements the NodeFeatureLister interface. +type nodeFeatureLister struct { + indexer cache.Indexer +} + +// NewNodeFeatureLister returns a new NodeFeatureLister. +func NewNodeFeatureLister(indexer cache.Indexer) NodeFeatureLister { + return &nodeFeatureLister{indexer: indexer} +} + +// List lists all NodeFeatures in the indexer. +func (s *nodeFeatureLister) List(selector labels.Selector) (ret []*v1alpha1.NodeFeature, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.NodeFeature)) + }) + return ret, err +} + +// NodeFeatures returns an object that can list and get NodeFeatures. +func (s *nodeFeatureLister) NodeFeatures(namespace string) NodeFeatureNamespaceLister { + return nodeFeatureNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// NodeFeatureNamespaceLister helps list and get NodeFeatures. +// All objects returned here must be treated as read-only. +type NodeFeatureNamespaceLister interface { + // List lists all NodeFeatures in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.NodeFeature, err error) + // Get retrieves the NodeFeature from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*v1alpha1.NodeFeature, error) + NodeFeatureNamespaceListerExpansion +} + +// nodeFeatureNamespaceLister implements the NodeFeatureNamespaceLister +// interface. +type nodeFeatureNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all NodeFeatures in the indexer for a given namespace. +func (s nodeFeatureNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.NodeFeature, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.NodeFeature)) + }) + return ret, err +} + +// Get retrieves the NodeFeature from the indexer for a given namespace and name. +func (s nodeFeatureNamespaceLister) Get(name string) (*v1alpha1.NodeFeature, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha1.Resource("nodefeature"), name) + } + return obj.(*v1alpha1.NodeFeature), nil +} diff --git a/pkg/nfd-client/base.go b/pkg/nfd-client/base.go index fa5e0b01a2..c965535421 100644 --- a/pkg/nfd-client/base.go +++ b/pkg/nfd-client/base.go @@ -49,6 +49,7 @@ type Args struct { CaFile string CertFile string KeyFile string + Kubeconfig string Server string ServerNameOverride string diff --git a/pkg/nfd-client/worker/nfd-worker.go b/pkg/nfd-client/worker/nfd-worker.go index a7fdeb1f6e..abc0fdb4a8 100644 --- a/pkg/nfd-client/worker/nfd-worker.go +++ b/pkg/nfd-client/worker/nfd-worker.go @@ -27,10 +27,16 @@ import ( "time" "golang.org/x/net/context" + "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/util/validation" "k8s.io/klog/v2" "sigs.k8s.io/yaml" + apiequality "k8s.io/apimachinery/pkg/api/equality" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/node-feature-discovery/pkg/apihelper" + nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1" + nfdclient "sigs.k8s.io/node-feature-discovery/pkg/generated/clientset/versioned" pb "sigs.k8s.io/node-feature-discovery/pkg/labeler" clientcommon "sigs.k8s.io/node-feature-discovery/pkg/nfd-client" "sigs.k8s.io/node-feature-discovery/pkg/utils" @@ -76,9 +82,10 @@ type Labels map[string]string type Args struct { clientcommon.Args - ConfigFile string - Oneshot bool - Options string + ConfigFile string + EnableNodeFeatureApi bool + Oneshot bool + Options string Klog map[string]*utils.KlogFlagVal Overrides ConfigOverrideArgs @@ -101,6 +108,7 @@ type nfdWorker struct { config *NFDConfig kubernetesNamespace string grpcClient pb.LabelerClient + nfdClient *nfdclient.Clientset stop chan struct{} // channel for signaling stop featureSources []source.FeatureSource labelSources []source.LabelSource @@ -150,6 +158,7 @@ func newDefaultConfig() *NFDConfig { func (w *nfdWorker) Run() error { klog.Infof("Node Feature Discovery Worker %s", version.Get()) klog.Infof("NodeName: '%s'", clientcommon.NodeName()) + klog.Infof("Kubernetes namespace: '%s'", w.kubernetesNamespace) // Create watcher for config file and read initial configuration configWatch, err := utils.CreateFsWatcher(time.Second, w.configFilePath) @@ -185,9 +194,8 @@ func (w *nfdWorker) Run() error { // Update the node with the feature labels. if !w.config.Core.NoPublish { - err := w.advertiseFeatureLabels(labels) - if err != nil { - return fmt.Errorf("failed to advertise labels: %s", err.Error()) + if err := w.advertiseFeatures(labels); err != nil { + return err } } @@ -205,7 +213,7 @@ func (w *nfdWorker) Run() error { return err } // Manage connection to master - if w.config.Core.NoPublish { + if w.config.Core.NoPublish || !w.args.EnableNodeFeatureApi { w.GrpcDisconnect() } @@ -524,6 +532,22 @@ func getFeatureLabels(source source.LabelSource, labelWhiteList regexp.Regexp) ( return labels, nil } +// advertiseFeatures advertises the features of a Kubernetes node +func (w *nfdWorker) advertiseFeatures(labels Labels) error { + if w.args.EnableNodeFeatureApi { + // Create/update NodeFeature CR object + if err := w.updateNodeFeatureObject(labels); err != nil { + return fmt.Errorf("failed to advertise features (via CRD API): %w", err) + } + } else { + // Create/update feature labels through gRPC connection to nfd-master + if err := w.advertiseFeatureLabels(labels); err != nil { + return fmt.Errorf("failed to advertise features (via gRPC): %w", err) + } + } + return nil +} + // advertiseFeatureLabels advertises the feature labels to a Kubernetes node // via the NFD server. func (w *nfdWorker) advertiseFeatureLabels(labels Labels) error { @@ -551,6 +575,85 @@ func (w *nfdWorker) advertiseFeatureLabels(labels Labels) error { return nil } +// updateNodeFeatureObject creates/updates the node-specific NodeFeature custom resource. +func (m *nfdWorker) updateNodeFeatureObject(labels Labels) error { + cli, err := m.getNfdClient() + if err != nil { + return err + } + nodename := clientcommon.NodeName() + namespace := m.kubernetesNamespace + + features := source.GetAllFeatures() + + // TODO: we could implement some simple caching of the object, only get it + // every 10 minutes or so because nobody else should really be modifying it + if nfr, err := cli.NfdV1alpha1().NodeFeatures(namespace).Get(context.TODO(), nodename, metav1.GetOptions{}); errors.IsNotFound(err) { + klog.Infof("creating NodeFeature object %q", nodename) + nfr = &nfdv1alpha1.NodeFeature{ + ObjectMeta: metav1.ObjectMeta{ + Name: nodename, + Annotations: map[string]string{nfdv1alpha1.WorkerVersionAnnotation: version.Get()}, + Labels: map[string]string{nfdv1alpha1.NodeFeatureObjNodeNameLabel: nodename}, + }, + Spec: nfdv1alpha1.NodeFeatureSpec{ + Features: *features, + Labels: labels, + }, + } + + nfrCreated, err := cli.NfdV1alpha1().NodeFeatures(namespace).Create(context.TODO(), nfr, metav1.CreateOptions{}) + if err != nil { + return fmt.Errorf("failed to create NodeFeature object %q: %w", nfr.Name, err) + } + + utils.KlogDump(4, "NodeFeature object created:", " ", nfrCreated) + } else if err != nil { + return fmt.Errorf("failed to get NodeFeature object: %w", err) + } else { + + nfrUpdated := nfr.DeepCopy() + nfrUpdated.Annotations = map[string]string{nfdv1alpha1.WorkerVersionAnnotation: version.Get()} + nfrUpdated.Labels = map[string]string{nfdv1alpha1.NodeFeatureObjNodeNameLabel: nodename} + nfrUpdated.Spec = nfdv1alpha1.NodeFeatureSpec{ + Features: *features, + Labels: labels, + } + + if !apiequality.Semantic.DeepEqual(nfr, nfrUpdated) { + klog.Infof("updating NodeFeature object %q", nodename) + nfrUpdated, err = cli.NfdV1alpha1().NodeFeatures(namespace).Update(context.TODO(), nfrUpdated, metav1.UpdateOptions{}) + if err != nil { + return fmt.Errorf("failed to update NodeFeature object %q: %w", nfr.Name, err) + } + utils.KlogDump(4, "NodeFeature object updated:", " ", nfrUpdated) + } else { + klog.V(1).Info("no changes in NodeFeature object, not updating") + } + } + return nil +} + +// getNfdClient returns the clientset for using the nfd CRD api +func (m *nfdWorker) getNfdClient() (*nfdclient.Clientset, error) { + if m.nfdClient != nil { + return m.nfdClient, nil + } + + kubeconfig, err := apihelper.GetKubeconfig(m.args.Kubeconfig) + if err != nil { + return nil, err + } + + c, err := nfdclient.NewForConfig(kubeconfig) + if err != nil { + return nil, err + } + + m.nfdClient = c + return c, nil +} + // UnmarshalJSON implements the Unmarshaler interface from "encoding/json" func (d *duration) UnmarshalJSON(data []byte) error { var v interface{} diff --git a/pkg/nfd-master/nfd-api-controller.go b/pkg/nfd-master/nfd-api-controller.go index 4a2acc46a8..84fd7d5240 100644 --- a/pkg/nfd-master/nfd-api-controller.go +++ b/pkg/nfd-master/nfd-api-controller.go @@ -32,42 +32,88 @@ import ( ) type nfdController struct { - ruleLister nfdlisters.NodeFeatureRuleLister + featureLister nfdlisters.NodeFeatureLister + ruleLister nfdlisters.NodeFeatureRuleLister stopChan chan struct{} + + updateAllNodesChan chan struct{} + updateOneNodeChan chan string } -func newNfdController(config *restclient.Config) (*nfdController, error) { +func newNfdController(config *restclient.Config, disableNodeFeature bool) (*nfdController, error) { c := &nfdController{ - stopChan: make(chan struct{}, 1), + stopChan: make(chan struct{}, 1), + updateAllNodesChan: make(chan struct{}, 1), + updateOneNodeChan: make(chan string), } nfdClient := nfdclientset.NewForConfigOrDie(config) informerFactory := nfdinformers.NewSharedInformerFactory(nfdClient, 5*time.Minute) + + // Add informer for NodeFeature objects + if !disableNodeFeature { + featureInformer := informerFactory.Nfd().V1alpha1().NodeFeatures() + if _, err := featureInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + key, _ := cache.MetaNamespaceKeyFunc(obj) + klog.V(2).Infof("NodeFeature %v added", key) + c.updateOneNode(obj) + }, + UpdateFunc: func(oldObj, newObj interface{}) { + key, _ := cache.MetaNamespaceKeyFunc(newObj) + klog.V(2).Infof("NodeFeature %v updated", key) + c.updateOneNode(newObj) + }, + DeleteFunc: func(obj interface{}) { + key, _ := cache.MetaNamespaceKeyFunc(obj) + klog.V(2).Infof("NodeFeature %v deleted", key) + c.updateOneNode(obj) + }, + }); err != nil { + return nil, err + } + c.featureLister = featureInformer.Lister() + } + + // Add informer for NodeFeatureRule objects ruleInformer := informerFactory.Nfd().V1alpha1().NodeFeatureRules() if _, err := ruleInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: func(object interface{}) { key, _ := cache.MetaNamespaceKeyFunc(object) klog.V(2).Infof("NodeFeatureRule %v added", key) + if !disableNodeFeature { + c.updateAllNodes() + } + // else: rules will be processed only when gRPC requests are received }, UpdateFunc: func(oldObject, newObject interface{}) { key, _ := cache.MetaNamespaceKeyFunc(newObject) klog.V(2).Infof("NodeFeatureRule %v updated", key) + if !disableNodeFeature { + c.updateAllNodes() + } + // else: rules will be processed only when gRPC requests are received }, DeleteFunc: func(object interface{}) { key, _ := cache.MetaNamespaceKeyFunc(object) klog.V(2).Infof("NodeFeatureRule %v deleted", key) + if !disableNodeFeature { + c.updateAllNodes() + } + // else: rules will be processed only when gRPC requests are received }, }); err != nil { return nil, err } + c.ruleLister = ruleInformer.Lister() + + // Start informers informerFactory.Start(c.stopChan) utilruntime.Must(nfdv1alpha1.AddToScheme(nfdscheme.Scheme)) - c.ruleLister = ruleInformer.Lister() - return c, nil } @@ -77,3 +123,32 @@ func (c *nfdController) stop() { default: } } + +func (c *nfdController) updateOneNode(obj interface{}) { + o, ok := obj.(*nfdv1alpha1.NodeFeature) + if !ok { + klog.Errorf("not a NodeFeature object (but of type %T): %v", obj, obj) + return + } + + nodeName, ok := o.Labels[nfdv1alpha1.NodeFeatureObjNodeNameLabel] + if !ok { + klog.Errorf("no node name for NodeFeature object %s/%s: %q label is missing", + o.Namespace, o.Name, nfdv1alpha1.NodeFeatureObjNodeNameLabel) + return + } + if nodeName == "" { + klog.Errorf("no node name for NodeFeature object %s/%s: %q label is empty", + o.Namespace, o.Name, nfdv1alpha1.NodeFeatureObjNodeNameLabel) + return + } + + c.updateOneNodeChan <- nodeName +} + +func (c *nfdController) updateAllNodes() { + select { + case c.updateAllNodesChan <- struct{}{}: + default: + } +} diff --git a/pkg/nfd-master/nfd-master-internal_test.go b/pkg/nfd-master/nfd-master-internal_test.go index fc522daece..cf50e49064 100644 --- a/pkg/nfd-master/nfd-master-internal_test.go +++ b/pkg/nfd-master/nfd-master-internal_test.go @@ -61,7 +61,7 @@ func newMockMaster(apihelper apihelper.APIHelpers) *nfdMaster { } } -func TestUpdateNodeFeatures(t *testing.T) { +func TestUpdateNodeObject(t *testing.T) { Convey("When I update the node using fake client", t, func() { fakeFeatureLabels := map[string]string{ nfdv1alpha1.FeatureLabelNs + "/source-feature.1": "1", @@ -112,10 +112,10 @@ func TestUpdateNodeFeatures(t *testing.T) { } mockAPIHelper.On("GetClient").Return(mockClient, nil) - mockAPIHelper.On("GetNode", mockClient, mockNodeName).Return(mockNode, nil).Once() + mockAPIHelper.On("GetNode", mockClient, mockNodeName).Return(mockNode, nil).Twice() mockAPIHelper.On("PatchNode", mockClient, mockNodeName, mock.MatchedBy(jsonPatchMatcher(metadataPatches))).Return(nil) mockAPIHelper.On("PatchNodeStatus", mockClient, mockNodeName, mock.MatchedBy(jsonPatchMatcher(statusPatches))).Return(nil) - err := mockMaster.updateNodeFeatures(mockClient, mockNodeName, fakeFeatureLabels, fakeAnnotations, fakeExtResources) + err := mockMaster.updateNodeObject(mockClient, mockNodeName, fakeFeatureLabels, fakeAnnotations, fakeExtResources, nil) Convey("Error is nil", func() { So(err, ShouldBeNil) @@ -125,7 +125,7 @@ func TestUpdateNodeFeatures(t *testing.T) { Convey("When I fail to update the node with feature labels", func() { expectedError := fmt.Errorf("no client is passed, client: ") mockAPIHelper.On("GetClient").Return(nil, expectedError) - err := mockMaster.updateNodeFeatures(nil, mockNodeName, fakeFeatureLabels, fakeAnnotations, fakeExtResources) + err := mockMaster.updateNodeObject(nil, mockNodeName, fakeFeatureLabels, fakeAnnotations, fakeExtResources, nil) Convey("Error is produced", func() { So(err, ShouldResemble, expectedError) @@ -135,7 +135,7 @@ func TestUpdateNodeFeatures(t *testing.T) { Convey("When I fail to get a mock client while updating feature labels", func() { expectedError := fmt.Errorf("no client is passed, client: ") mockAPIHelper.On("GetClient").Return(nil, expectedError) - err := mockMaster.updateNodeFeatures(nil, mockNodeName, fakeFeatureLabels, fakeAnnotations, fakeExtResources) + err := mockMaster.updateNodeObject(nil, mockNodeName, fakeFeatureLabels, fakeAnnotations, fakeExtResources, nil) Convey("Error is produced", func() { So(err, ShouldResemble, expectedError) @@ -145,8 +145,8 @@ func TestUpdateNodeFeatures(t *testing.T) { Convey("When I fail to get a mock node while updating feature labels", func() { expectedError := errors.New("fake error") mockAPIHelper.On("GetClient").Return(mockClient, nil) - mockAPIHelper.On("GetNode", mockClient, mockNodeName).Return(nil, expectedError).Once() - err := mockMaster.updateNodeFeatures(mockClient, mockNodeName, fakeFeatureLabels, fakeAnnotations, fakeExtResources) + mockAPIHelper.On("GetNode", mockClient, mockNodeName).Return(nil, expectedError).Twice() + err := mockMaster.updateNodeObject(mockClient, mockNodeName, fakeFeatureLabels, fakeAnnotations, fakeExtResources, nil) Convey("Error is produced", func() { So(err, ShouldEqual, expectedError) @@ -156,9 +156,9 @@ func TestUpdateNodeFeatures(t *testing.T) { Convey("When I fail to update a mock node while updating feature labels", func() { expectedError := errors.New("fake error") mockAPIHelper.On("GetClient").Return(mockClient, nil) - mockAPIHelper.On("GetNode", mockClient, mockNodeName).Return(mockNode, nil).Once() - mockAPIHelper.On("PatchNode", mockClient, mockNodeName, mock.Anything).Return(expectedError).Once() - err := mockMaster.updateNodeFeatures(mockClient, mockNodeName, fakeFeatureLabels, fakeAnnotations, fakeExtResources) + mockAPIHelper.On("GetNode", mockClient, mockNodeName).Return(mockNode, nil).Twice() + mockAPIHelper.On("PatchNode", mockClient, mockNodeName, mock.Anything).Return(expectedError).Twice() + err := mockMaster.updateNodeObject(mockClient, mockNodeName, fakeFeatureLabels, fakeAnnotations, fakeExtResources, nil) Convey("Error is produced", func() { So(err.Error(), ShouldEndWith, expectedError.Error()) @@ -294,7 +294,7 @@ func TestRemovingExtResources(t *testing.T) { func TestSetLabels(t *testing.T) { Convey("When servicing SetLabels request", t, func() { - const workerName = "mock-worker" + const workerName = mockNodeName const workerVer = "0.1-test" mockHelper := &apihelper.MockAPIHelpers{} mockMaster := newMockMaster(mockHelper) @@ -324,7 +324,7 @@ func TestSetLabels(t *testing.T) { } mockHelper.On("GetClient").Return(mockClient, nil) - mockHelper.On("GetNode", mockClient, workerName).Return(mockNode, nil) + mockHelper.On("GetNode", mockClient, workerName).Return(mockNode, nil).Twice() mockHelper.On("PatchNode", mockClient, mockNodeName, mock.MatchedBy(jsonPatchMatcher(expectedPatches))).Return(nil) mockHelper.On("PatchNodeStatus", mockClient, mockNodeName, mock.MatchedBy(jsonPatchMatcher(expectedStatusPatches))).Return(nil) _, err := mockMaster.SetLabels(mockCtx, mockReq) diff --git a/pkg/nfd-master/nfd-master.go b/pkg/nfd-master/nfd-master.go index db4f9f38e2..afc9719e39 100644 --- a/pkg/nfd-master/nfd-master.go +++ b/pkg/nfd-master/nfd-master.go @@ -36,6 +36,7 @@ import ( "google.golang.org/grpc/health/grpc_health_v1" "google.golang.org/grpc/peer" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/labels" label "k8s.io/apimachinery/pkg/labels" "k8s.io/client-go/kubernetes" restclient "k8s.io/client-go/rest" @@ -69,6 +70,7 @@ type Args struct { Kubeconfig string LabelWhiteList utils.RegexpVal FeatureRulesController bool + EnableNodeFeatureApi bool NoPublish bool EnableTaints bool Port int @@ -87,6 +89,7 @@ type nfdMaster struct { *nfdController args Args + namespace string nodeName string server *grpc.Server stop chan struct{} @@ -98,9 +101,10 @@ type nfdMaster struct { // NewNfdMaster creates a new NfdMaster server instance. func NewNfdMaster(args *Args) (NfdMaster, error) { nfd := &nfdMaster{args: *args, - nodeName: os.Getenv("NODE_NAME"), - ready: make(chan bool, 1), - stop: make(chan struct{}, 1), + nodeName: os.Getenv("NODE_NAME"), + namespace: utils.GetKubernetesNamespace(), + ready: make(chan bool, 1), + stop: make(chan struct{}, 1), } if args.Instance != "" { @@ -144,6 +148,7 @@ func (m *nfdMaster) Run() error { klog.Infof("Master instance: %q", m.args.Instance) } klog.Infof("NodeName: %q", m.nodeName) + klog.Infof("Kubernetes namespace: %q", m.namespace) if m.args.Prune { return m.prune() @@ -155,7 +160,7 @@ func (m *nfdMaster) Run() error { return err } klog.Info("starting nfd api controller") - m.nfdController, err = newNfdController(kubeconfig) + m.nfdController, err = newNfdController(kubeconfig, !m.args.EnableNodeFeatureApi) if err != nil { return fmt.Errorf("failed to initialize CRD controller: %w", err) } @@ -170,7 +175,14 @@ func (m *nfdMaster) Run() error { // Run gRPC server grpcErr := make(chan error, 1) - go m.runGrpcServer(grpcErr) + if !m.args.EnableNodeFeatureApi { + go m.runGrpcServer(grpcErr) + } + + // Run updater that handles events from the nfd CRD API. + if m.nfdController != nil { + go m.nfdAPIUpdateHandler() + } // Notify that we're ready to accept connections m.ready <- true @@ -245,6 +257,22 @@ func (m *nfdMaster) runGrpcServer(errChan chan<- error) { } } +// nfdAPIUpdateHandler handles events from the nfd API controller. +func (m *nfdMaster) nfdAPIUpdateHandler() { + for { + select { + case <-m.nfdController.updateAllNodesChan: + if err := m.nfdAPIUpdateAllNodes(); err != nil { + klog.Error(err) + } + case nodeName := <-m.nfdController.updateOneNodeChan: + if err := m.nfdAPIUpdateOneNode(nodeName); err != nil { + klog.Error(err) + } + } + } +} + // Stop NfdMaster func (m *nfdMaster) Stop() { m.server.GracefulStop() @@ -290,16 +318,9 @@ func (m *nfdMaster) prune() error { klog.Infof("pruning node %q...", node.Name) // Prune labels and extended resources - err := m.updateNodeFeatures(cli, node.Name, Labels{}, Annotations{}, ExtendedResources{}) + err := m.updateNodeObject(cli, node.Name, Labels{}, Annotations{}, ExtendedResources{}, []corev1.Taint{}) if err != nil { - return fmt.Errorf("failed to prune labels from node %q: %v", node.Name, err) - } - - // Prune taints - err = m.setTaints(cli, []corev1.Taint{}, node.Name) - - if err != nil { - return fmt.Errorf("failed to prune taints from node %q: %v", node.Name, err) + return fmt.Errorf("failed to prune node %q: %v", node.Name, err) } // Prune annotations @@ -421,20 +442,6 @@ func (m *nfdMaster) SetLabels(c context.Context, r *pb.SetLabelsRequest) (*pb.Se klog.Infof("received labeling request for node %q", r.NodeName) } - // Mix in CR-originated labels - rawLabels := make(map[string]string) - if r.Labels != nil { - // NOTE: we effectively mangle the request struct by not creating a deep copy of the map - rawLabels = r.Labels - } - crLabels, crTaints := m.processNodeFeatureRule(r) - - for k, v := range crLabels { - rawLabels[k] = v - } - - labels, extendedResources := filterFeatureLabels(rawLabels, m.args.ExtraLabelNs, m.args.LabelWhiteList.Regexp, m.args.ResourceLabels) - if !m.args.NoPublish { cli, err := m.apihelper.GetClient() if err != nil { @@ -444,27 +451,124 @@ func (m *nfdMaster) SetLabels(c context.Context, r *pb.SetLabelsRequest) (*pb.Se // Advertise NFD worker version as an annotation annotations := Annotations{m.instanceAnnotation(nfdv1alpha1.WorkerVersionAnnotation): r.NfdVersion} - err = m.updateNodeFeatures(cli, r.NodeName, labels, annotations, extendedResources) - if err != nil { - klog.Errorf("failed to advertise labels: %v", err) + // Create labels et al + if err := m.refreshNodeFeatures(cli, r.NodeName, annotations, r.GetLabels(), r.GetFeatures()); err != nil { return &pb.SetLabelsReply{}, err } + } + return &pb.SetLabelsReply{}, nil +} + +func (m *nfdMaster) nfdAPIUpdateAllNodes() error { + klog.Info("will process all nodes in the cluster") + + cli, err := m.apihelper.GetClient() + if err != nil { + return err + } - // set taints - var taints []corev1.Taint - if m.args.EnableTaints { - taints = crTaints + nodes, err := m.apihelper.GetNodes(cli) + if err != nil { + return err + } + + for _, node := range nodes.Items { + if err := m.nfdAPIUpdateOneNode(node.Name); err != nil { + return err } + } - // Call setTaints even though the feature flag is disabled. This - // ensures that we delete NFD owned stale taints when flag got - // turned off. - err = m.setTaints(cli, taints, r.NodeName) - if err != nil { - return &pb.SetLabelsReply{}, err + return nil +} + +func (m *nfdMaster) nfdAPIUpdateOneNode(nodeName string) error { + sel := labels.SelectorFromSet(labels.Set{nfdv1alpha1.NodeFeatureObjNodeNameLabel: nodeName}) + objs, err := m.nfdController.featureLister.List(sel) + if len(objs) == 0 { + klog.Infof("no NodeFeature object exists for node %q, skipping...", nodeName) + return nil + } else if err != nil { + return fmt.Errorf("failed to get NodeFeature resources for node %q: %w", nodeName, err) + } + + // Sort our objects + sort.Slice(objs, func(i, j int) bool { + // Objects in our nfd namespace gets into the beginning of the list + if objs[i].Namespace == m.namespace && objs[j].Namespace != m.namespace { + return true } + if objs[i].Namespace != m.namespace && objs[j].Namespace == m.namespace { + return false + } + // After the nfd namespace, sort objects by their name + if objs[i].Name != objs[j].Name { + return objs[i].Name < objs[j].Name + } + // Objects with the same name are sorted by their namespace + return objs[i].Namespace < objs[j].Namespace + }) + + if m.args.NoPublish { + return nil } - return &pb.SetLabelsReply{}, nil + + klog.V(1).Infof("processing node %q, initiated by NodeFeature API", nodeName) + + // Merge in features + // + // TODO: support multiple NodeFeature objects. There are two obvious options to implement this: + // 1. Merge features of all objects into one joint object + // 2. Change the rule api to support handle multiple objects + // Of these #2 would probably perform better with lot less data to copy. We + // could probably even get rid of the DeepCopy in this scenario. + features := objs[0].DeepCopy() + + annotations := Annotations{} + if objs[0].Namespace == m.namespace && objs[0].Name == nodeName { + // This is the one created by nfd-worker + if v := objs[0].Annotations[nfdv1alpha1.WorkerVersionAnnotation]; v != "" { + annotations[nfdv1alpha1.WorkerVersionAnnotation] = v + } + } + + // Create labels et al + cli, err := m.apihelper.GetClient() + if err != nil { + return err + } + if err := m.refreshNodeFeatures(cli, nodeName, annotations, features.Spec.Labels, &features.Spec.Features); err != nil { + return err + } + + return nil +} + +func (m *nfdMaster) refreshNodeFeatures(cli *kubernetes.Clientset, nodeName string, annotations, labels map[string]string, features *nfdv1alpha1.Features) error { + if labels == nil { + labels = make(map[string]string) + } + + crLabels, crTaints := m.processNodeFeatureRule(features) + + // Mix in CR-originated labels + for k, v := range crLabels { + labels[k] = v + } + + labels, extendedResources := filterFeatureLabels(labels, m.args.ExtraLabelNs, m.args.LabelWhiteList.Regexp, m.args.ResourceLabels) + + var taints []corev1.Taint + if m.args.EnableTaints { + taints = crTaints + } + + err := m.updateNodeObject(cli, nodeName, labels, annotations, extendedResources, taints) + if err != nil { + klog.Errorf("failed to update node %q: %v", nodeName, err) + return err + } + + return nil } // setTaints sets node taints and annotations based on the taints passed via @@ -572,7 +676,7 @@ func authorizeClient(c context.Context, checkNodeName bool, nodeName string) err return nil } -func (m *nfdMaster) processNodeFeatureRule(r *pb.SetLabelsRequest) (map[string]string, []corev1.Taint) { +func (m *nfdMaster) processNodeFeatureRule(features *nfdv1alpha1.Features) (map[string]string, []corev1.Taint) { if m.nfdController == nil { return nil, nil } @@ -589,9 +693,6 @@ func (m *nfdMaster) processNodeFeatureRule(r *pb.SetLabelsRequest) (map[string]s return nil, nil } - // Helper struct for rule processing - features := r.GetFeatures() - // Process all rule CRs for _, spec := range ruleSpecs { switch { @@ -621,10 +722,10 @@ func (m *nfdMaster) processNodeFeatureRule(r *pb.SetLabelsRequest) (map[string]s return labels, taints } -// updateNodeFeatures ensures the Kubernetes node object is up to date, +// updateNodeObject ensures the Kubernetes node object is up to date, // creating new labels and extended resources where necessary and removing // outdated ones. Also updates the corresponding annotations. -func (m *nfdMaster) updateNodeFeatures(cli *kubernetes.Clientset, nodeName string, labels Labels, annotations Annotations, extendedResources ExtendedResources) error { +func (m *nfdMaster) updateNodeObject(cli *kubernetes.Clientset, nodeName string, labels Labels, annotations Annotations, extendedResources ExtendedResources, taints []corev1.Taint) error { if cli == nil { return fmt.Errorf("no client is passed, client: %v", cli) } @@ -677,6 +778,12 @@ func (m *nfdMaster) updateNodeFeatures(cli *kubernetes.Clientset, nodeName strin klog.V(1).Infof("no updates to node %q", nodeName) } + // Set taints + err = m.setTaints(cli, taints, node.Name) + if err != nil { + return err + } + return err }