diff --git a/Makefile b/Makefile index d7fdd3004..a73ec0260 100644 --- a/Makefile +++ b/Makefile @@ -183,6 +183,7 @@ uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified deploy: update-mgr-env manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} && $(KUSTOMIZE) edit set nameprefix ${MANAGER_NAME_PREFIX} cd config/default && $(KUSTOMIZE) edit set image rbac-proxy=$(RBAC_PROXY_IMG) + cd config/webhook && $(KUSTOMIZE) edit set nameprefix ${MANAGER_NAME_PREFIX} $(KUSTOMIZE) build config/default | kubectl apply -f - undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. @@ -232,6 +233,7 @@ bundle: update-mgr-env manifests kustomize operator-sdk rename-csv build-prometh rm -rf bundle # $(OPERATOR_SDK) generate kustomize manifests --package $(BUNDLE_PACKAGE) -q cd config/default && $(KUSTOMIZE) edit set namespace $(OPERATOR_NAMESPACE) + cd config/webhook && $(KUSTOMIZE) edit set nameprefix ${MANAGER_NAME_PREFIX} cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} && $(KUSTOMIZE) edit set nameprefix ${MANAGER_NAME_PREFIX} cd config/default && $(KUSTOMIZE) edit set image rbac-proxy=$(RBAC_PROXY_IMG) $(KUSTOMIZE) build config/manifests | $(OPERATOR_SDK) generate bundle -q --package $(BUNDLE_PACKAGE) --version $(VERSION) $(BUNDLE_METADATA_OPTS) diff --git a/PROJECT b/PROJECT index 3d789566a..a46588799 100644 --- a/PROJECT +++ b/PROJECT @@ -16,6 +16,9 @@ resources: kind: LVMCluster path: github.com/red-hat-storage/lvm-operator/api/v1alpha1 version: v1alpha1 + webhooks: + validation: true + webhookVersion: v1 - api: crdVersion: v1 namespaced: true diff --git a/api/v1alpha1/lvmcluster_webhook.go b/api/v1alpha1/lvmcluster_webhook.go new file mode 100644 index 000000000..58f23f539 --- /dev/null +++ b/api/v1alpha1/lvmcluster_webhook.go @@ -0,0 +1,266 @@ +/* +Copyright 2022 Red Hat Openshift Data Foundation. + +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 ( + "fmt" + "strings" + + "k8s.io/apimachinery/pkg/runtime" + 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 lvmclusterlog = logf.Log.WithName("lvmcluster-webhook") + +var _ webhook.Validator = &LVMCluster{} + +var ( + ErrDeviceClassNotFound = fmt.Errorf("DeviceClass not found in the LVMCluster") + ErrThinPoolConfigNotSet = fmt.Errorf("ThinPoolConfig is not set for the DeviceClass") +) + +//+kubebuilder:webhook:path=/validate-lvm-topolvm-io-v1alpha1-lvmcluster,mutating=false,failurePolicy=fail,sideEffects=None,groups=lvm.topolvm.io,resources=lvmclusters,verbs=create;update,versions=v1alpha1,name=vlvmcluster.kb.io,admissionReviewVersions=v1 + +func (l *LVMCluster) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(l). + Complete() +} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (l *LVMCluster) ValidateCreate() error { + lvmclusterlog.Info("validate create", "name", l.Name) + + if len(l.Spec.Storage.DeviceClasses) != 1 { + return fmt.Errorf("Exactly one deviceClass is allowed") + } + + err := l.verifyPathsAreNotEmpty() + if err != nil { + return err + } + + err = l.verifyAbsolutePath() + if err != nil { + return err + } + + err = l.verifyNoDeviceOverlap() + if err != nil { + return err + } + + return nil +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (l *LVMCluster) ValidateUpdate(old runtime.Object) error { + lvmclusterlog.Info("validate update", "name", l.Name) + + if len(l.Spec.Storage.DeviceClasses) != 1 { + return fmt.Errorf("Exactly one deviceClass is allowed") + } + + err := l.verifyPathsAreNotEmpty() + if err != nil { + return err + } + + err = l.verifyAbsolutePath() + if err != nil { + return err + } + + err = l.verifyNoDeviceOverlap() + if err != nil { + return err + } + + oldLVMCluster, ok := old.(*LVMCluster) + if !ok { + return fmt.Errorf("Failed to parse LVMCluster.") + } + + for _, deviceClass := range l.Spec.Storage.DeviceClasses { + var newThinPoolConfig, oldThinPoolConfig *ThinPoolConfig + var newDevices, oldDevices []string + + newThinPoolConfig = deviceClass.ThinPoolConfig + oldThinPoolConfig, err = oldLVMCluster.getThinPoolsConfigOfDeviceClass(deviceClass.Name) + + if (newThinPoolConfig != nil && oldThinPoolConfig == nil && err != ErrDeviceClassNotFound) || + (newThinPoolConfig == nil && oldThinPoolConfig != nil) { + return fmt.Errorf("ThinPoolConfig can not be changed") + } + + if newThinPoolConfig != nil && oldThinPoolConfig != nil { + if newThinPoolConfig.Name != oldThinPoolConfig.Name { + return fmt.Errorf("ThinPoolConfig.Name can not be changed") + } else if newThinPoolConfig.SizePercent != oldThinPoolConfig.SizePercent { + return fmt.Errorf("ThinPoolConfig.SizePercent can not be changed") + } else if newThinPoolConfig.OverprovisionRatio != oldThinPoolConfig.OverprovisionRatio { + return fmt.Errorf("ThinPoolConfig.OverprovisionRatio can not be changed") + } + } + + if deviceClass.DeviceSelector != nil { + newDevices = deviceClass.DeviceSelector.Paths + } + + oldDevices, err = oldLVMCluster.getPathsOfDeviceClass(deviceClass.Name) + + // if devices are removed now + if len(oldDevices) > len(newDevices) { + return fmt.Errorf("Invalid:devices can not be removed from the LVMCluster once added.") + } + + // if devices are added now + if len(oldDevices) == 0 && len(newDevices) > 0 && err != ErrDeviceClassNotFound { + return fmt.Errorf("Invalid:devices can not be added in the LVMCluster once created without devices.") + } + + deviceMap := make(map[string]bool) + + for _, device := range oldDevices { + deviceMap[device] = true + } + + for _, device := range newDevices { + delete(deviceMap, device) + } + + // if any old device is removed now + if len(deviceMap) != 0 { + return fmt.Errorf("Invalid:some of devices are deleted from the LVMCluster. "+ + "Device can not be removed from the LVMCluster once added. "+ + "oldDevices:%s, newDevices:%s", oldDevices, newDevices) + } + } + + return nil +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (l *LVMCluster) ValidateDelete() error { + lvmclusterlog.Info("validate delete", "name", l.Name) + + return nil +} + +func (l *LVMCluster) verifyPathsAreNotEmpty() error { + + for _, deviceClass := range l.Spec.Storage.DeviceClasses { + if deviceClass.DeviceSelector != nil { + if len(deviceClass.DeviceSelector.Paths) == 0 { + return fmt.Errorf("Path list should not be empty when DeviceSelector is specified.") + } + } + } + + return nil +} + +func (l *LVMCluster) verifyAbsolutePath() error { + + for _, deviceClass := range l.Spec.Storage.DeviceClasses { + if deviceClass.DeviceSelector != nil { + for _, path := range deviceClass.DeviceSelector.Paths { + if !strings.HasPrefix(path, "/dev/") { + return fmt.Errorf("Given path %s is not an absolute path. "+ + "Please provide the absolute path to the device", path) + } + } + } + } + + return nil +} + +func (l *LVMCluster) verifyNoDeviceOverlap() error { + + // make sure no device overlap with another VGs + // use map to find the duplicate entries for paths + /* + { + "nodeSelector1": { + "/dev/sda": "vg1", + "/dev/sdb": "vg1" + }, + "nodeSelector2": { + "/dev/sda": "vg1", + "/dev/sdb": "vg1" + } + } + */ + devices := make(map[string]map[string]string) + + for _, deviceClass := range l.Spec.Storage.DeviceClasses { + if deviceClass.DeviceSelector != nil { + nodeSelector := deviceClass.NodeSelector.String() + for _, path := range deviceClass.DeviceSelector.Paths { + if val, ok := devices[nodeSelector][path]; ok { + var err error + if val != deviceClass.Name { + err = fmt.Errorf("Error: device path %s overlaps in two different deviceClasss %s and %s", path, val, deviceClass.Name) + } else { + err = fmt.Errorf("Error: device path %s is specified at multiple places in deviceClass %s", path, val) + } + return err + } + + if devices[nodeSelector] == nil { + devices[nodeSelector] = make(map[string]string) + } + + devices[nodeSelector][path] = deviceClass.Name + } + } + } + + return nil +} + +func (l *LVMCluster) getPathsOfDeviceClass(deviceClassName string) ([]string, error) { + + for _, deviceClass := range l.Spec.Storage.DeviceClasses { + if deviceClass.Name == deviceClassName { + if deviceClass.DeviceSelector != nil { + return deviceClass.DeviceSelector.Paths, nil + } + return []string{}, nil + } + } + + return []string{}, ErrDeviceClassNotFound +} + +func (l *LVMCluster) getThinPoolsConfigOfDeviceClass(deviceClassName string) (*ThinPoolConfig, error) { + + for _, deviceClass := range l.Spec.Storage.DeviceClasses { + if deviceClass.Name == deviceClassName { + if deviceClass.ThinPoolConfig != nil { + return deviceClass.ThinPoolConfig, nil + } + return nil, ErrThinPoolConfigNotSet + } + } + + return nil, ErrDeviceClassNotFound +} diff --git a/api/v1alpha1/webhook_suite_test.go b/api/v1alpha1/webhook_suite_test.go new file mode 100644 index 000000000..32f12f2e2 --- /dev/null +++ b/api/v1alpha1/webhook_suite_test.go @@ -0,0 +1,135 @@ +/* +Copyright 2022 Red Hat Openshift Data Foundation. + +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 ( + "context" + "crypto/tls" + "fmt" + "net" + "path/filepath" + "testing" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + admissionv1beta1 "k8s.io/api/admission/v1beta1" + //+kubebuilder:scaffold:imports + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/rest" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + "sigs.k8s.io/controller-runtime/pkg/envtest/printer" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +var cfg *rest.Config +var k8sClient client.Client +var testEnv *envtest.Environment +var ctx context.Context +var cancel context.CancelFunc + +func TestAPIs(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecsWithDefaultAndCustomReporters(t, + "Webhook Suite", + []Reporter{printer.NewlineReporter{}}) +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + ctx, cancel = context.WithCancel(context.TODO()) + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: false, + WebhookInstallOptions: envtest.WebhookInstallOptions{ + Paths: []string{filepath.Join("..", "..", "config", "webhook")}, + }, + } + + var err error + // cfg is defined in this file globally. + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + scheme := runtime.NewScheme() + err = AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + err = admissionv1beta1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + //+kubebuilder:scaffold:scheme + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) + + // start webhook server using Manager + webhookInstallOptions := &testEnv.WebhookInstallOptions + mgr, err := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: scheme, + Host: webhookInstallOptions.LocalServingHost, + Port: webhookInstallOptions.LocalServingPort, + CertDir: webhookInstallOptions.LocalServingCertDir, + LeaderElection: false, + MetricsBindAddress: "0", + }) + Expect(err).NotTo(HaveOccurred()) + + err = (&LVMCluster{}).SetupWebhookWithManager(mgr) + Expect(err).NotTo(HaveOccurred()) + + //+kubebuilder:scaffold:webhook + + go func() { + defer GinkgoRecover() + err = mgr.Start(ctx) + Expect(err).NotTo(HaveOccurred()) + }() + + // wait for the webhook server to get ready + dialer := &net.Dialer{Timeout: time.Second} + addrPort := fmt.Sprintf("%s:%d", webhookInstallOptions.LocalServingHost, webhookInstallOptions.LocalServingPort) + Eventually(func() error { + conn, err := tls.DialWithDialer(dialer, "tcp", addrPort, &tls.Config{InsecureSkipVerify: true}) + if err != nil { + return err + } + conn.Close() + return nil + }).Should(Succeed()) + +}, 60) + +var _ = AfterSuite(func() { + cancel() + By("tearing down the test environment") + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +}) diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 74dd50ac8..372e7ec51 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -2,7 +2,7 @@ // +build !ignore_autogenerated /* -Copyright 2021 Red Hat Openshift Data Foundation. +Copyright 2022 Red Hat Openshift Data Foundation. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -24,7 +24,7 @@ package v1alpha1 import ( conditionsv1 "github.com/openshift/custom-resource-status/conditions/v1" "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/bundle/manifests/lvm-operator-webhook-service_v1_service.yaml b/bundle/manifests/lvm-operator-webhook-service_v1_service.yaml new file mode 100644 index 000000000..d5148f996 --- /dev/null +++ b/bundle/manifests/lvm-operator-webhook-service_v1_service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + annotations: + service.beta.openshift.io/serving-cert-secret-name: lvm-operator-webhook-server-cert + creationTimestamp: null + name: lvm-operator-webhook-service +spec: + ports: + - port: 443 + protocol: TCP + targetPort: 9443 + selector: + app.kubernetes.io/name: lvm-operator +status: + loadBalancer: {} diff --git a/bundle/manifests/lvm-operator.clusterserviceversion.yaml b/bundle/manifests/lvm-operator.clusterserviceversion.yaml index 59acde771..e1c0dfc96 100644 --- a/bundle/manifests/lvm-operator.clusterserviceversion.yaml +++ b/bundle/manifests/lvm-operator.clusterserviceversion.yaml @@ -276,18 +276,6 @@ spec: exporter: lvm-operator spec: containers: - - args: - - --secure-listen-address=0.0.0.0:8443 - - --upstream=http://127.0.0.1:8080/ - - --logtostderr=true - - --v=10 - image: gcr.io/kubebuilder/kube-rbac-proxy:v0.8.0 - name: kube-rbac-proxy - ports: - - containerPort: 8443 - name: https - protocol: TCP - resources: {} - args: - --health-probe-bind-address=:8081 - --metrics-bind-address=127.0.0.1:8080 @@ -323,6 +311,10 @@ spec: initialDelaySeconds: 15 periodSeconds: 20 name: manager + ports: + - containerPort: 9443 + name: webhook-server + protocol: TCP readinessProbe: httpGet: path: /readyz @@ -338,6 +330,22 @@ spec: memory: 50Mi securityContext: allowPrivilegeEscalation: false + volumeMounts: + - mountPath: /tmp/k8s-webhook-server/serving-certs + name: cert + readOnly: true + - args: + - --secure-listen-address=0.0.0.0:8443 + - --upstream=http://127.0.0.1:8080/ + - --logtostderr=true + - --v=10 + image: gcr.io/kubebuilder/kube-rbac-proxy:v0.8.0 + name: kube-rbac-proxy + ports: + - containerPort: 8443 + name: https + protocol: TCP + resources: {} - command: - /metricsexporter image: quay.io/ocs-dev/lvm-operator:latest @@ -353,6 +361,11 @@ spec: runAsNonRoot: true serviceAccountName: lvm-controller-manager terminationGracePeriodSeconds: 10 + volumes: + - name: cert + secret: + defaultMode: 420 + secretName: lvm-operator-webhook-server-cert permissions: - rules: - apiGroups: @@ -411,3 +424,24 @@ spec: provider: name: Red Hat version: 0.0.1 + webhookdefinitions: + - admissionReviewVersions: + - v1 + containerPort: 443 + deploymentName: lvm-operator-controller-manager + failurePolicy: Fail + generateName: vlvmcluster.kb.io + rules: + - apiGroups: + - lvm.topolvm.io + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - lvmclusters + sideEffects: None + targetPort: 9443 + type: ValidatingAdmissionWebhook + webhookPath: /validate-lvm-topolvm-io-v1alpha1-lvmcluster diff --git a/config/certmanager/certificate.yaml b/config/certmanager/certificate.yaml new file mode 100644 index 000000000..52d866183 --- /dev/null +++ b/config/certmanager/certificate.yaml @@ -0,0 +1,25 @@ +# 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: + name: selfsigned-issuer + namespace: system +spec: + selfSigned: {} +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + 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 000000000..bebea5a59 --- /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 000000000..e631f7773 --- /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/default/kustomization.yaml b/config/default/kustomization.yaml index 9dfa92b46..114a3afb7 100644 --- a/config/default/kustomization.yaml +++ b/config/default/kustomization.yaml @@ -24,6 +24,8 @@ namespace: lvm-operator-system patchesStrategicMerge: - manager_auth_proxy_patch.yaml - manager_custom_env.yaml +- manager_webhook_patch.yaml +- webhookcainjection_patch.yaml # Mount the controller config file for loading manager configurations # through a ComponentConfig type @@ -46,6 +48,7 @@ resources: - ../rbac - ../manager - ../prometheus +- ../webhook images: - name: rbac-proxy newName: gcr.io/kubebuilder/kube-rbac-proxy diff --git a/config/default/manager_webhook_patch.yaml b/config/default/manager_webhook_patch.yaml new file mode 100644 index 000000000..88b6b5924 --- /dev/null +++ b/config/default/manager_webhook_patch.yaml @@ -0,0 +1,23 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: manager + 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: lvm-operator-webhook-server-cert diff --git a/config/default/webhookcainjection_patch.yaml b/config/default/webhookcainjection_patch.yaml new file mode 100644 index 000000000..161c21967 --- /dev/null +++ b/config/default/webhookcainjection_patch.yaml @@ -0,0 +1,17 @@ +# 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: MutatingWebhookConfiguration +#metadata: +# name: mutating-webhook-configuration +# annotations: +# cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + name: validating-webhook-configuration + annotations: + #cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + service.beta.openshift.io/inject-cabundle: "true" diff --git a/config/webhook/kustomization.yaml b/config/webhook/kustomization.yaml new file mode 100644 index 000000000..40b9a143f --- /dev/null +++ b/config/webhook/kustomization.yaml @@ -0,0 +1,11 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namePrefix: lvm-operator- + +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 000000000..25e21e3c9 --- /dev/null +++ b/config/webhook/kustomizeconfig.yaml @@ -0,0 +1,25 @@ +# 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: MutatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/name + - kind: ValidatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/name + +namespace: +- kind: MutatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/namespace + create: true +- 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 000000000..90d43e4bc --- /dev/null +++ b/config/webhook/manifests.yaml @@ -0,0 +1,28 @@ + +--- +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-lvm-topolvm-io-v1alpha1-lvmcluster + failurePolicy: Fail + name: vlvmcluster.kb.io + rules: + - apiGroups: + - lvm.topolvm.io + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - lvmclusters + sideEffects: None diff --git a/config/webhook/service.yaml b/config/webhook/service.yaml new file mode 100644 index 000000000..d045722af --- /dev/null +++ b/config/webhook/service.yaml @@ -0,0 +1,15 @@ + +apiVersion: v1 +kind: Service +metadata: + annotations: + service.beta.openshift.io/serving-cert-secret-name: lvm-operator-webhook-server-cert + name: webhook-service + namespace: system +spec: + ports: + - port: 443 + protocol: TCP + targetPort: 9443 + selector: + app.kubernetes.io/name: lvm-operator diff --git a/hack/boilerplate.go.txt b/hack/boilerplate.go.txt index 45dbbbbcf..7914acc2f 100644 --- a/hack/boilerplate.go.txt +++ b/hack/boilerplate.go.txt @@ -1,5 +1,5 @@ /* -Copyright 2021. +Copyright 2022 Red Hat Openshift Data Foundation. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -12,4 +12,4 @@ 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. -*/ \ No newline at end of file +*/ diff --git a/main.go b/main.go index bc1dde721..b62d8cd35 100644 --- a/main.go +++ b/main.go @@ -34,6 +34,7 @@ import ( snapapi "github.com/kubernetes-csi/external-snapshotter/client/v4/apis/volumesnapshot/v1" secv1client "github.com/openshift/client-go/security/clientset/versioned/typed/security/v1" + lvmv1alpha1 "github.com/red-hat-storage/lvm-operator/api/v1alpha1" "github.com/red-hat-storage/lvm-operator/controllers" topolvmv1 "github.com/topolvm/topolvm/api/v1" @@ -102,6 +103,10 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "LVMCluster") os.Exit(1) } + if err = (&lvmv1alpha1.LVMCluster{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "LVMCluster") + os.Exit(1) + } //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {