From 0210796946b42076fcf57644ecf409747d55d687 Mon Sep 17 00:00:00 2001 From: wmousa Date: Sat, 9 Sep 2023 21:57:18 +0300 Subject: [PATCH] feat: Add support for NO Admission Controller This change implements validating admission controller for supported CRDs NicClusterPolicy, HostDeviceNetwork, MacvlanNetwork, and IPoIBNetwork Currently, this commit covers: - Using operator-sdk to scaffold the webhook for the CRDs - Implementing validation rules for NicClusterPolicy. - IBKubernetes.PKeyGUIDPoolRangeStart - IBKubernetes.PKeyGUIDPoolRangeEnd - OFEDDriver.Version - RdmaSharedDevicePlugin.Config - Configuration is a valid JSON and check it schema - Resource name is valid for k8s - At least one of supported selectors exists - All selectors are strings - SriovNetworkDevicePlugin.Config - Configuration is a valid JSON and check it schema - Resource name is valid for k8s - At least one of supported selectors exists - All selectors are strings - Implemented validation rules for HostDeviceNetwork. - ResourceName - Adding admission_controller.yaml to deploy using helm templates - Providing helm values to manage webhook deployment Signed-off-by: wmousa --- Dockerfile | 1 + PROJECT | 6 + api/v1alpha1/hostdevicenetwork_webhook.go | 91 +++ .../hostdevicenetwork_webhook_test.go | 46 ++ api/v1alpha1/nicclusterpolicy_webhook.go | 364 ++++++++++++ api/v1alpha1/nicclusterpolicy_webhook_test.go | 527 ++++++++++++++++++ api/v1alpha1/suite_test.go | 6 + api/v1alpha1/zz_generated.deepcopy.go | 2 +- config/certmanager/certificate.yaml | 39 ++ config/certmanager/kustomization.yaml | 5 + config/certmanager/kustomizeconfig.yaml | 16 + config/crd/kustomization.yaml | 10 +- config/default/manager_webhook_patch.yaml | 26 + config/default/webhookcainjection_patch.yaml | 15 + config/manager/manager.yaml | 3 + config/webhook/kustomization.yaml | 6 + config/webhook/kustomizeconfig.yaml | 18 + config/webhook/manifests.yaml | 47 ++ config/webhook/service.yaml | 13 + deploy/operator.yaml | 2 + deployment/network-operator/README.md | 38 ++ .../templates/admission_controller.yaml | 129 +++++ ...anox.com_v1alpha1_nicclusterpolicy_cr.yaml | 2 + .../network-operator/templates/operator.yaml | 26 + deployment/network-operator/values.yaml | 14 + go.mod | 3 + go.sum | 6 + hack/templates/values/values.template | 14 + main.go | 20 + webhook-schemas/accelerator_selector.json | 124 +++++ webhook-schemas/aux_net_device.json | 212 +++++++ webhook-schemas/net_device.json | 268 +++++++++ .../rdma_shared_device_plugin.json | 117 ++++ .../sriov_network_device_plugin.json | 49 ++ 34 files changed, 2258 insertions(+), 7 deletions(-) create mode 100644 api/v1alpha1/hostdevicenetwork_webhook.go create mode 100644 api/v1alpha1/hostdevicenetwork_webhook_test.go create mode 100644 api/v1alpha1/nicclusterpolicy_webhook.go create mode 100644 api/v1alpha1/nicclusterpolicy_webhook_test.go create mode 100644 config/certmanager/certificate.yaml create mode 100644 config/certmanager/kustomization.yaml create mode 100644 config/certmanager/kustomizeconfig.yaml create mode 100644 config/default/manager_webhook_patch.yaml create mode 100644 config/default/webhookcainjection_patch.yaml create mode 100644 config/webhook/kustomization.yaml create mode 100644 config/webhook/kustomizeconfig.yaml create mode 100644 config/webhook/manifests.yaml create mode 100644 config/webhook/service.yaml create mode 100644 deployment/network-operator/templates/admission_controller.yaml create mode 100644 webhook-schemas/accelerator_selector.json create mode 100644 webhook-schemas/aux_net_device.json create mode 100644 webhook-schemas/net_device.json create mode 100644 webhook-schemas/rdma_shared_device_plugin.json create mode 100644 webhook-schemas/sriov_network_device_plugin.json diff --git a/Dockerfile b/Dockerfile index 06e1a3d0..0a7a32ac 100644 --- a/Dockerfile +++ b/Dockerfile @@ -74,6 +74,7 @@ COPY --from=builder /workspace/manager . COPY --from=builder /workspace/kubectl /usr/local/bin COPY --from=builder /workspace/crds /crds +COPY /webhook-schemas /webhook-schemas COPY manifests/ manifests/ USER 65532:65532 diff --git a/PROJECT b/PROJECT index b309020a..cadef16f 100644 --- a/PROJECT +++ b/PROJECT @@ -23,6 +23,9 @@ resources: kind: NicClusterPolicy path: github.com/Mellanox/network-operator/api/v1alpha1 version: v1alpha1 + webhooks: + validation: true + webhookVersion: v1 - api: crdVersion: v1 controller: true @@ -31,4 +34,7 @@ resources: kind: HostDeviceNetwork path: github.com/Mellanox/network-operator/api/v1alpha1 version: v1alpha1 + webhooks: + validation: true + webhookVersion: v1 version: "3" diff --git a/api/v1alpha1/hostdevicenetwork_webhook.go b/api/v1alpha1/hostdevicenetwork_webhook.go new file mode 100644 index 00000000..a773ebd3 --- /dev/null +++ b/api/v1alpha1/hostdevicenetwork_webhook.go @@ -0,0 +1,91 @@ +/* +2023 NVIDIA CORPORATION & AFFILIATES + +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. +*/ + +package v1alpha1 + +import ( + "regexp" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/validation/field" + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" +) + +// log is for logging in this package. +var hostDeviceNetworkLog = logf.Log.WithName("hostdevicenetwork-resource") + +func (w *HostDeviceNetwork) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(w). + Complete() +} + +//nolint:lll +//+kubebuilder:webhook:path=/validate-mellanox-com-v1alpha1-hostdevicenetwork,mutating=false,failurePolicy=fail,sideEffects=None,groups=mellanox.com,resources=hostdevicenetworks,verbs=create;update,versions=v1alpha1,name=vhostdevicenetwork.kb.io,admissionReviewVersions=v1 + +var _ webhook.Validator = &HostDeviceNetwork{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (w *HostDeviceNetwork) ValidateCreate() error { + hostDeviceNetworkLog.Info("validate create", "name", w.Name) + + return w.validateHostDeviceNetwork() +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (w *HostDeviceNetwork) ValidateUpdate(_ runtime.Object) error { + hostDeviceNetworkLog.Info("validate update", "name", w.Name) + + return w.validateHostDeviceNetwork() +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (w *HostDeviceNetwork) ValidateDelete() error { + hostDeviceNetworkLog.Info("validate delete", "name", w.Name) + + // Validation for delete call is not required + return nil +} + +/* +We are validating here HostDeviceNetwork: + - ResourceName must be valid for k8s +*/ + +func (w *HostDeviceNetwork) validateHostDeviceNetwork() error { + resourceName := w.Spec.ResourceName + if !isValidHostDeviceNetworkResourceName(resourceName) { + var allErrs field.ErrorList + allErrs = append(allErrs, field.Invalid(field.NewPath("Spec"), resourceName, + "Invalid Resource name, it must consist of alphanumeric characters, '-', '_' or '.', "+ + "and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', "+ + "regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')")) + return apierrors.NewInvalid( + schema.GroupKind{Group: "mellanox.com", Kind: "HostDeviceNetwork"}, + w.Name, allErrs) + } + return nil +} + +func isValidHostDeviceNetworkResourceName(resourceName string) bool { + resourceNamePattern := `^([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]$` + resourceNameRegex := regexp.MustCompile(resourceNamePattern) + return resourceNameRegex.MatchString(resourceName) +} diff --git a/api/v1alpha1/hostdevicenetwork_webhook_test.go b/api/v1alpha1/hostdevicenetwork_webhook_test.go new file mode 100644 index 00000000..e18a241f --- /dev/null +++ b/api/v1alpha1/hostdevicenetwork_webhook_test.go @@ -0,0 +1,46 @@ +/* +2023 NVIDIA CORPORATION & AFFILIATES + +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. +*/ +package v1alpha1 //nolint:dupl + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +//nolint:dupl +var _ = Describe("Validate", func() { + Context("HostDeviceNetwork tests", func() { + It("Valid ResourceName", func() { + hostDeviceNetwork := HostDeviceNetwork{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: HostDeviceNetworkSpec{ + ResourceName: "hostdev", + }, + } + Expect(hostDeviceNetwork.ValidateCreate()).NotTo(HaveOccurred()) + }) + It("Invalid ResourceName", func() { + hostDeviceNetwork := HostDeviceNetwork{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: HostDeviceNetworkSpec{ + ResourceName: "hostdev!!", + }, + } + Expect(hostDeviceNetwork.ValidateCreate().Error()).To(ContainSubstring("Invalid Resource name")) + }) + }) +}) diff --git a/api/v1alpha1/nicclusterpolicy_webhook.go b/api/v1alpha1/nicclusterpolicy_webhook.go new file mode 100644 index 00000000..74c7db55 --- /dev/null +++ b/api/v1alpha1/nicclusterpolicy_webhook.go @@ -0,0 +1,364 @@ +/* +2023 NVIDIA CORPORATION & AFFILIATES + +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. +*/ + +package v1alpha1 + +import ( + "encoding/json" + "fmt" + "math/big" + "os" + "regexp" + "strings" + + "github.com/xeipuuv/gojsonschema" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/validation/field" + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" +) + +// log is for logging in this package. +var nicClusterPolicyLog = logf.Log.WithName("nicclusterpolicy-resource") + +var schemaValidators *schemaValidator + +func (w *NicClusterPolicy) SetupWebhookWithManager(mgr ctrl.Manager) error { + nicClusterPolicyLog.Info("Nic cluster policy webhook admission controller") + InitSchemaValidator("./webhook-schemas") + return ctrl.NewWebhookManagedBy(mgr). + For(w). + Complete() +} + +//nolint:lll +//+kubebuilder:webhook:path=/validate-mellanox-com-v1alpha1-nicclusterpolicy,mutating=false,failurePolicy=fail,sideEffects=None,groups=mellanox.com,resources=nicclusterpolicies,verbs=create;update,versions=v1alpha1,name=vnicclusterpolicy.kb.io,admissionReviewVersions=v1 + +var _ webhook.Validator = &NicClusterPolicy{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (w *NicClusterPolicy) ValidateCreate() error { + nicClusterPolicyLog.Info("validate create", "name", w.Name) + return w.validateNicClusterPolicy() +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (w *NicClusterPolicy) ValidateUpdate(_ runtime.Object) error { + nicClusterPolicyLog.Info("validate update", "name", w.Name) + return w.validateNicClusterPolicy() +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (w *NicClusterPolicy) ValidateDelete() error { + nicClusterPolicyLog.Info("validate delete", "name", w.Name) + + // Validation for delete call is not required + return nil +} + +/* +We are validating here NicClusterPolicy: + 1. IBKubernetes.pKeyGUIDPoolRangeStart and IBKubernetes.pKeyGUIDPoolRangeEnd must be valid GUID and valid range. + 2. OFEDDriver.version must be a valid ofed version. + 3. RdmaSharedDevicePlugin.Config. + 3.1. Configuration is a valid JSON and check its schema. + 3.2. resourceName is valid for k8s. + 3.3. At least one of the supported selectors exists. + 3.4. All selectors are strings. + 4. SriovNetworkDevicePlugin.Config. + 4.1. Configuration is a valid JSON and check its schema. + 4.2. resourceName is valid for k8s. + 4.3. At least one of the supported selectors exists. + 4.4. All selectors are strings. +*/ +func (w *NicClusterPolicy) validateNicClusterPolicy() error { + var allErrs field.ErrorList + // Validate IBKubernetes + ibKubernetes := w.Spec.IBKubernetes + if ibKubernetes != nil { + allErrs = append(allErrs, ibKubernetes.validate(field.NewPath("spec").Child("ibKubernetes"))...) + } + + // Validate OFEDDriverSpec + ofedDriver := w.Spec.OFEDDriver + if ofedDriver != nil { + allErrs = append(allErrs, ofedDriver.validateVersion(field.NewPath("spec").Child("ofedDriver"))...) + } + // Validate RdmaSharedDevicePlugin + rdmaSharedDevicePlugin := w.Spec.RdmaSharedDevicePlugin + if rdmaSharedDevicePlugin != nil { + allErrs = append(allErrs, w.Spec.RdmaSharedDevicePlugin.validateRdmaSharedDevicePlugin( + field.NewPath("spec").Child("rdmaSharedDevicePlugin"))...) + } + // Validate SriovDevicePlugin + sriovNetworkDevicePlugin := w.Spec.SriovDevicePlugin + if sriovNetworkDevicePlugin != nil { + allErrs = append(allErrs, w.Spec.SriovDevicePlugin.validateSriovNetworkDevicePlugin( + field.NewPath("spec").Child("sriovNetworkDevicePlugin"))...) + } + + if len(allErrs) == 0 { + return nil + } + return apierrors.NewInvalid( + schema.GroupKind{Group: "mellanox.com", Kind: "NicClusterPolicy"}, + w.Name, allErrs) +} +func (dp *DevicePluginSpec) validateSriovNetworkDevicePlugin(fldPath *field.Path) field.ErrorList { + var allErrs field.ErrorList + var sriovNetworkDevicePluginConfigJSON map[string]interface{} + sriovNetworkDevicePluginConfig := *dp.Config + + // Validate if the SRIOV Network Device Plugin Config is a valid json + if err := json.Unmarshal([]byte(sriovNetworkDevicePluginConfig), &sriovNetworkDevicePluginConfigJSON); err != nil { + allErrs = append(allErrs, field.Invalid(fldPath.Child("Config"), dp.Config, + "Invalid json of SriovNetworkDevicePluginConfig")) + return allErrs + } + + // Load the JSON Schema + sriovNetworkDevicePluginSchema, err := schemaValidators.GetSchema("sriov_network_device_plugin") + if err != nil { + allErrs = append(allErrs, field.Invalid(fldPath.Child("Config"), dp.Config, + "Invalid json schema "+err.Error())) + return allErrs + } + acceleratorJSONSchema, err := schemaValidators.GetSchema("accelerator_selector") + if err != nil { + allErrs = append(allErrs, field.Invalid(fldPath.Child("Config"), dp.Config, + "Invalid json schema "+err.Error())) + return allErrs + } + netDeviceJSONSchema, err := schemaValidators.GetSchema("net_device") + if err != nil { + allErrs = append(allErrs, field.Invalid(fldPath.Child("Config"), dp.Config, + "Invalid json schema "+err.Error())) + return allErrs + } + auxNetDeviceJSONSchema, err := schemaValidators.GetSchema("aux_net_device") + if err != nil { + allErrs = append(allErrs, field.Invalid(fldPath.Child("Config"), dp.Config, + "Invalid json schema "+err.Error())) + return allErrs + } + + // Load the Sriov Network Device Plugin JSON Loader + sriovNetworkDevicePluginConfigJSONLoader := gojsonschema.NewStringLoader(sriovNetworkDevicePluginConfig) + + // Perform schema validation + result, err := sriovNetworkDevicePluginSchema.Validate(sriovNetworkDevicePluginConfigJSONLoader) + if err != nil { + allErrs = append(allErrs, field.Invalid(fldPath.Child("Config"), dp.Config, + "Invalid json configuration of SriovNetworkDevicePluginConfig"+err.Error())) + return allErrs + } else if !result.Valid() { + for _, ResultErr := range result.Errors() { + allErrs = append(allErrs, field.Invalid(fldPath.Child("Config"), dp.Config, ResultErr.Description())) + } + return allErrs + } + if resourceListInterface := sriovNetworkDevicePluginConfigJSON["resourceList"]; resourceListInterface != nil { + resourceList, _ := resourceListInterface.([]interface{}) + for _, resourceInterface := range resourceList { + resource := resourceInterface.(map[string]interface{}) + resourceJSONString, _ := json.Marshal(resource) + resourceJSONLoader := gojsonschema.NewStringLoader(string(resourceJSONString)) + var selectorResult *gojsonschema.Result + var selectorErr error + resourceName := resource["resourceName"].(string) + if !isValidSriovNetworkDevicePluginResourceName(resourceName) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("Config"), dp.Config, + "Invalid Resource name, it must consist of alphanumeric characters, '_' or '.', "+ + "and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', "+ + "or '123_abc', regex used for validation is '([A-Za-z0-9][A-Za-z0-9_.]*)?[A-Za-z0-9]')")) + return allErrs + } + deviceType := resource["deviceType"] + switch deviceType { + case "accelerator": + selectorResult, selectorErr = acceleratorJSONSchema.Validate(resourceJSONLoader) + case "auxNetDevice": + selectorResult, selectorErr = auxNetDeviceJSONSchema.Validate(resourceJSONLoader) + default: + selectorResult, selectorErr = netDeviceJSONSchema.Validate(resourceJSONLoader) + } + if selectorErr != nil { + allErrs = append(allErrs, field.Invalid(fldPath.Child("Config"), dp.Config, + selectorErr.Error())) + } else if !selectorResult.Valid() { + for _, selectorResultErr := range selectorResult.Errors() { + allErrs = append(allErrs, field.Invalid(fldPath.Child("Config"), dp.Config, + selectorResultErr.Description())) + } + } + } + } + return allErrs +} + +func (dp *DevicePluginSpec) validateRdmaSharedDevicePlugin(fldPath *field.Path) field.ErrorList { + var allErrs field.ErrorList + var rdmaSharedDevicePluginConfigJSON map[string]interface{} + rdmaSharedDevicePluginConfig := *dp.Config + + // Validate if the RDMA Shared Device Plugin Config is a valid json + if err := json.Unmarshal([]byte(rdmaSharedDevicePluginConfig), &rdmaSharedDevicePluginConfigJSON); err != nil { + allErrs = append(allErrs, field.Invalid(fldPath.Child("Config"), + dp.Config, "Invalid json of RdmaSharedDevicePluginConfig"+err.Error())) + return allErrs + } + + // Perform schema validation + rdmaSharedDevicePluginSchema, err := schemaValidators.GetSchema("rdma_shared_device_plugin") + if err != nil { + allErrs = append(allErrs, field.Invalid(fldPath.Child("Config"), dp.Config, + "Invalid json schema "+err.Error())) + return allErrs + } + rdmaSharedDevicePluginConfigJSONLoader := gojsonschema.NewStringLoader(rdmaSharedDevicePluginConfig) + result, err := rdmaSharedDevicePluginSchema.Validate(rdmaSharedDevicePluginConfigJSONLoader) + if err != nil { + allErrs = append(allErrs, field.Invalid(fldPath.Child("Config"), dp.Config, + "Invalid json of RdmaSharedDevicePluginConfig"+err.Error())) + } else if result.Valid() { + configListInterface := rdmaSharedDevicePluginConfigJSON["configList"] + configList, _ := configListInterface.([]interface{}) + for _, configInterface := range configList { + config := configInterface.(map[string]interface{}) + resourceName := config["resourceName"].(string) + if !isValidRdmaSharedDevicePluginResourceName(resourceName) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("Config"), + dp.Config, "Invalid Resource name, it must consist of alphanumeric characters, "+ + "'-', '_' or '.', and must start and end with an alphanumeric character "+ + "(e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0"+ + "-9_.]*)?[A-Za-z0-9]')")) + } + } + } else { + for _, ResultErr := range result.Errors() { + allErrs = append(allErrs, field.Invalid(fldPath.Child("Config"), dp.Config, ResultErr.Description())) + } + } + return allErrs +} + +// validate is a helper function to perform validation for IBKubernetesSpec. +func (ibk *IBKubernetesSpec) validate(fldPath *field.Path) field.ErrorList { + var allErrs field.ErrorList + + if !isValidPKeyGUID(ibk.PKeyGUIDPoolRangeStart) || !isValidPKeyGUID(ibk.PKeyGUIDPoolRangeEnd) { + if !isValidPKeyGUID(ibk.PKeyGUIDPoolRangeStart) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("pKeyGUIDPoolRangeStart"), + ibk.PKeyGUIDPoolRangeStart, "pKeyGUIDPoolRangeStart must be a valid GUID format:"+ + "xx:xx:xx:xx:xx:xx:xx:xx with Hexa numbers")) + } + if !isValidPKeyGUID(ibk.PKeyGUIDPoolRangeEnd) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("pKeyGUIDPoolRangeEnd"), + ibk.PKeyGUIDPoolRangeEnd, "pKeyGUIDPoolRangeEnd must be a valid GUID format: "+ + "xx:xx:xx:xx:xx:xx:xx:xx with Hexa numbers")) + } + return allErrs + } else if !isValidPKeyRange(ibk.PKeyGUIDPoolRangeStart, ibk.PKeyGUIDPoolRangeEnd) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("pKeyGUIDPoolRangeEnd"), + ibk.PKeyGUIDPoolRangeEnd, "pKeyGUIDPoolRangeStart-pKeyGUIDPoolRangeEnd must be a valid range")) + } + return allErrs +} + +// isValidPKeyGUID checks if a given string is a valid GUID format. +func isValidPKeyGUID(guid string) bool { + PKeyGUIDPattern := `^([0-9A-Fa-f]{2}:){7}([0-9A-Fa-f]{2})$` + PKeyGUIDRegex := regexp.MustCompile(PKeyGUIDPattern) + return PKeyGUIDRegex.MatchString(guid) +} + +// isValidPKeyRange checks if range of startGUID and endGUID sis valid +func isValidPKeyRange(startGUID, endGUID string) bool { + startGUIDWithoutSeparator := strings.ReplaceAll(startGUID, ":", "") + endGUIDWithoutSeparator := strings.ReplaceAll(endGUID, ":", "") + + startGUIDIntValue := new(big.Int) + endGUIDIntValue := new(big.Int) + startGUIDIntValue, _ = startGUIDIntValue.SetString(startGUIDWithoutSeparator, 16) + endGUIDIntValue, _ = endGUIDIntValue.SetString(endGUIDWithoutSeparator, 16) + return endGUIDIntValue.Cmp(startGUIDIntValue) > 0 +} + +func (ofedSpec *OFEDDriverSpec) validateVersion(fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + // Perform version validation logic here + if !isValidOFEDVersion(ofedSpec.Version) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("version"), ofedSpec.Version, + `invalid OFED version, the regex used for validation is ^(\d+\.\d+-\d+(\.\d+)*)$ `)) + } + return allErrs +} + +// isValidOFEDVersion is a custom function to validate OFED version +func isValidOFEDVersion(version string) bool { + versionPattern := `^(\d+\.\d+-\d+(\.\d+)*)$` + versionRegex := regexp.MustCompile(versionPattern) + return versionRegex.MatchString(version) +} + +func isValidSriovNetworkDevicePluginResourceName(resourceName string) bool { + resourceNamePattern := `^([A-Za-z0-9][A-Za-z0-9_.]*)?[A-Za-z0-9]$` + resourceNameRegex := regexp.MustCompile(resourceNamePattern) + return resourceNameRegex.MatchString(resourceName) +} + +func isValidRdmaSharedDevicePluginResourceName(resourceName string) bool { + resourceNamePattern := `^([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]$` + resourceNameRegex := regexp.MustCompile(resourceNamePattern) + return resourceNameRegex.MatchString(resourceName) +} + +// +kubebuilder:object:generate=false +type schemaValidator struct { + schemas map[string]*gojsonschema.Schema +} + +func (sv *schemaValidator) GetSchema(schemaName string) (*gojsonschema.Schema, error) { + s, ok := sv.schemas[schemaName] + if !ok { + return nil, fmt.Errorf("validation schema not found: %s", schemaName) + } + return s, nil +} + +func InitSchemaValidator(schemaPath string) { + sv := &schemaValidator{ + schemas: make(map[string]*gojsonschema.Schema), + } + files, err := os.ReadDir(schemaPath) + if err != nil { + nicClusterPolicyLog.Error(err, "fail to read validation schema files") + panic(err) + } + for _, f := range files { + s, err := gojsonschema.NewSchema(gojsonschema.NewReferenceLoader(fmt.Sprintf("file://%s/%s", schemaPath, f.Name()))) + if err != nil { + nicClusterPolicyLog.Error(err, "fail to load validation schema") + panic(err) + } + sv.schemas[strings.TrimSuffix(f.Name(), ".json")] = s + } + schemaValidators = sv +} diff --git a/api/v1alpha1/nicclusterpolicy_webhook_test.go b/api/v1alpha1/nicclusterpolicy_webhook_test.go new file mode 100644 index 00000000..d203669a --- /dev/null +++ b/api/v1alpha1/nicclusterpolicy_webhook_test.go @@ -0,0 +1,527 @@ +/* +2023 NVIDIA CORPORATION & AFFILIATES + +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. +*/ +package v1alpha1 //nolint:dupl + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +//nolint:dupl +var _ = Describe("Validate", func() { + Context("NicClusterPolicy tests", func() { + It("Valid GUID range", func() { + nicClusterPolicy := NicClusterPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: NicClusterPolicySpec{ + IBKubernetes: &IBKubernetesSpec{ + PKeyGUIDPoolRangeStart: "00:00:00:00:00:00:00:00", + PKeyGUIDPoolRangeEnd: "00:00:00:00:00:00:00:01", + ImageSpec: ImageSpec{ + Image: "ib-kubernetes", + Repository: "ghcr.io/mellanox", + Version: "v1.0.2", + ImagePullSecrets: []string{}, + }, + }, + }, + } + Expect(nicClusterPolicy.ValidateCreate()).NotTo(HaveOccurred()) + }) + It("Invalid GUID range", func() { + nicClusterPolicy := NicClusterPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: NicClusterPolicySpec{ + IBKubernetes: &IBKubernetesSpec{ + PKeyGUIDPoolRangeStart: "00:00:00:00:00:00:00:02", + PKeyGUIDPoolRangeEnd: "00:00:00:00:00:00:00:00", + ImageSpec: ImageSpec{ + Image: "ib-kubernetes", + Repository: "ghcr.io/mellanox", + Version: "v1.0.2", + ImagePullSecrets: []string{}, + }, + }, + }, + } + Expect(nicClusterPolicy.ValidateCreate().Error()).To(ContainSubstring( + "pKeyGUIDPoolRangeStart-pKeyGUIDPoolRangeEnd must be a valid range")) + }) + It("Invalid start and end GUID", func() { + nicClusterPolicy := NicClusterPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: NicClusterPolicySpec{ + IBKubernetes: &IBKubernetesSpec{ + PKeyGUIDPoolRangeStart: "00:00:00:00", + PKeyGUIDPoolRangeEnd: "00:00:00:00", + ImageSpec: ImageSpec{ + Image: "ib-kubernetes", + Repository: "ghcr.io/mellanox", + Version: "v1.0.2", + ImagePullSecrets: []string{}, + }, + }, + }, + } + Expect(nicClusterPolicy.ValidateCreate().Error()).To(And( + ContainSubstring("pKeyGUIDPoolRangeStart must be a valid GUID format"), + ContainSubstring("pKeyGUIDPoolRangeEnd must be a valid GUID format"))) + }) + It("Valid MOFED version", func() { + nicClusterPolicy := NicClusterPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: NicClusterPolicySpec{ + OFEDDriver: &OFEDDriverSpec{ + ImageSpec: ImageSpec{ + Image: "mofed", + Repository: "ghcr.io/mellanox", + Version: "23.10-0.2.2.0", + ImagePullSecrets: []string{}, + }, + }, + }, + } + Expect(nicClusterPolicy.ValidateCreate()).NotTo(HaveOccurred()) + }) + It("InValid MOFED version", func() { + nicClusterPolicy := NicClusterPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: NicClusterPolicySpec{ + OFEDDriver: &OFEDDriverSpec{ + ImageSpec: ImageSpec{ + Image: "mofed", + Repository: "ghcr.io/mellanox", + Version: "23-10-0.2.2.0", + ImagePullSecrets: []string{}, + }, + }, + }, + } + Expect(nicClusterPolicy.ValidateCreate().Error()).To(ContainSubstring("invalid OFED version")) + }) + It("Valid RDMA config JSON", func() { + rdmaConfig := `{ + "configList": [{ + "resourceName": "rdma_shared_device_a", + "rdmaHcaMax": 63, + "selectors": { + "vendors": ["15b3"], + "deviceIDs": ["101b"]}}]}` + nicClusterPolicy := NicClusterPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: NicClusterPolicySpec{ + RdmaSharedDevicePlugin: &DevicePluginSpec{ + ImageSpecWithConfig: ImageSpecWithConfig{ + Config: &rdmaConfig, + ImageSpec: ImageSpec{ + Image: "k8s-rdma-shared-dev-plugin", + Repository: "ghcr.io/mellanox", + Version: "sha-fe7f371c7e1b8315bf900f71cd25cfc1251dc775", + ImagePullSecrets: []string{}, + }, + }, + }, + }, + } + Expect(nicClusterPolicy.ValidateCreate()).NotTo(HaveOccurred()) + }) + It("Valid RDMA config JSON", func() { + rdmaConfig := `{ + "configList": [{ + "resourceName": "rdma_shared_device_a", + "rdmaHcaMax": 63, + "selectors": { + "vendors": ["15b3"], + "deviceIDs": ["101b"]}}]}` + nicClusterPolicy := NicClusterPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: NicClusterPolicySpec{ + RdmaSharedDevicePlugin: &DevicePluginSpec{ + ImageSpecWithConfig: ImageSpecWithConfig{ + Config: &rdmaConfig, + ImageSpec: ImageSpec{ + Image: "k8s-rdma-shared-dev-plugin", + Repository: "ghcr.io/mellanox", + Version: "sha-fe7f371c7e1b8315bf900f71cd25cfc1251dc775", + ImagePullSecrets: []string{}, + }, + }, + }, + }, + } + Expect(nicClusterPolicy.ValidateCreate()).NotTo(HaveOccurred()) + }) + It("Invalid RDMA config JSON, missing starting {", func() { + invalidRdmaConfigJSON := ` + "configList": [{ + "resourceName": "rdma_shared_device_a", + "rdmaHcaMax": 63, + "selectors": { + "vendors": ["15b3"], + "deviceIDs": ["101b"]}}]}` + nicClusterPolicy := NicClusterPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: NicClusterPolicySpec{ + RdmaSharedDevicePlugin: &DevicePluginSpec{ + ImageSpecWithConfig: ImageSpecWithConfig{ + Config: &invalidRdmaConfigJSON, + ImageSpec: ImageSpec{ + Image: "k8s-rdma-shared-dev-plugin", + Repository: "ghcr.io/mellanox", + Version: "sha-fe7f371c7e1b8315bf900f71cd25cfc1251dc775", + ImagePullSecrets: []string{}, + }, + }, + }, + }, + } + Expect(nicClusterPolicy.ValidateCreate().Error()).To(ContainSubstring( + "Invalid json of RdmaSharedDevicePluginConfig")) + }) + It("Invalid RDMA config JSON schema, resourceName not valid", func() { + invalidRdmaConfigJSON := `{ + "configList": [{ + "resourceName": "rdma-shared-device-a!!", + "rdmaHcaMax": 63, + "selectors": { + "vendors": ["15b3"], + "deviceIDs": ["101b"]}}]}` + nicClusterPolicy := NicClusterPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: NicClusterPolicySpec{ + RdmaSharedDevicePlugin: &DevicePluginSpec{ + ImageSpecWithConfig: ImageSpecWithConfig{ + Config: &invalidRdmaConfigJSON, + ImageSpec: ImageSpec{ + Image: "k8s-rdma-shared-dev-plugin", + Repository: "ghcr.io/mellanox", + Version: "sha-fe7f371c7e1b8315bf900f71cd25cfc1251dc775", + ImagePullSecrets: []string{}, + }, + }, + }, + }, + } + Expect(nicClusterPolicy.ValidateCreate().Error()).To(ContainSubstring("Invalid Resource name")) + }) + It("Invalid RDMA config JSON schema, no configList provided", func() { + invalidRdmaConfigJSON := `{ + "configList_a": [{ + "resourceName": "rdma_shared_device_a", + "rdmaHcaMax": 63, + "selectors": { + "vendors": ["15b3"], + "deviceIDs": ["101b"]}}]}` + nicClusterPolicy := NicClusterPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: NicClusterPolicySpec{ + RdmaSharedDevicePlugin: &DevicePluginSpec{ + ImageSpecWithConfig: ImageSpecWithConfig{ + Config: &invalidRdmaConfigJSON, + ImageSpec: ImageSpec{ + Image: "k8s-rdma-shared-dev-plugin", + Repository: "ghcr.io/mellanox", + Version: "sha-fe7f371c7e1b8315bf900f71cd25cfc1251dc775", + ImagePullSecrets: []string{}, + }, + }, + }, + }, + } + Expect(nicClusterPolicy.ValidateCreate().Error()).To(ContainSubstring("configList is required")) + }) + It("Invalid RDMA config JSON schema, none of the selectors are provided", func() { + invalidRdmaConfigJSON := `{ + "configList": [{ + "resourceName": "rdma_shared_device_a", + "rdmaHcaMax": 63, + "selectors": {}}]}` + nicClusterPolicy := NicClusterPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: NicClusterPolicySpec{ + RdmaSharedDevicePlugin: &DevicePluginSpec{ + ImageSpecWithConfig: ImageSpecWithConfig{ + Config: &invalidRdmaConfigJSON, + ImageSpec: ImageSpec{ + Image: "k8s-rdma-shared-dev-plugin", + Repository: "ghcr.io/mellanox", + Version: "sha-fe7f371c7e1b8315bf900f71cd25cfc1251dc775", + ImagePullSecrets: []string{}, + }, + }, + }, + }, + } + Expect(nicClusterPolicy.ValidateCreate().Error()).To(ContainSubstring("vendors is required")) + }) + It("Invalid RDMA config JSON, vendors must be list of strings", func() { + invalidRdmaConfigJSON := `{ + "configList": [{ + "resourceName": "rdma_shared_device_a", + "rdmaHcaMax": 63, + "selectors": { + "vendors": [15], + "deviceIDs": ["101b"]}}]}` + nicClusterPolicy := NicClusterPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: NicClusterPolicySpec{ + RdmaSharedDevicePlugin: &DevicePluginSpec{ + ImageSpecWithConfig: ImageSpecWithConfig{ + Config: &invalidRdmaConfigJSON, + ImageSpec: ImageSpec{ + Image: "k8s-rdma-shared-dev-plugin", + Repository: "ghcr.io/mellanox", + Version: "sha-fe7f371c7e1b8315bf900f71cd25cfc1251dc775", + ImagePullSecrets: []string{}, + }, + }, + }, + }, + } + Expect(nicClusterPolicy.ValidateCreate().Error()).To(ContainSubstring( + "Invalid type. Expected: string, given: integer")) + }) + It("Invalid RDMA config JSON, deviceIDs must be list of strings", func() { + invalidRdmaConfigJSON := `{ + "configList": [{ + "resourceName": "rdma_shared_device_a", + "rdmaHcaMax": 63, + "selectors": { + "vendors": ["15b3"], + "deviceIDs": [1010]}}]}` + nicClusterPolicy := NicClusterPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: NicClusterPolicySpec{ + RdmaSharedDevicePlugin: &DevicePluginSpec{ + ImageSpecWithConfig: ImageSpecWithConfig{ + Config: &invalidRdmaConfigJSON, + ImageSpec: ImageSpec{ + Image: "k8s-rdma-shared-dev-plugin", + Repository: "ghcr.io/mellanox", + Version: "sha-fe7f371c7e1b8315bf900f71cd25cfc1251dc775", + ImagePullSecrets: []string{}, + }, + }, + }, + }, + } + Expect(nicClusterPolicy.ValidateCreate().Error()).To(ContainSubstring( + "Invalid type. Expected: string, given: integer")) + }) + It("Valid SriovDevicePlugin config JSON", func() { + sriovConfig := `{ + "resourceList": [{ + "resourceName": "hostdev", + "selectors": { + "vendors": ["15b3"], + "devices": ["101b"]}}]}` + nicClusterPolicy := NicClusterPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: NicClusterPolicySpec{ + SriovDevicePlugin: &DevicePluginSpec{ + ImageSpecWithConfig: ImageSpecWithConfig{ + Config: &sriovConfig, + ImageSpec: ImageSpec{ + Image: "sriov-network-device-plugin", + Repository: "nvcr.io/nvstaging/mellanox", + Version: "network-operator-23.10.0-beta.1", + ImagePullSecrets: []string{}, + }, + }, + }, + }, + } + Expect(nicClusterPolicy.ValidateCreate()).NotTo(HaveOccurred()) + }) + It("Valid SriovDevicePlugin config JSON, selectors object is a list ", func() { + sriovConfig := `{ + "resourceList": [{ + "resourceName": "hostdev", + "selectors": [{ + "vendors": ["15b3"], + "devices": ["101b"]}]}]}` + nicClusterPolicy := NicClusterPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: NicClusterPolicySpec{ + SriovDevicePlugin: &DevicePluginSpec{ + ImageSpecWithConfig: ImageSpecWithConfig{ + Config: &sriovConfig, + ImageSpec: ImageSpec{ + Image: "sriov-network-device-plugin", + Repository: "nvcr.io/nvstaging/mellanox", + Version: "network-operator-23.10.0-beta.1", + ImagePullSecrets: []string{}, + }, + }, + }, + }, + } + Expect(nicClusterPolicy.ValidateCreate()).NotTo(HaveOccurred()) + }) + It("Invalid SriovDevicePlugin config JSON, missing starting {", func() { + invalidSriovConfigJSON := ` + "resourceList": [{ + "resourceName": "hostdev", + "selectors": { + "vendors": ["15b3"], + "devices": ["101b"]}}]}` + nicClusterPolicy := NicClusterPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: NicClusterPolicySpec{ + SriovDevicePlugin: &DevicePluginSpec{ + ImageSpecWithConfig: ImageSpecWithConfig{ + Config: &invalidSriovConfigJSON, + ImageSpec: ImageSpec{ + Image: "sriov-network-device-plugin", + Repository: "nvcr.io/nvstaging/mellanox", + Version: "network-operator-23.10.0-beta.1", + ImagePullSecrets: []string{}, + }, + }, + }, + }, + } + Expect(nicClusterPolicy.ValidateCreate().Error()).To(ContainSubstring( + "Invalid json of SriovNetworkDevicePluginConfig")) + }) + It("Invalid SriovDevicePlugin config JSON schema, resourceName not valid", func() { + invalidSriovConfigJSON := `{ + "resourceList": [{ + "resourceName": "sriov-network-device-plugin", + "selectors": { + "vendors": ["15b3"], + "devices": ["101b"]}}]}` + nicClusterPolicy := NicClusterPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: NicClusterPolicySpec{ + SriovDevicePlugin: &DevicePluginSpec{ + ImageSpecWithConfig: ImageSpecWithConfig{ + Config: &invalidSriovConfigJSON, + ImageSpec: ImageSpec{ + Image: "sriov-network-device-plugin", + Repository: "nvcr.io/nvstaging/mellanox", + Version: "network-operator-23.10.0-beta.1", + ImagePullSecrets: []string{}, + }, + }, + }, + }, + } + Expect(nicClusterPolicy.ValidateCreate().Error()).To(ContainSubstring("Invalid Resource name")) + }) + It("Invalid SriovDevicePlugin config JSON schema, no resourceList provided", func() { + invalidSriovConfigJSON := `{ + "resourceList_a": [{ + "resourceName": "sriov_network_device_plugin", + "selectors": { + "vendors": ["15b3"], + "devices": ["101b"]}}]}` + nicClusterPolicy := NicClusterPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: NicClusterPolicySpec{ + SriovDevicePlugin: &DevicePluginSpec{ + ImageSpecWithConfig: ImageSpecWithConfig{ + Config: &invalidSriovConfigJSON, + ImageSpec: ImageSpec{ + Image: "sriov-network-device-plugin", + Repository: "nvcr.io/nvstaging/mellanox", + Version: "network-operator-23.10.0-beta.1", + ImagePullSecrets: []string{}, + }, + }, + }, + }, + } + Expect(nicClusterPolicy.ValidateCreate().Error()).To(ContainSubstring("resourceList is required")) + }) + It("Invalid SriovDevicePlugin config JSON schema, none of the selectors are provided", func() { + invalidSriovConfigJSON := `{ + "resourceList": [{ + "resourceName": "sriov_network_device_plugin", + "selectors": {}}]}` + nicClusterPolicy := NicClusterPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: NicClusterPolicySpec{ + SriovDevicePlugin: &DevicePluginSpec{ + ImageSpecWithConfig: ImageSpecWithConfig{ + Config: &invalidSriovConfigJSON, + ImageSpec: ImageSpec{ + Image: "sriov-network-device-plugin", + Repository: "nvcr.io/nvstaging/mellanox", + Version: "network-operator-23.10.0-beta.1", + ImagePullSecrets: []string{}, + }, + }, + }, + }, + } + Expect(nicClusterPolicy.ValidateCreate().Error()).To(ContainSubstring("vendors is required")) + }) + It("Invalid SriovDevicePlugin config JSON, vendors must be list of strings", func() { + sriovConfig := `{ + "resourceList": [{ + "resourceName": "hostdev", + "selectors": { + "vendors": [15], + "devices": ["101b"]}}]}` + nicClusterPolicy := NicClusterPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: NicClusterPolicySpec{ + SriovDevicePlugin: &DevicePluginSpec{ + ImageSpecWithConfig: ImageSpecWithConfig{ + Config: &sriovConfig, + ImageSpec: ImageSpec{ + Image: "sriov-network-device-plugin", + Repository: "nvcr.io/nvstaging/mellanox", + Version: "network-operator-23.10.0-beta.1", + ImagePullSecrets: []string{}, + }, + }, + }, + }, + } + Expect(nicClusterPolicy.ValidateCreate().Error()).To(ContainSubstring( + "Invalid type. Expected: string, given: integer")) + }) + It("Invalid SriovDevicePlugin config JSON, devices must be list of strings", func() { + sriovConfig := `{ + "resourceList": [{ + "resourceName": "hostdev", + "selectors": { + "vendors": ["15b3"], + "devices": [1020]}}]}` + nicClusterPolicy := NicClusterPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: NicClusterPolicySpec{ + SriovDevicePlugin: &DevicePluginSpec{ + ImageSpecWithConfig: ImageSpecWithConfig{ + Config: &sriovConfig, + ImageSpec: ImageSpec{ + Image: "sriov-network-device-plugin", + Repository: "nvcr.io/nvstaging/mellanox", + Version: "network-operator-23.10.0-beta.1", + ImagePullSecrets: []string{}, + }, + }, + }, + }, + } + Expect(nicClusterPolicy.ValidateCreate().Error()).To(ContainSubstring( + "Invalid type. Expected: string, given: integer")) + }) + }) +}) diff --git a/api/v1alpha1/suite_test.go b/api/v1alpha1/suite_test.go index c69414de..1150f803 100644 --- a/api/v1alpha1/suite_test.go +++ b/api/v1alpha1/suite_test.go @@ -20,9 +20,15 @@ import ( . "github.com/onsi/gomega" "testing" + + mellanoxv1alpha1 "github.com/Mellanox/network-operator/api/v1alpha1" ) func TestV1alpha1(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "V1alpha1 Suite") } + +var _ = BeforeSuite(func() { + mellanoxv1alpha1.InitSchemaValidator("../../webhook-schemas") +}) diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 697bcf36..f2233813 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -23,7 +23,7 @@ package v1alpha1 import ( "k8s.io/api/core/v1" - runtime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. diff --git a/config/certmanager/certificate.yaml b/config/certmanager/certificate.yaml new file mode 100644 index 00000000..813ec769 --- /dev/null +++ b/config/certmanager/certificate.yaml @@ -0,0 +1,39 @@ +# The following manifests contain a self-signed issuer CR and a certificate CR. +# More document can be found at https://docs.cert-manager.io +# WARNING: Targets CertManager v1.0. Check https://cert-manager.io/docs/installation/upgrading/ for breaking changes. +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + labels: + app.kubernetes.io/name: certificate + app.kubernetes.io/instance: serving-cert + app.kubernetes.io/component: certificate + app.kubernetes.io/created-by: nvidia-network-operator + app.kubernetes.io/part-of: nvidia-network-operator + app.kubernetes.io/managed-by: kustomize + name: selfsigned-issuer + namespace: system +spec: + selfSigned: {} +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + labels: + app.kubernetes.io/name: certificate + app.kubernetes.io/instance: serving-cert + app.kubernetes.io/component: certificate + app.kubernetes.io/created-by: nvidia-network-operator + app.kubernetes.io/part-of: nvidia-network-operator + app.kubernetes.io/managed-by: kustomize + name: serving-cert # this name should match the one appeared in kustomizeconfig.yaml + namespace: system +spec: + # $(SERVICE_NAME) and $(SERVICE_NAMESPACE) will be substituted by kustomize + dnsNames: + - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc + - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc.cluster.local + issuerRef: + kind: Issuer + name: selfsigned-issuer + secretName: webhook-server-cert # this secret will not be prefixed, since it's not managed by kustomize diff --git a/config/certmanager/kustomization.yaml b/config/certmanager/kustomization.yaml new file mode 100644 index 00000000..bebea5a5 --- /dev/null +++ b/config/certmanager/kustomization.yaml @@ -0,0 +1,5 @@ +resources: +- certificate.yaml + +configurations: +- kustomizeconfig.yaml diff --git a/config/certmanager/kustomizeconfig.yaml b/config/certmanager/kustomizeconfig.yaml new file mode 100644 index 00000000..90d7c313 --- /dev/null +++ b/config/certmanager/kustomizeconfig.yaml @@ -0,0 +1,16 @@ +# This configuration is for teaching kustomize how to update name ref and var substitution +nameReference: +- kind: Issuer + group: cert-manager.io + fieldSpecs: + - kind: Certificate + group: cert-manager.io + path: spec/issuerRef/name + +varReference: +- kind: Certificate + group: cert-manager.io + path: spec/commonName +- kind: Certificate + group: cert-manager.io + path: spec/dnsNames diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 1ea8199a..4dfde8dc 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -11,16 +11,14 @@ resources: patchesStrategicMerge: # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. # patches here are for enabling the conversion webhook for each CRD -#- patches/webhook_in_macvlannetworks.yaml -#- patches/webhook_in_nicclusterpolicies.yaml -#- patches/webhook_in_hostdevicenetworks.yaml +# - patches/webhook_in_nicclusterpolicies.yaml +# - patches/webhook_in_hostdevicenetworks.yaml #+kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix. # patches here are for enabling the CA injection for each CRD -#- patches/cainjection_in_macvlannetworks.yaml -#- patches/cainjection_in_nicclusterpolicies.yaml -#- patches/cainjection_in_hostdevicenetworks.yaml +# - patches/cainjection_in_nicclusterpolicies.yaml +# - patches/cainjection_in_hostdevicenetworks.yaml #+kubebuilder:scaffold:crdkustomizecainjectionpatch # the following config is for teaching kustomize how to do kustomization for CRDs. diff --git a/config/default/manager_webhook_patch.yaml b/config/default/manager_webhook_patch.yaml new file mode 100644 index 00000000..663dbca2 --- /dev/null +++ b/config/default/manager_webhook_patch.yaml @@ -0,0 +1,26 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: manager + env: + - name: "ENABLE_WEBHOOKS" + value: "false" + ports: + - containerPort: 9443 + name: webhook-server + protocol: TCP + volumeMounts: + - mountPath: /tmp/k8s-webhook-server/serving-certs + name: cert + readOnly: true + volumes: + - name: cert + secret: + defaultMode: 420 + secretName: webhook-server-cert diff --git a/config/default/webhookcainjection_patch.yaml b/config/default/webhookcainjection_patch.yaml new file mode 100644 index 00000000..33780cc4 --- /dev/null +++ b/config/default/webhookcainjection_patch.yaml @@ -0,0 +1,15 @@ +# This patch add annotation to admission webhook config and +# the variables $(CERTIFICATE_NAMESPACE) and $(CERTIFICATE_NAME) will be substituted by kustomize. +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + labels: + app.kubernetes.io/name: validatingwebhookconfiguration + app.kubernetes.io/instance: validating-webhook-configuration + app.kubernetes.io/component: webhook + app.kubernetes.io/created-by: nvidia-network-operator + app.kubernetes.io/part-of: nvidia-network-operator + app.kubernetes.io/managed-by: kustomize + name: validating-webhook-configuration + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index 9da4443d..1771793c 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -59,6 +59,9 @@ spec: imagePullPolicy: IfNotPresent image: controller:latest name: manager + env: + - name: ENABLE_WEBHOOKS + value: "false" securityContext: allowPrivilegeEscalation: false livenessProbe: diff --git a/config/webhook/kustomization.yaml b/config/webhook/kustomization.yaml new file mode 100644 index 00000000..9cf26134 --- /dev/null +++ b/config/webhook/kustomization.yaml @@ -0,0 +1,6 @@ +resources: +- manifests.yaml +- service.yaml + +configurations: +- kustomizeconfig.yaml diff --git a/config/webhook/kustomizeconfig.yaml b/config/webhook/kustomizeconfig.yaml new file mode 100644 index 00000000..e809f782 --- /dev/null +++ b/config/webhook/kustomizeconfig.yaml @@ -0,0 +1,18 @@ +# the following config is for teaching kustomize where to look at when substituting vars. +# It requires kustomize v2.1.0 or newer to work properly. +nameReference: +- kind: Service + version: v1 + fieldSpecs: + - kind: ValidatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/name + +namespace: +- kind: ValidatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/namespace + create: true + +varReference: +- path: metadata/annotations diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml new file mode 100644 index 00000000..9576fb35 --- /dev/null +++ b/config/webhook/manifests.yaml @@ -0,0 +1,47 @@ +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + creationTimestamp: null + name: validating-webhook-configuration +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-mellanox-com-v1alpha1-hostdevicenetwork + failurePolicy: Fail + name: vhostdevicenetwork.kb.io + rules: + - apiGroups: + - mellanox.com + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - hostdevicenetworks + sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-mellanox-com-v1alpha1-nicclusterpolicy + failurePolicy: Fail + name: vnicclusterpolicy.kb.io + rules: + - apiGroups: + - mellanox.com + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - nicclusterpolicies + sideEffects: None diff --git a/config/webhook/service.yaml b/config/webhook/service.yaml new file mode 100644 index 00000000..3f638bd9 --- /dev/null +++ b/config/webhook/service.yaml @@ -0,0 +1,13 @@ + +apiVersion: v1 +kind: Service +metadata: + name: webhook-service + namespace: system +spec: + ports: + - port: 443 + protocol: TCP + targetPort: 9443 + selector: + control-plane: controller-manager diff --git a/deploy/operator.yaml b/deploy/operator.yaml index 25a2e033..6faa9e19 100644 --- a/deploy/operator.yaml +++ b/deploy/operator.yaml @@ -59,3 +59,5 @@ spec: fieldPath: metadata.namespace - name: OPERATOR_NAME value: "network-operator" + - name: ENABLE_WEBHOOKS + value: $ENABLE_WEBHOOKS diff --git a/deployment/network-operator/README.md b/deployment/network-operator/README.md index 16438614..d4219dbb 100644 --- a/deployment/network-operator/README.md +++ b/deployment/network-operator/README.md @@ -149,6 +149,40 @@ $ helm install -n network-operator --create-namespace --wait network-operator ./ $ kubectl -n network-operator get pods ``` +#### Deploy Network Operator with Admission Controller + +The Admission Controller can be optionally included as part of the Network Operator installation process. +It has the capability to validate supported Custom Resource Definitions (CRDs), which currently include NicClusterPolicy and HostDeviceNetwork. +By default, the deployment of the admission controller is disabled. To enable it, you must set `operator.admissionController.enabled` to `true`. + +Enabling the admission controller provides you with two options for managing certificates. +You can either utilize [cert-manager](https://cert-manager.io/docs/installation/) for generating a self-signed certificate automatically, or you can provide your own self-signed certificate. + +To use `cert-manager`, ensure that `operator.admissionController.useCertManager` is set to `true`. Additionally, make sure that you deploy cert-manager before initiating the Network Operator deployment. + +If you prefer not to use `cert-manager`, set `operator.admissionController.useCertManager` to `false`, and then provide your custom certificate and key using `operator.admissionController.certificate.tlsCrt` and `operator.admissionController.certificate.tlsKey`. + +> __NOTE__: When using your own certificate, the certificate must be valid for -webhook-service.< +> Release_Namespace>.svc, e.g. network-operator-webhook-service.network-operator.svc + +> __NOTE__: When deploying network operator with admission controller using helm, you need to append `--wait` to helm install and helm upgrade commands +> + +##### Generating self-signed certificate using OpenSSL + +To generate a self-signed SSL certificate valid for a specific hostname, you can use the `openssl` command-line tool. +First, navigate to the directory where you want to store your certificate and key files. Then, run the following +command: + +```bash +SVCNAME="network-operator-webhook-service.network-operator.svc" +openssl req -x509 -nodes -batch -newkey rsa:2048 -keyout server.key -out server.crt -days 365 -addext "subjectAltName=DNS:$SVCNAME" +``` + +Replace `SVCNAME` with the SVC name follows this convention -webhook-service..svc. +This command will generate a new RSA key pair with 2048 bits and create a self-signed certificate (`server.crt`) and +private key (`server.key`) that are valid for 365 days. + ## Helm Tests Network Operator has Helm tests to verify deployment. To run tests it is required to set the following chart parameters @@ -344,6 +378,10 @@ parameters. | Name | Type | Default | Description | |------------------------------------------------------|--------|------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `operator.admissionController.enabled` | bool | `False` | deploy with admission controller webhook | +| `operator.admissionController.useCertManager` | bool | `False` | use cert-manager for generating self-signed certificate | +| `operator.admissionController.certificate.tlsCrt` | string | `` | External certificate crt. Ignored if cert-manager is used. | +| `operator.admissionController.certificate.tlsKey` | string | `` | External certificate key. Ignored if cert-manager is used. | | `nfd.enabled` | bool | `True` | deploy Node Feature Discovery | | `nfd.deployNodeFeatureRules` | bool | `True` | deploy Node Feature Rules to label the nodes | | `sriovNetworkOperator.enabled` | bool | `False` | deploy SR-IOV Network Operator | diff --git a/deployment/network-operator/templates/admission_controller.yaml b/deployment/network-operator/templates/admission_controller.yaml new file mode 100644 index 00000000..a8246e60 --- /dev/null +++ b/deployment/network-operator/templates/admission_controller.yaml @@ -0,0 +1,129 @@ +--- +{{- if .Values.operator.admissionController.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ .Release.Name }}-webhook-service + namespace: {{ .Release.Namespace }} +spec: + ports: + - port: 443 + protocol: TCP + targetPort: 9443 + selector: + control-plane: {{ .Release.Name }}-controller +{{- end }} +--- +{{- if and .Values.operator.admissionController.enabled .Values.operator.admissionController.useCertManager }} +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + labels: + app.kubernetes.io/component: certificate + app.kubernetes.io/created-by: {{ .Release.Name }} + app.kubernetes.io/instance: serving-cert + app.kubernetes.io/name: certificate + app.kubernetes.io/part-of: {{ .Release.Name }} + name: {{ .Release.Name }}-serving-cert + namespace: {{ .Release.Namespace }} +spec: + dnsNames: + - {{ .Release.Name }}-webhook-service.{{ .Release.Namespace }}.svc + - {{ .Release.Name }}-webhook-service.{{ .Release.Namespace }}.svc.cluster.local + issuerRef: + kind: Issuer + name: {{ .Release.Name }}-selfsigned-issuer + secretName: webhook-server-cert +{{- end }} +--- +{{- if and .Values.operator.admissionController.enabled .Values.operator.admissionController.useCertManager }} +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + labels: + app.kubernetes.io/component: certificate + app.kubernetes.io/created-by: {{ .Release.Name }} + app.kubernetes.io/instance: serving-cert + app.kubernetes.io/name: certificate + app.kubernetes.io/part-of: {{ .Release.Name }} + name: {{ .Release.Name }}-selfsigned-issuer + namespace: {{ .Release.Namespace }} +spec: + selfSigned: {} +{{- end }} +--- +{{- if .Values.operator.admissionController.enabled }} +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + {{- if .Values.operator.admissionController.useCertManager }} + annotations: + cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ .Release.Name }}-serving-cert + {{- end }} + labels: + app.kubernetes.io/component: webhook + app.kubernetes.io/created-by: {{ .Release.Name }} + app.kubernetes.io/instance: validating-webhook-configuration + app.kubernetes.io/name: validatingwebhookconfiguration + app.kubernetes.io/part-of: {{ .Release.Name }} + name: {{ .Release.Name }}-validating-webhook-configuration +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: {{ .Release.Name }}-webhook-service + namespace: {{ .Release.Namespace }} + path: /validate-mellanox-com-v1alpha1-hostdevicenetwork + {{- if not .Values.operator.admissionController.useCertManager }} + caBundle: {{ .Values.operator.admissionController.certificate.tlsCrt | b64enc | quote }} + {{- end }} + failurePolicy: Fail + name: vhostdevicenetwork.kb.io + rules: + - apiGroups: + - mellanox.com + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - hostdevicenetworks + sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: {{ .Release.Name }}-webhook-service + namespace: {{ .Release.Namespace }} + path: /validate-mellanox-com-v1alpha1-nicclusterpolicy + {{- if not .Values.operator.admissionController.useCertManager }} + caBundle: {{ .Values.operator.admissionController.certificate.tlsCrt | b64enc | quote }} + {{- end }} + failurePolicy: Fail + name: vnicclusterpolicy.kb.io + rules: + - apiGroups: + - mellanox.com + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - nicclusterpolicies + sideEffects: None +{{- end }} +--- +{{- if and .Values.operator.admissionController.enabled (not .Values.operator.admissionController.useCertManager) }} +apiVersion: v1 +kind: Secret +metadata: + name: webhook-server-cert + namespace: {{ .Release.Namespace }} +type: Opaque +data: + tls.crt: {{ .Values.operator.admissionController.certificate.tlsCrt | b64enc | quote }} + tls.key: {{ .Values.operator.admissionController.certificate.tlsKey | b64enc | quote }} +{{- end }} diff --git a/deployment/network-operator/templates/mellanox.com_v1alpha1_nicclusterpolicy_cr.yaml b/deployment/network-operator/templates/mellanox.com_v1alpha1_nicclusterpolicy_cr.yaml index 4da2ac34..c71d9f75 100644 --- a/deployment/network-operator/templates/mellanox.com_v1alpha1_nicclusterpolicy_cr.yaml +++ b/deployment/network-operator/templates/mellanox.com_v1alpha1_nicclusterpolicy_cr.yaml @@ -18,6 +18,8 @@ apiVersion: mellanox.com/v1alpha1 kind: NicClusterPolicy metadata: name: nic-cluster-policy + annotations: + helm.sh/hook: post-install,post-upgrade spec: {{- if .Values.nodeAffinity }} nodeAffinity: diff --git a/deployment/network-operator/templates/operator.yaml b/deployment/network-operator/templates/operator.yaml index 15e415d5..618205b7 100644 --- a/deployment/network-operator/templates/operator.yaml +++ b/deployment/network-operator/templates/operator.yaml @@ -19,6 +19,7 @@ metadata: name: {{ include "network-operator.fullname" . }} labels: {{- include "network-operator.labels" . | nindent 4 }} + control-plane: {{ .Release.Name }}-controller namespace: {{ .Release.Namespace }} spec: replicas: 1 @@ -27,8 +28,11 @@ spec: {{- include "network-operator.selectorLabels" . | nindent 6 }} template: metadata: + annotations: + kubectl.kubernetes.io/default-container: {{ .Chart.Name }} labels: nvidia.com/ofed-driver-upgrade-drain.skip: "true" + control-plane: {{ .Release.Name }}-controller {{- include "network-operator.selectorLabels" . | nindent 8 }} spec: {{- with .Values.operator.nodeSelector }} @@ -49,6 +53,16 @@ spec: - name: {{ .Chart.Name }} # Replace this with the built image name image: "{{ .Values.operator.repository }}/{{ .Values.operator.image }}:{{ .Values.operator.tag | default .Chart.AppVersion }}" + {{- if .Values.operator.admissionController.enabled }} + ports: + - containerPort: 9443 + name: webhook-server + protocol: TCP + volumeMounts: + - mountPath: /tmp/k8s-webhook-server/serving-certs + name: cert + readOnly: true + {{- end }} command: - /manager env: @@ -66,3 +80,15 @@ spec: fieldPath: metadata.namespace - name: OPERATOR_NAME value: "network-operator" + - name: ENABLE_WEBHOOKS + value: "{{ .Values.operator.admissionController.enabled }}" + {{- if .Values.operator.admissionController.enabled }} + securityContext: + runAsUser: 65532 + terminationGracePeriodSeconds: 10 + volumes: + - name: cert + secret: + defaultMode: 420 + secretName: webhook-server-cert + {{- end }} diff --git a/deployment/network-operator/values.yaml b/deployment/network-operator/values.yaml index 33367eec..1925398e 100644 --- a/deployment/network-operator/values.yaml +++ b/deployment/network-operator/values.yaml @@ -144,6 +144,20 @@ operator: fullnameOverride: "" # tag, if defined will use the given image tag, else Chart.AppVersion will be used # tag + admissionController: + enabled: false + useCertManager: true + # certificate: + # tlsCrt: | + # -----BEGIN CERTIFICATE----- + # MIIMIICLDCCAdKgAwIBAgIBADAKBggqhkjOPQQDAjB9MQswCQYDVQQGEwJCRTEPMA0G + # ... + # -----END CERTIFICATE----- + # tlsKey: | + # -----BEGIN EC PRIVATE KEY----- + # MHcl4wOuDwKQa+upc8GftXE2C//4mKANBC6It01gUaTIpo= + # ... + # -----END EC PRIVATE KEY----- imagePullSecrets: [] diff --git a/go.mod b/go.mod index 24a91907..92e4aea3 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/openshift/api v0.0.0-20210428205234-a8389931bee7 github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.8.2 + github.com/xeipuuv/gojsonschema v1.2.0 k8s.io/api v0.26.4 k8s.io/apimachinery v0.26.4 k8s.io/client-go v0.26.4 @@ -73,6 +74,8 @@ require ( github.com/spf13/cobra v1.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/objx v0.5.0 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xlab/treeprint v1.1.0 // indirect go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect go.uber.org/atomic v1.7.0 // indirect diff --git a/go.sum b/go.sum index aa377196..a84af029 100644 --- a/go.sum +++ b/go.sum @@ -365,6 +365,12 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xlab/treeprint v1.1.0 h1:G/1DjNkPpfZCFt9CSh6b5/nY4VimlbHF3Rh4obvtzDk= github.com/xlab/treeprint v1.1.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/hack/templates/values/values.template b/hack/templates/values/values.template index 395694d1..173464a2 100644 --- a/hack/templates/values/values.template +++ b/hack/templates/values/values.template @@ -144,6 +144,20 @@ operator: fullnameOverride: "" # tag, if defined will use the given image tag, else Chart.AppVersion will be used # tag + admissionController: + enabled: false + useCertManager: true + # certificate: + # tlsCrt: | + # -----BEGIN CERTIFICATE----- + # MIIMIICLDCCAdKgAwIBAgIBADAKBggqhkjOPQQDAjB9MQswCQYDVQQGEwJCRTEPMA0G + # ... + # -----END CERTIFICATE----- + # tlsKey: | + # -----BEGIN EC PRIVATE KEY----- + # MHcl4wOuDwKQa+upc8GftXE2C//4mKANBC6It01gUaTIpo= + # ... + # -----END EC PRIVATE KEY----- imagePullSecrets: [] diff --git a/main.go b/main.go index 9d4fe232..26c31712 100644 --- a/main.go +++ b/main.go @@ -58,6 +58,19 @@ func init() { // +kubebuilder:scaffold:scheme } +func setupWebhookControllers(mgr ctrl.Manager) error { + if err := (&mellanoxcomv1alpha1.HostDeviceNetwork{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "HostDeviceNetwork") + return err + } + if err := (&mellanoxcomv1alpha1.NicClusterPolicy{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "NicClusterPolicy") + + return err + } + return nil +} + func setupCRDControllers(ctx context.Context, c client.Client, mgr ctrl.Manager) error { ctrLog := setupLog.WithName("controller") clusterTypeProvider, err := clustertype.NewProvider(ctx, c) @@ -173,6 +186,13 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "Upgrade") os.Exit(1) } + + if os.Getenv("ENABLE_WEBHOOKS") == "true" { + if err := setupWebhookControllers(mgr); err != nil { + os.Exit(1) + } + } + // +kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("health", healthz.Ping); err != nil { diff --git a/webhook-schemas/accelerator_selector.json b/webhook-schemas/accelerator_selector.json new file mode 100644 index 00000000..be7bd85a --- /dev/null +++ b/webhook-schemas/accelerator_selector.json @@ -0,0 +1,124 @@ +{ + "type": "object", + "properties": { + "selectors": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "object", + "properties": { + "vendors": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "devices": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "drivers": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "pciAddresses": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + }, + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "vendors" + ] + }, + { + "required": [ + "devices" + ] + }, + { + "required": [ + "drivers" + ] + }, + { + "required": [ + "pciAddresses" + ] + } + ] + } + }, + { + "type": "object", + "properties": { + "vendors": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "devices": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "drivers": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "pciAddresses": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + }, + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "vendors" + ] + }, + { + "required": [ + "devices" + ] + }, + { + "required": [ + "drivers" + ] + }, + { + "required": [ + "pciAddresses" + ] + } + ] + } + ] + } + } +} diff --git a/webhook-schemas/aux_net_device.json b/webhook-schemas/aux_net_device.json new file mode 100644 index 00000000..2fb0240a --- /dev/null +++ b/webhook-schemas/aux_net_device.json @@ -0,0 +1,212 @@ +{ + "type": "object", + "properties": { + "selectors": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "object", + "properties": { + "vendors": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "devices": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "drivers": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "pfNames": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "rootDevices": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "linkTypes": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "isRdma": { + "type": "boolean" + }, + "auxTypes": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + }, + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "vendors" + ] + }, + { + "required": [ + "devices" + ] + }, + { + "required": [ + "drivers" + ] + }, + { + "required": [ + "pfNames" + ] + }, + { + "required": [ + "rootDevices" + ] + }, + { + "required": [ + "linkTypes" + ] + }, + { + "required": [ + "isRdma" + ] + }, + { + "required": [ + "auxTypes" + ] + } + ] + } + }, + { + "type": "object", + "properties": { + "vendors": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "devices": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "drivers": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "pfNames": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "rootDevices": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "linkTypes": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "isRdma": { + "type": "boolean" + }, + "auxTypes": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + }, + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "vendors" + ] + }, + { + "required": [ + "devices" + ] + }, + { + "required": [ + "drivers" + ] + }, + { + "required": [ + "pfNames" + ] + }, + { + "required": [ + "rootDevices" + ] + }, + { + "required": [ + "linkTypes" + ] + }, + { + "required": [ + "isRdma" + ] + }, + { + "required": [ + "auxTypes" + ] + } + ] + } + ] + } + } +} diff --git a/webhook-schemas/net_device.json b/webhook-schemas/net_device.json new file mode 100644 index 00000000..2d3f025c --- /dev/null +++ b/webhook-schemas/net_device.json @@ -0,0 +1,268 @@ +{ + "type": "object", + "properties": { + "selectors": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "object", + "properties": { + "vendors": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "devices": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "drivers": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "pciAddresses": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "pfNames": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "rootDevices": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "linkTypes": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "ddpProfiles": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "isRdma": { + "type": "boolean" + }, + "needVhostNet": { + "type": "boolean" + }, + "vdpaType": { + "type": "string" + } + }, + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "vendors" + ] + }, + { + "required": [ + "devices" + ] + }, + { + "required": [ + "drivers" + ] + }, + { + "required": [ + "pciAddresses" + ] + }, + { + "required": [ + "pfNames" + ] + }, + { + "required": [ + "rootDevices" + ] + }, + { + "required": [ + "linkTypes" + ] + }, + { + "required": [ + "ddpProfiles" + ] + }, + { + "required": [ + "isRdma" + ] + }, + { + "required": [ + "needVhostNet" + ] + }, + { + "required": [ + "vdpaType" + ] + } + ] + } + }, + { + "type": "object", + "properties": { + "vendors": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "devices": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "drivers": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "pciAddresses": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "pfNames": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "rootDevices": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "linkTypes": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "ddpProfiles": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "isRdma": { + "type": "boolean" + }, + "needVhostNet": { + "type": "boolean" + }, + "vdpaType": { + "type": "string" + } + }, + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "vendors" + ] + }, + { + "required": [ + "devices" + ] + }, + { + "required": [ + "drivers" + ] + }, + { + "required": [ + "pciAddresses" + ] + }, + { + "required": [ + "pfNames" + ] + }, + { + "required": [ + "rootDevices" + ] + }, + { + "required": [ + "linkTypes" + ] + }, + { + "required": [ + "ddpProfiles" + ] + }, + { + "required": [ + "isRdma" + ] + }, + { + "required": [ + "needVhostNet" + ] + }, + { + "required": [ + "vdpaType" + ] + } + ] + } + ] + } + } +} diff --git a/webhook-schemas/rdma_shared_device_plugin.json b/webhook-schemas/rdma_shared_device_plugin.json new file mode 100644 index 00000000..fc15e617 --- /dev/null +++ b/webhook-schemas/rdma_shared_device_plugin.json @@ -0,0 +1,117 @@ +{ + "type": "object", + "properties": { + "configList": { + "type": "array", + "items": { + "type": "object", + "properties": { + "resourceName": { + "type": "string" + }, + "resourcePrefix": { + "type": "string" + }, + "rdmaHcaMax": { + "type": "integer" + }, + "devices": { + "type": "array", + "minLength": 1, + "items": { + "type": "string", + "minLength": 1 + } + }, + "selectors": { + "type": "object", + "properties": { + "vendors": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "deviceIDs": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "drivers": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "ifNames": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "linkTypes": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + }, + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "vendors" + ] + }, + { + "required": [ + "deviceIDs" + ] + }, + { + "required": [ + "drivers" + ] + }, + { + "required": [ + "ifNames" + ] + }, + { + "required": [ + "linkTypes" + ] + } + ] + } + }, + "anyOf": [ + { + "required": [ + "selectors" + ] + }, + { + "required": [ + "devices" + ] + } + ], + "required": [ + "resourceName", + "rdmaHcaMax" + ] + } + } + }, + "required": [ + "configList" + ] +} diff --git a/webhook-schemas/sriov_network_device_plugin.json b/webhook-schemas/sriov_network_device_plugin.json new file mode 100644 index 00000000..4d8a83f6 --- /dev/null +++ b/webhook-schemas/sriov_network_device_plugin.json @@ -0,0 +1,49 @@ +{ + "type": "object", + "properties": { + "resourceList": { + "type": "array", + "items": { + "type": "object", + "properties": { + "resourceName": { + "type": "string" + }, + "resourcePrefix": { + "type": "string" + }, + "deviceType": { + "type": "string", + "enum": [ + "accelerator", + "netDevice", + "auxNetDevice" + ] + }, + "excludeTopology": { + "type": "boolean" + }, + "selectors": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "object" + } + }, + { + "type": "object" + } + ] + } + } + }, + "required": [ + "resourceName" + ] + } + }, + "required": [ + "resourceList" + ] +}