diff --git a/Dockerfile b/Dockerfile index b86ba06..2a8fe00 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,7 @@ COPY go.sum go.sum RUN go mod download COPY cmd/main.go cmd/main.go -COPY internal/controller/ internal/controller/ +COPY internal/ internal/ RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go diff --git a/Makefile b/Makefile index 778810e..c4da67a 100644 --- a/Makefile +++ b/Makefile @@ -243,26 +243,31 @@ mv $(1) $(1)-$(3) ;\ ln -sf $(1)-$(3) $(1) endef -HELM_DEPENDS ?= commons-operator listener-operator secret-operator TEST_NAMESPACE = kubedoop-operators +HELM_DEPENDS ?= "" + .PHONY: helm-install-depends helm-install-depends: helm ## Install the helm chart depends. - $(HELM) repo add kubedoop https://zncdatadev.github.io/kubedoop-helm-charts/ -ifneq ($(strip $(HELM_DEPENDS)),) - for dep in $(HELM_DEPENDS); do \ - $(HELM) upgrade --install --create-namespace --namespace $(TEST_NAMESPACE) --wait $$dep kubedoop/$$dep --version $(VERSION); \ - done -endif + # install cert-manage + KUBECONFIG=$(KIND_KUBECONFIG) kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.16.2/cert-manager.yaml + @if [ ! -z "$(strip $(HELM_DEPENDS))" ]; then \ + $(HELM) repo add kubedoop https://zncdatadev.github.io/kubedoop-helm-charts/; \ + for dep in $(HELM_DEPENDS); do \ + $(HELM) upgrade --install --create-namespace --namespace $(TEST_NAMESPACE) --wait $$dep kubedoop/$$dep --version $(VERSION); \ + done; \ + fi ## helm uninstall depends .PHONY: helm-uninstall-depends helm-uninstall-depends: helm ## Uninstall the helm chart depends. -ifneq ($(strip $(HELM_DEPENDS)),) - for dep in $(HELM_DEPENDS); do \ - $(HELM) uninstall --namespace $(TEST_NAMESPACE) $$dep; \ - done -endif + # uninstall cert-manage + KUBECONFIG=$(KIND_KUBECONFIG) kubectl delete -f https://github.com/cert-manager/cert-manager/releases/download/v1.16.2/cert-manager.yaml + @if [ ! -z "$(strip $(HELM_DEPENDS))" ]; then \ + for dep in $(HELM_DEPENDS); do \ + $(HELM) uninstall --namespace $(TEST_NAMESPACE) $$dep; \ + done; \ + fi ##@ Chainsaw-E2E @@ -313,7 +318,7 @@ $(CHAINSAW): $(LOCALBIN) } .PHONY: chainsaw-setup -chainsaw-setup: docker-build ## Run the chainsaw setup +chainsaw-setup: docker-build helm-install-depends ## Run the chainsaw setup $(KIND) --name $(KIND_CLUSTER) load docker-image $(IMG) KUBECONFIG=$(KIND_KUBECONFIG) $(MAKE) deploy diff --git a/PROJECT b/PROJECT index 89bdeed..c458ade 100644 --- a/PROJECT +++ b/PROJECT @@ -16,8 +16,13 @@ resources: domain: kubedoop.dev group: authentication kind: AuthenticationClass - path: github.com/zncdatadev/commons-operator/api/authentication/v1alpha1 + path: github.com/zncdatadev/operator-go/pkg/apis/authentication/v1alpha1 + # path: github.com/zncdatadev/commons-operator/api/authentication/v1alpha1 version: v1alpha1 + webhooks: + defaulting: true + validation: true + webhookVersion: v1 - api: crdVersion: v1 namespaced: true @@ -25,8 +30,13 @@ resources: domain: kubedoop.dev group: s3 kind: S3Connection - path: github.com/zncdatadev/commons-operator/api/s3/v1alpha1 + path: github.com/zncdatadev/operator-go/pkg/apis/s3/v1alpha1 + # path: github.com/zncdatadev/commons-operator/api/s3/v1alpha1 version: v1alpha1 + webhooks: + defaulting: true + validation: true + webhookVersion: v1 - api: crdVersion: v1 namespaced: true @@ -34,6 +44,11 @@ resources: domain: kubedoop.dev group: s3 kind: S3Bucket - path: github.com/zncdatadev/commons-operator/api/s3/v1alpha1 + path: github.com/zncdatadev/operator-go/pkg/apis/s3/v1alpha1 + # path: github.com/zncdatadev/commons-operator/api/s3/v1alpha1 version: v1alpha1 + webhooks: + defaulting: true + validation: true + webhookVersion: v1 version: "3" diff --git a/cmd/main.go b/cmd/main.go index 5a09b0d..5b0ce2a 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -25,6 +25,8 @@ import ( // to ensure that exec-entrypoint and run can make use of them. _ "k8s.io/client-go/plugin/pkg/client/auth" + authenticationv1alpha1 "github.com/zncdatadev/operator-go/pkg/apis/authentication/v1alpha1" + s3v1alpha1 "github.com/zncdatadev/operator-go/pkg/apis/s3/v1alpha1" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" @@ -37,13 +39,9 @@ import ( "github.com/zncdatadev/commons-operator/internal/controller/pod_enrichment" "github.com/zncdatadev/commons-operator/internal/controller/restart" - + webhookauthenticationv1alpha1 "github.com/zncdatadev/commons-operator/internal/webhook/authentication/v1alpha1" + webhooks3v1alpha1 "github.com/zncdatadev/commons-operator/internal/webhook/s3/v1alpha1" // +kubebuilder:scaffold:imports - - // Now we need to import the constants package to fix olm bundle generate error - // later refactoring with constants feature, we will use it. - _ "github.com/zncdatadev/operator-go/pkg/apis/authentication/v1alpha1" - _ "github.com/zncdatadev/operator-go/pkg/apis/s3/v1alpha1" ) var ( @@ -53,6 +51,9 @@ var ( func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + utilruntime.Must(authenticationv1alpha1.AddToScheme(scheme)) + utilruntime.Must(s3v1alpha1.AddToScheme(scheme)) + // +kubebuilder:scaffold:scheme } @@ -171,6 +172,27 @@ func main() { os.Exit(1) } + // nolint:goconst + if os.Getenv("ENABLE_WEBHOOKS") != "false" { + if err = webhookauthenticationv1alpha1.SetupAuthenticationClassWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "AuthenticationClass") + os.Exit(1) + } + } + // nolint:goconst + if os.Getenv("ENABLE_WEBHOOKS") != "false" { + if err = webhooks3v1alpha1.SetupS3ConnectionWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "S3Connection") + os.Exit(1) + } + } + // nolint:goconst + if os.Getenv("ENABLE_WEBHOOKS") != "false" { + if err = webhooks3v1alpha1.SetupS3BucketWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "S3Bucket") + os.Exit(1) + } + } // +kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { setupLog.Error(err, "unable to set up health check") diff --git a/config/certmanager/certificate.yaml b/config/certmanager/certificate.yaml new file mode 100644 index 0000000..d2fec0f --- /dev/null +++ b/config/certmanager/certificate.yaml @@ -0,0 +1,35 @@ +# 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: commons-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: commons-operator + app.kubernetes.io/part-of: commons-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 0000000..bebea5a --- /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 0000000..cf6f89e --- /dev/null +++ b/config/certmanager/kustomizeconfig.yaml @@ -0,0 +1,8 @@ +# This configuration is for teaching kustomize how to update name ref substitution +nameReference: +- kind: Issuer + group: cert-manager.io + fieldSpecs: + - kind: Certificate + group: cert-manager.io + path: spec/issuerRef/name diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml index ab7854a..4e84e5d 100644 --- a/config/default/kustomization.yaml +++ b/config/default/kustomization.yaml @@ -20,9 +20,9 @@ resources: - ../manager # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # crd/kustomization.yaml -#- ../webhook +- ../webhook # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. -#- ../certmanager +- ../certmanager # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. #- ../prometheus # [METRICS] Expose the controller manager metrics service. @@ -43,135 +43,135 @@ patches: # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # crd/kustomization.yaml -#- path: manager_webhook_patch.yaml +- path: manager_webhook_patch.yaml # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. # Uncomment the following replacements to add the cert-manager CA injection annotations -#replacements: -# - source: # Uncomment the following block if you have any webhook -# kind: Service -# version: v1 -# name: webhook-service -# fieldPath: .metadata.name # Name of the service -# targets: -# - select: -# kind: Certificate -# group: cert-manager.io -# version: v1 -# fieldPaths: -# - .spec.dnsNames.0 -# - .spec.dnsNames.1 -# options: -# delimiter: '.' -# index: 0 -# create: true -# - source: -# kind: Service -# version: v1 -# name: webhook-service -# fieldPath: .metadata.namespace # Namespace of the service -# targets: -# - select: -# kind: Certificate -# group: cert-manager.io -# version: v1 -# fieldPaths: -# - .spec.dnsNames.0 -# - .spec.dnsNames.1 -# options: -# delimiter: '.' -# index: 1 -# create: true -# -# - source: # Uncomment the following block if you have a ValidatingWebhook (--programmatic-validation) -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert # This name should match the one in certificate.yaml -# fieldPath: .metadata.namespace # Namespace of the certificate CR -# targets: -# - select: -# kind: ValidatingWebhookConfiguration -# fieldPaths: -# - .metadata.annotations.[cert-manager.io/inject-ca-from] -# options: -# delimiter: '/' -# index: 0 -# create: true -# - source: -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert # This name should match the one in certificate.yaml -# fieldPath: .metadata.name -# targets: -# - select: -# kind: ValidatingWebhookConfiguration -# fieldPaths: -# - .metadata.annotations.[cert-manager.io/inject-ca-from] -# options: -# delimiter: '/' -# index: 1 -# create: true -# -# - source: # Uncomment the following block if you have a DefaultingWebhook (--defaulting ) -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert # This name should match the one in certificate.yaml -# fieldPath: .metadata.namespace # Namespace of the certificate CR -# targets: -# - select: -# kind: MutatingWebhookConfiguration -# fieldPaths: -# - .metadata.annotations.[cert-manager.io/inject-ca-from] -# options: -# delimiter: '/' -# index: 0 -# create: true -# - source: -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert # This name should match the one in certificate.yaml -# fieldPath: .metadata.name -# targets: -# - select: -# kind: MutatingWebhookConfiguration -# fieldPaths: -# - .metadata.annotations.[cert-manager.io/inject-ca-from] -# options: -# delimiter: '/' -# index: 1 -# create: true -# -# - source: # Uncomment the following block if you have a ConversionWebhook (--conversion) -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert # This name should match the one in certificate.yaml -# fieldPath: .metadata.namespace # Namespace of the certificate CR -# targets: -# - select: -# kind: CustomResourceDefinition -# fieldPaths: -# - .metadata.annotations.[cert-manager.io/inject-ca-from] -# options: -# delimiter: '/' -# index: 0 -# create: true -# - source: -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert # This name should match the one in certificate.yaml -# fieldPath: .metadata.name -# targets: -# - select: -# kind: CustomResourceDefinition -# fieldPaths: -# - .metadata.annotations.[cert-manager.io/inject-ca-from] -# options: -# delimiter: '/' -# index: 1 -# create: true +replacements: +- source: # Uncomment the following block if you have any webhook + kind: Service + version: v1 + name: webhook-service + fieldPath: .metadata.name # Name of the service + targets: + - select: + kind: Certificate + group: cert-manager.io + version: v1 + fieldPaths: + - .spec.dnsNames.0 + - .spec.dnsNames.1 + options: + delimiter: '.' + index: 0 + create: true +- source: + kind: Service + version: v1 + name: webhook-service + fieldPath: .metadata.namespace # Namespace of the service + targets: + - select: + kind: Certificate + group: cert-manager.io + version: v1 + fieldPaths: + - .spec.dnsNames.0 + - .spec.dnsNames.1 + options: + delimiter: '.' + index: 1 + create: true + +- source: # Uncomment the following block if you have a ValidatingWebhook (--programmatic-validation) + kind: Certificate + group: cert-manager.io + version: v1 + name: serving-cert # This name should match the one in certificate.yaml + fieldPath: .metadata.namespace # Namespace of the certificate CR + targets: + - select: + kind: ValidatingWebhookConfiguration + fieldPaths: + - .metadata.annotations.[cert-manager.io/inject-ca-from] + options: + delimiter: '/' + index: 0 + create: true +- source: + kind: Certificate + group: cert-manager.io + version: v1 + name: serving-cert # This name should match the one in certificate.yaml + fieldPath: .metadata.name + targets: + - select: + kind: ValidatingWebhookConfiguration + fieldPaths: + - .metadata.annotations.[cert-manager.io/inject-ca-from] + options: + delimiter: '/' + index: 1 + create: true + +- source: # Uncomment the following block if you have a DefaultingWebhook (--defaulting ) + kind: Certificate + group: cert-manager.io + version: v1 + name: serving-cert # This name should match the one in certificate.yaml + fieldPath: .metadata.namespace # Namespace of the certificate CR + targets: + - select: + kind: MutatingWebhookConfiguration + fieldPaths: + - .metadata.annotations.[cert-manager.io/inject-ca-from] + options: + delimiter: '/' + index: 0 + create: true +- source: + kind: Certificate + group: cert-manager.io + version: v1 + name: serving-cert # This name should match the one in certificate.yaml + fieldPath: .metadata.name + targets: + - select: + kind: MutatingWebhookConfiguration + fieldPaths: + - .metadata.annotations.[cert-manager.io/inject-ca-from] + options: + delimiter: '/' + index: 1 + create: true + +- source: # Uncomment the following block if you have a ConversionWebhook (--conversion) + kind: Certificate + group: cert-manager.io + version: v1 + name: serving-cert # This name should match the one in certificate.yaml + fieldPath: .metadata.namespace # Namespace of the certificate CR + targets: + - select: + kind: CustomResourceDefinition + fieldPaths: + - .metadata.annotations.[cert-manager.io/inject-ca-from] + options: + delimiter: '/' + index: 0 + create: true +- source: + kind: Certificate + group: cert-manager.io + version: v1 + name: serving-cert # This name should match the one in certificate.yaml + fieldPath: .metadata.name + targets: + - select: + kind: CustomResourceDefinition + fieldPaths: + - .metadata.annotations.[cert-manager.io/inject-ca-from] + options: + delimiter: '/' + index: 1 + create: true diff --git a/config/default/manager_webhook_patch.yaml b/config/default/manager_webhook_patch.yaml new file mode 100644 index 0000000..bd4b6a7 --- /dev/null +++ b/config/default/manager_webhook_patch.yaml @@ -0,0 +1,26 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system + labels: + app.kubernetes.io/name: commons-operator + app.kubernetes.io/managed-by: kustomize +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: webhook-server-cert diff --git a/config/network-policy/allow-webhook-traffic.yaml b/config/network-policy/allow-webhook-traffic.yaml new file mode 100644 index 0000000..9e466b9 --- /dev/null +++ b/config/network-policy/allow-webhook-traffic.yaml @@ -0,0 +1,26 @@ +# This NetworkPolicy allows ingress traffic to your webhook server running +# as part of the controller-manager from specific namespaces and pods. CR(s) which uses webhooks +# will only work when applied in namespaces labeled with 'webhook: enabled' +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + labels: + app.kubernetes.io/name: commons-operator + app.kubernetes.io/managed-by: kustomize + name: allow-webhook-traffic + namespace: system +spec: + podSelector: + matchLabels: + control-plane: controller-manager + policyTypes: + - Ingress + ingress: + # This allows ingress traffic from any namespace with the label webhook: enabled + - from: + - namespaceSelector: + matchLabels: + webhook: enabled # Only from namespaces with this label + ports: + - port: 443 + protocol: TCP diff --git a/config/network-policy/kustomization.yaml b/config/network-policy/kustomization.yaml index ec0fb5e..0872bee 100644 --- a/config/network-policy/kustomization.yaml +++ b/config/network-policy/kustomization.yaml @@ -1,2 +1,3 @@ resources: +- allow-webhook-traffic.yaml - allow-metrics-traffic.yaml diff --git a/config/webhook/kustomization.yaml b/config/webhook/kustomization.yaml new file mode 100644 index 0000000..9cf2613 --- /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 0000000..206316e --- /dev/null +++ b/config/webhook/kustomizeconfig.yaml @@ -0,0 +1,22 @@ +# the following config is for teaching kustomize where to look at when substituting nameReference. +# 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 diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml new file mode 100644 index 0000000..4ebba55 --- /dev/null +++ b/config/webhook/manifests.yaml @@ -0,0 +1,132 @@ +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + name: mutating-webhook-configuration +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /mutate-authentication-kubedoop-dev-v1alpha1-authenticationclass + failurePolicy: Fail + name: mauthenticationclass-v1alpha1.kb.io + rules: + - apiGroups: + - authentication.kubedoop.dev + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - authenticationclasses + sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /mutate-s3-kubedoop-dev-v1alpha1-s3bucket + failurePolicy: Fail + name: ms3bucket-v1alpha1.kb.io + rules: + - apiGroups: + - s3.kubedoop.dev + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - s3buckets + sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /mutate-s3-kubedoop-dev-v1alpha1-s3connection + failurePolicy: Fail + name: ms3connection-v1alpha1.kb.io + rules: + - apiGroups: + - s3.kubedoop.dev + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - s3connections + sideEffects: None +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + name: validating-webhook-configuration +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-authentication-kubedoop-dev-v1alpha1-authenticationclass + failurePolicy: Fail + name: vauthenticationclass-v1alpha1.kb.io + rules: + - apiGroups: + - authentication.kubedoop.dev + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - authenticationclasses + sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-s3-kubedoop-dev-v1alpha1-s3bucket + failurePolicy: Fail + name: vs3bucket-v1alpha1.kb.io + rules: + - apiGroups: + - s3.kubedoop.dev + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - s3buckets + sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-s3-kubedoop-dev-v1alpha1-s3connection + failurePolicy: Fail + name: vs3connection-v1alpha1.kb.io + rules: + - apiGroups: + - s3.kubedoop.dev + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - s3connections + sideEffects: None diff --git a/config/webhook/service.yaml b/config/webhook/service.yaml new file mode 100644 index 0000000..5b22cb7 --- /dev/null +++ b/config/webhook/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/name: commons-operator + app.kubernetes.io/managed-by: kustomize + name: webhook-service + namespace: system +spec: + ports: + - port: 443 + protocol: TCP + targetPort: 9443 + selector: + control-plane: controller-manager diff --git a/internal/controller/pod_enrichment/restart_suite_test.go b/internal/controller/pod_enrichment/restart_suite_test.go index 74779f8..257024a 100644 --- a/internal/controller/pod_enrichment/restart_suite_test.go +++ b/internal/controller/pod_enrichment/restart_suite_test.go @@ -54,7 +54,10 @@ var _ = BeforeSuite(func() { ctx, cancel = context.WithCancel(context.TODO()) By("bootstrapping test environment") - testEnv = &envtest.Environment{ErrorIfCRDPathMissing: true} + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: true, + } var err error diff --git a/internal/controller/restart/restart_suite_test.go b/internal/controller/restart/restart_suite_test.go index ae4e7da..a8041bf 100644 --- a/internal/controller/restart/restart_suite_test.go +++ b/internal/controller/restart/restart_suite_test.go @@ -55,6 +55,7 @@ var _ = BeforeSuite(func() { By("bootstrapping test environment") testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")}, ErrorIfCRDPathMissing: true, } diff --git a/internal/webhook/authentication/v1alpha1/authenticationclass_webhook.go b/internal/webhook/authentication/v1alpha1/authenticationclass_webhook.go new file mode 100644 index 0000000..6506018 --- /dev/null +++ b/internal/webhook/authentication/v1alpha1/authenticationclass_webhook.go @@ -0,0 +1,125 @@ +/* +Copyright 2023 zncdatadev. + +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" + "fmt" + + authenticationv1alpha1 "github.com/zncdatadev/operator-go/pkg/apis/authentication/v1alpha1" + "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" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +// nolint:unused +// log is for logging in this package. +var authenticationclasslog = logf.Log.WithName("authenticationclass-resource") + +// SetupAuthenticationClassWebhookWithManager registers the webhook for AuthenticationClass in the manager. +func SetupAuthenticationClassWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr).For(&authenticationv1alpha1.AuthenticationClass{}). + WithValidator(&AuthenticationClassCustomValidator{}). + WithDefaulter(&AuthenticationClassCustomDefaulter{}). + Complete() +} + +// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! + +// +kubebuilder:webhook:path=/mutate-authentication-kubedoop-dev-v1alpha1-authenticationclass,mutating=true,failurePolicy=fail,sideEffects=None,groups=authentication.kubedoop.dev,resources=authenticationclasses,verbs=create;update,versions=v1alpha1,name=mauthenticationclass-v1alpha1.kb.io,admissionReviewVersions=v1 + +// AuthenticationClassCustomDefaulter struct is responsible for setting default values on the custom resource of the +// Kind AuthenticationClass when those are created or updated. +// +// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods, +// as it is used only for temporary operations and does not need to be deeply copied. +type AuthenticationClassCustomDefaulter struct { + // TODO(user): Add more fields as needed for defaulting +} + +var _ webhook.CustomDefaulter = &AuthenticationClassCustomDefaulter{} + +// Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind AuthenticationClass. +func (d *AuthenticationClassCustomDefaulter) Default(ctx context.Context, obj runtime.Object) error { + authenticationclass, ok := obj.(*authenticationv1alpha1.AuthenticationClass) + + if !ok { + return fmt.Errorf("expected an AuthenticationClass object but got %T", obj) + } + authenticationclasslog.Info("Defaulting for AuthenticationClass", "name", authenticationclass.GetName()) + + // TODO(user): fill in your defaulting logic. + + return nil +} + +// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. +// NOTE: The 'path' attribute must follow a specific pattern and should not be modified directly here. +// Modifying the path for an invalid path can cause API server errors; failing to locate the webhook. +// +kubebuilder:webhook:path=/validate-authentication-kubedoop-dev-v1alpha1-authenticationclass,mutating=false,failurePolicy=fail,sideEffects=None,groups=authentication.kubedoop.dev,resources=authenticationclasses,verbs=create;update,versions=v1alpha1,name=vauthenticationclass-v1alpha1.kb.io,admissionReviewVersions=v1 + +// AuthenticationClassCustomValidator struct is responsible for validating the AuthenticationClass resource +// when it is created, updated, or deleted. +// +// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods, +// as this struct is used only for temporary operations and does not need to be deeply copied. +type AuthenticationClassCustomValidator struct { + // TODO(user): Add more fields as needed for validation +} + +var _ webhook.CustomValidator = &AuthenticationClassCustomValidator{} + +// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type AuthenticationClass. +func (v *AuthenticationClassCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + authenticationclass, ok := obj.(*authenticationv1alpha1.AuthenticationClass) + if !ok { + return nil, fmt.Errorf("expected a AuthenticationClass object but got %T", obj) + } + authenticationclasslog.Info("Validation for AuthenticationClass upon creation", "name", authenticationclass.GetName()) + + // TODO(user): fill in your validation logic upon object creation. + + return nil, nil +} + +// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type AuthenticationClass. +func (v *AuthenticationClassCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + authenticationclass, ok := newObj.(*authenticationv1alpha1.AuthenticationClass) + if !ok { + return nil, fmt.Errorf("expected a AuthenticationClass object for the newObj but got %T", newObj) + } + authenticationclasslog.Info("Validation for AuthenticationClass upon update", "name", authenticationclass.GetName()) + + // TODO(user): fill in your validation logic upon object update. + + return nil, nil +} + +// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type AuthenticationClass. +func (v *AuthenticationClassCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + authenticationclass, ok := obj.(*authenticationv1alpha1.AuthenticationClass) + if !ok { + return nil, fmt.Errorf("expected a AuthenticationClass object but got %T", obj) + } + authenticationclasslog.Info("Validation for AuthenticationClass upon deletion", "name", authenticationclass.GetName()) + + // TODO(user): fill in your validation logic upon object deletion. + + return nil, nil +} diff --git a/internal/webhook/authentication/v1alpha1/authenticationclass_webhook_test.go b/internal/webhook/authentication/v1alpha1/authenticationclass_webhook_test.go new file mode 100644 index 0000000..f2a7526 --- /dev/null +++ b/internal/webhook/authentication/v1alpha1/authenticationclass_webhook_test.go @@ -0,0 +1,86 @@ +/* +Copyright 2023 zncdatadev. + +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 ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + authenticationv1alpha1 "github.com/zncdatadev/operator-go/pkg/apis/authentication/v1alpha1" +) + +var _ = Describe("AuthenticationClass Webhook", func() { + var ( + obj *authenticationv1alpha1.AuthenticationClass + oldObj *authenticationv1alpha1.AuthenticationClass + validator AuthenticationClassCustomValidator + defaulter AuthenticationClassCustomDefaulter + ) + + BeforeEach(func() { + obj = &authenticationv1alpha1.AuthenticationClass{} + oldObj = &authenticationv1alpha1.AuthenticationClass{} + validator = AuthenticationClassCustomValidator{} + Expect(validator).NotTo(BeNil(), "Expected validator to be initialized") + defaulter = AuthenticationClassCustomDefaulter{} + Expect(defaulter).NotTo(BeNil(), "Expected defaulter to be initialized") + Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized") + Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") + // TODO (user): Add any setup logic common to all tests + }) + + AfterEach(func() { + // TODO (user): Add any teardown logic common to all tests + }) + + Context("When creating AuthenticationClass under Defaulting Webhook", func() { + // TODO (user): Add logic for defaulting webhooks + // Example: + // It("Should apply defaults when a required field is empty", func() { + // By("simulating a scenario where defaults should be applied") + // obj.SomeFieldWithDefault = "" + // By("calling the Default method to apply defaults") + // defaulter.Default(ctx, obj) + // By("checking that the default values are set") + // Expect(obj.SomeFieldWithDefault).To(Equal("default_value")) + // }) + }) + + Context("When creating or updating AuthenticationClass under Validating Webhook", func() { + // TODO (user): Add logic for validating webhooks + // Example: + // It("Should deny creation if a required field is missing", func() { + // By("simulating an invalid creation scenario") + // obj.SomeRequiredField = "" + // Expect(validator.ValidateCreate(ctx, obj)).Error().To(HaveOccurred()) + // }) + // + // It("Should admit creation if all required fields are present", func() { + // By("simulating an invalid creation scenario") + // obj.SomeRequiredField = "valid_value" + // Expect(validator.ValidateCreate(ctx, obj)).To(BeNil()) + // }) + // + // It("Should validate updates correctly", func() { + // By("simulating a valid update scenario") + // oldObj.SomeRequiredField = "updated_value" + // obj.SomeRequiredField = "updated_value" + // Expect(validator.ValidateUpdate(ctx, oldObj, obj)).To(BeNil()) + // }) + }) + +}) diff --git a/internal/webhook/authentication/v1alpha1/webhook_suite_test.go b/internal/webhook/authentication/v1alpha1/webhook_suite_test.go new file mode 100644 index 0000000..f829cfe --- /dev/null +++ b/internal/webhook/authentication/v1alpha1/webhook_suite_test.go @@ -0,0 +1,154 @@ +/* +Copyright 2023 zncdatadev. + +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" + "os" + "path/filepath" + "runtime" + "testing" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + admissionv1 "k8s.io/api/admission/v1" + + // +kubebuilder:scaffold:imports + authenticationv1alpha1 "github.com/zncdatadev/operator-go/pkg/apis/authentication/v1alpha1" + apimachineryruntime "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" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + "sigs.k8s.io/controller-runtime/pkg/webhook" +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +var ( + cancel context.CancelFunc + cfg *rest.Config + ctx context.Context + k8sClient client.Client + testEnv *envtest.Environment +) + +func TestAPIs(t *testing.T) { + testK8sVersion := os.Getenv("ENVTEST_K8S_VERSION") + if testK8sVersion == "" { + logf.Log.Info("ENVTEST_K8S_VERSION is not set, using default version") + testK8sVersion = "1.26.1" + } + if asserts := os.Getenv("KUBEBUILDER_ASSETS"); asserts == "" { + logf.Log.Info("KUBEBUILDER_ASSETS is not set, using default version " + testK8sVersion) + err := os.Setenv("KUBEBUILDER_ASSETS", filepath.Join("..", "..", "..", "bin", "k8s", fmt.Sprintf("%s-%s-%s", testK8sVersion, runtime.GOOS, runtime.GOARCH))) + if err != nil { + t.Errorf("Failed to set KUBEBUILDER_ASSETS") + } + } + + RegisterFailHandler(Fail) + + RunSpecs(t, "Authentication Webhook Suite") +} + +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 := apimachineryruntime.NewScheme() + err = authenticationv1alpha1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + err = admissionv1.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, + WebhookServer: webhook.NewServer(webhook.Options{ + Host: webhookInstallOptions.LocalServingHost, + Port: webhookInstallOptions.LocalServingPort, + CertDir: webhookInstallOptions.LocalServingCertDir, + }), + LeaderElection: false, + Metrics: metricsserver.Options{BindAddress: "0"}, + }) + Expect(err).NotTo(HaveOccurred()) + + err = SetupAuthenticationClassWebhookWithManager(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 + } + + return conn.Close() + }).Should(Succeed()) +}) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + cancel() + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +}) diff --git a/internal/webhook/s3/v1alpha1/s3bucket_webhook.go b/internal/webhook/s3/v1alpha1/s3bucket_webhook.go new file mode 100644 index 0000000..4a6f5ed --- /dev/null +++ b/internal/webhook/s3/v1alpha1/s3bucket_webhook.go @@ -0,0 +1,126 @@ +/* +Copyright 2023 zncdatadev. + +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" + "fmt" + + "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" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + s3v1alpha1 "github.com/zncdatadev/operator-go/pkg/apis/s3/v1alpha1" +) + +// nolint:unused +// log is for logging in this package. +var s3bucketlog = logf.Log.WithName("s3bucket-resource") + +// SetupS3BucketWebhookWithManager registers the webhook for S3Bucket in the manager. +func SetupS3BucketWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr).For(&s3v1alpha1.S3Bucket{}). + WithValidator(&S3BucketCustomValidator{}). + WithDefaulter(&S3BucketCustomDefaulter{}). + Complete() +} + +// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! + +// +kubebuilder:webhook:path=/mutate-s3-kubedoop-dev-v1alpha1-s3bucket,mutating=true,failurePolicy=fail,sideEffects=None,groups=s3.kubedoop.dev,resources=s3buckets,verbs=create;update,versions=v1alpha1,name=ms3bucket-v1alpha1.kb.io,admissionReviewVersions=v1 + +// S3BucketCustomDefaulter struct is responsible for setting default values on the custom resource of the +// Kind S3Bucket when those are created or updated. +// +// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods, +// as it is used only for temporary operations and does not need to be deeply copied. +type S3BucketCustomDefaulter struct { + // TODO(user): Add more fields as needed for defaulting +} + +var _ webhook.CustomDefaulter = &S3BucketCustomDefaulter{} + +// Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind S3Bucket. +func (d *S3BucketCustomDefaulter) Default(ctx context.Context, obj runtime.Object) error { + s3bucket, ok := obj.(*s3v1alpha1.S3Bucket) + + if !ok { + return fmt.Errorf("expected an S3Bucket object but got %T", obj) + } + s3bucketlog.Info("Defaulting for S3Bucket", "name", s3bucket.GetName()) + + // TODO(user): fill in your defaulting logic. + + return nil +} + +// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. +// NOTE: The 'path' attribute must follow a specific pattern and should not be modified directly here. +// Modifying the path for an invalid path can cause API server errors; failing to locate the webhook. +// +kubebuilder:webhook:path=/validate-s3-kubedoop-dev-v1alpha1-s3bucket,mutating=false,failurePolicy=fail,sideEffects=None,groups=s3.kubedoop.dev,resources=s3buckets,verbs=create;update,versions=v1alpha1,name=vs3bucket-v1alpha1.kb.io,admissionReviewVersions=v1 + +// S3BucketCustomValidator struct is responsible for validating the S3Bucket resource +// when it is created, updated, or deleted. +// +// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods, +// as this struct is used only for temporary operations and does not need to be deeply copied. +type S3BucketCustomValidator struct { + // TODO(user): Add more fields as needed for validation +} + +var _ webhook.CustomValidator = &S3BucketCustomValidator{} + +// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type S3Bucket. +func (v *S3BucketCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + s3bucket, ok := obj.(*s3v1alpha1.S3Bucket) + if !ok { + return nil, fmt.Errorf("expected a S3Bucket object but got %T", obj) + } + s3bucketlog.Info("Validation for S3Bucket upon creation", "name", s3bucket.GetName()) + + // TODO(user): fill in your validation logic upon object creation. + + return nil, nil +} + +// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type S3Bucket. +func (v *S3BucketCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + s3bucket, ok := newObj.(*s3v1alpha1.S3Bucket) + if !ok { + return nil, fmt.Errorf("expected a S3Bucket object for the newObj but got %T", newObj) + } + s3bucketlog.Info("Validation for S3Bucket upon update", "name", s3bucket.GetName()) + + // TODO(user): fill in your validation logic upon object update. + + return nil, nil +} + +// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type S3Bucket. +func (v *S3BucketCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + s3bucket, ok := obj.(*s3v1alpha1.S3Bucket) + if !ok { + return nil, fmt.Errorf("expected a S3Bucket object but got %T", obj) + } + s3bucketlog.Info("Validation for S3Bucket upon deletion", "name", s3bucket.GetName()) + + // TODO(user): fill in your validation logic upon object deletion. + + return nil, nil +} diff --git a/internal/webhook/s3/v1alpha1/s3bucket_webhook_test.go b/internal/webhook/s3/v1alpha1/s3bucket_webhook_test.go new file mode 100644 index 0000000..7497cb0 --- /dev/null +++ b/internal/webhook/s3/v1alpha1/s3bucket_webhook_test.go @@ -0,0 +1,87 @@ +/* +Copyright 2023 zncdatadev. + +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 ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + s3v1alpha1 "github.com/zncdatadev/operator-go/pkg/apis/s3/v1alpha1" + // TODO (user): Add any additional imports if needed +) + +var _ = Describe("S3Bucket Webhook", func() { + var ( + obj *s3v1alpha1.S3Bucket + oldObj *s3v1alpha1.S3Bucket + validator S3BucketCustomValidator + defaulter S3BucketCustomDefaulter + ) + + BeforeEach(func() { + obj = &s3v1alpha1.S3Bucket{} + oldObj = &s3v1alpha1.S3Bucket{} + validator = S3BucketCustomValidator{} + Expect(validator).NotTo(BeNil(), "Expected validator to be initialized") + defaulter = S3BucketCustomDefaulter{} + Expect(defaulter).NotTo(BeNil(), "Expected defaulter to be initialized") + Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized") + Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") + // TODO (user): Add any setup logic common to all tests + }) + + AfterEach(func() { + // TODO (user): Add any teardown logic common to all tests + }) + + Context("When creating S3Bucket under Defaulting Webhook", func() { + // TODO (user): Add logic for defaulting webhooks + // Example: + // It("Should apply defaults when a required field is empty", func() { + // By("simulating a scenario where defaults should be applied") + // obj.SomeFieldWithDefault = "" + // By("calling the Default method to apply defaults") + // defaulter.Default(ctx, obj) + // By("checking that the default values are set") + // Expect(obj.SomeFieldWithDefault).To(Equal("default_value")) + // }) + }) + + Context("When creating or updating S3Bucket under Validating Webhook", func() { + // TODO (user): Add logic for validating webhooks + // Example: + // It("Should deny creation if a required field is missing", func() { + // By("simulating an invalid creation scenario") + // obj.SomeRequiredField = "" + // Expect(validator.ValidateCreate(ctx, obj)).Error().To(HaveOccurred()) + // }) + // + // It("Should admit creation if all required fields are present", func() { + // By("simulating an invalid creation scenario") + // obj.SomeRequiredField = "valid_value" + // Expect(validator.ValidateCreate(ctx, obj)).To(BeNil()) + // }) + // + // It("Should validate updates correctly", func() { + // By("simulating a valid update scenario") + // oldObj.SomeRequiredField = "updated_value" + // obj.SomeRequiredField = "updated_value" + // Expect(validator.ValidateUpdate(ctx, oldObj, obj)).To(BeNil()) + // }) + }) + +}) diff --git a/internal/webhook/s3/v1alpha1/s3connection_webhook.go b/internal/webhook/s3/v1alpha1/s3connection_webhook.go new file mode 100644 index 0000000..6d84e7d --- /dev/null +++ b/internal/webhook/s3/v1alpha1/s3connection_webhook.go @@ -0,0 +1,126 @@ +/* +Copyright 2023 zncdatadev. + +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" + "fmt" + + "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" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + s3v1alpha1 "github.com/zncdatadev/operator-go/pkg/apis/s3/v1alpha1" +) + +// nolint:unused +// log is for logging in this package. +var s3connectionlog = logf.Log.WithName("s3connection-resource") + +// SetupS3ConnectionWebhookWithManager registers the webhook for S3Connection in the manager. +func SetupS3ConnectionWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr).For(&s3v1alpha1.S3Connection{}). + WithValidator(&S3ConnectionCustomValidator{}). + WithDefaulter(&S3ConnectionCustomDefaulter{}). + Complete() +} + +// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! + +// +kubebuilder:webhook:path=/mutate-s3-kubedoop-dev-v1alpha1-s3connection,mutating=true,failurePolicy=fail,sideEffects=None,groups=s3.kubedoop.dev,resources=s3connections,verbs=create;update,versions=v1alpha1,name=ms3connection-v1alpha1.kb.io,admissionReviewVersions=v1 + +// S3ConnectionCustomDefaulter struct is responsible for setting default values on the custom resource of the +// Kind S3Connection when those are created or updated. +// +// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods, +// as it is used only for temporary operations and does not need to be deeply copied. +type S3ConnectionCustomDefaulter struct { + // TODO(user): Add more fields as needed for defaulting +} + +var _ webhook.CustomDefaulter = &S3ConnectionCustomDefaulter{} + +// Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind S3Connection. +func (d *S3ConnectionCustomDefaulter) Default(ctx context.Context, obj runtime.Object) error { + s3connection, ok := obj.(*s3v1alpha1.S3Connection) + + if !ok { + return fmt.Errorf("expected an S3Connection object but got %T", obj) + } + s3connectionlog.Info("Defaulting for S3Connection", "name", s3connection.GetName()) + + // TODO(user): fill in your defaulting logic. + + return nil +} + +// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. +// NOTE: The 'path' attribute must follow a specific pattern and should not be modified directly here. +// Modifying the path for an invalid path can cause API server errors; failing to locate the webhook. +// +kubebuilder:webhook:path=/validate-s3-kubedoop-dev-v1alpha1-s3connection,mutating=false,failurePolicy=fail,sideEffects=None,groups=s3.kubedoop.dev,resources=s3connections,verbs=create;update,versions=v1alpha1,name=vs3connection-v1alpha1.kb.io,admissionReviewVersions=v1 + +// S3ConnectionCustomValidator struct is responsible for validating the S3Connection resource +// when it is created, updated, or deleted. +// +// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods, +// as this struct is used only for temporary operations and does not need to be deeply copied. +type S3ConnectionCustomValidator struct { + // TODO(user): Add more fields as needed for validation +} + +var _ webhook.CustomValidator = &S3ConnectionCustomValidator{} + +// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type S3Connection. +func (v *S3ConnectionCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + s3connection, ok := obj.(*s3v1alpha1.S3Connection) + if !ok { + return nil, fmt.Errorf("expected a S3Connection object but got %T", obj) + } + s3connectionlog.Info("Validation for S3Connection upon creation", "name", s3connection.GetName()) + + // TODO(user): fill in your validation logic upon object creation. + + return nil, nil +} + +// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type S3Connection. +func (v *S3ConnectionCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + s3connection, ok := newObj.(*s3v1alpha1.S3Connection) + if !ok { + return nil, fmt.Errorf("expected a S3Connection object for the newObj but got %T", newObj) + } + s3connectionlog.Info("Validation for S3Connection upon update", "name", s3connection.GetName()) + + // TODO(user): fill in your validation logic upon object update. + + return nil, nil +} + +// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type S3Connection. +func (v *S3ConnectionCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + s3connection, ok := obj.(*s3v1alpha1.S3Connection) + if !ok { + return nil, fmt.Errorf("expected a S3Connection object but got %T", obj) + } + s3connectionlog.Info("Validation for S3Connection upon deletion", "name", s3connection.GetName()) + + // TODO(user): fill in your validation logic upon object deletion. + + return nil, nil +} diff --git a/internal/webhook/s3/v1alpha1/s3connection_webhook_test.go b/internal/webhook/s3/v1alpha1/s3connection_webhook_test.go new file mode 100644 index 0000000..5275316 --- /dev/null +++ b/internal/webhook/s3/v1alpha1/s3connection_webhook_test.go @@ -0,0 +1,87 @@ +/* +Copyright 2023 zncdatadev. + +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 ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + s3v1alpha1 "github.com/zncdatadev/operator-go/pkg/apis/s3/v1alpha1" + // TODO (user): Add any additional imports if needed +) + +var _ = Describe("S3Connection Webhook", func() { + var ( + obj *s3v1alpha1.S3Connection + oldObj *s3v1alpha1.S3Connection + validator S3ConnectionCustomValidator + defaulter S3ConnectionCustomDefaulter + ) + + BeforeEach(func() { + obj = &s3v1alpha1.S3Connection{} + oldObj = &s3v1alpha1.S3Connection{} + validator = S3ConnectionCustomValidator{} + Expect(validator).NotTo(BeNil(), "Expected validator to be initialized") + defaulter = S3ConnectionCustomDefaulter{} + Expect(defaulter).NotTo(BeNil(), "Expected defaulter to be initialized") + Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized") + Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") + // TODO (user): Add any setup logic common to all tests + }) + + AfterEach(func() { + // TODO (user): Add any teardown logic common to all tests + }) + + Context("When creating S3Connection under Defaulting Webhook", func() { + // TODO (user): Add logic for defaulting webhooks + // Example: + // It("Should apply defaults when a required field is empty", func() { + // By("simulating a scenario where defaults should be applied") + // obj.SomeFieldWithDefault = "" + // By("calling the Default method to apply defaults") + // defaulter.Default(ctx, obj) + // By("checking that the default values are set") + // Expect(obj.SomeFieldWithDefault).To(Equal("default_value")) + // }) + }) + + Context("When creating or updating S3Connection under Validating Webhook", func() { + // TODO (user): Add logic for validating webhooks + // Example: + // It("Should deny creation if a required field is missing", func() { + // By("simulating an invalid creation scenario") + // obj.SomeRequiredField = "" + // Expect(validator.ValidateCreate(ctx, obj)).Error().To(HaveOccurred()) + // }) + // + // It("Should admit creation if all required fields are present", func() { + // By("simulating an invalid creation scenario") + // obj.SomeRequiredField = "valid_value" + // Expect(validator.ValidateCreate(ctx, obj)).To(BeNil()) + // }) + // + // It("Should validate updates correctly", func() { + // By("simulating a valid update scenario") + // oldObj.SomeRequiredField = "updated_value" + // obj.SomeRequiredField = "updated_value" + // Expect(validator.ValidateUpdate(ctx, oldObj, obj)).To(BeNil()) + // }) + }) + +}) diff --git a/internal/webhook/s3/v1alpha1/webhook_suite_test.go b/internal/webhook/s3/v1alpha1/webhook_suite_test.go new file mode 100644 index 0000000..a414941 --- /dev/null +++ b/internal/webhook/s3/v1alpha1/webhook_suite_test.go @@ -0,0 +1,157 @@ +/* +Copyright 2023 zncdatadev. + +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" + "os" + "path/filepath" + "runtime" + "testing" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + admissionv1 "k8s.io/api/admission/v1" + + // +kubebuilder:scaffold:imports + s3v1alpha1 "github.com/zncdatadev/operator-go/pkg/apis/s3/v1alpha1" + apimachineryruntime "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" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + "sigs.k8s.io/controller-runtime/pkg/webhook" +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +var ( + cancel context.CancelFunc + cfg *rest.Config + ctx context.Context + k8sClient client.Client + testEnv *envtest.Environment +) + +func TestAPIs(t *testing.T) { + testK8sVersion := os.Getenv("ENVTEST_K8S_VERSION") + if testK8sVersion == "" { + logf.Log.Info("ENVTEST_K8S_VERSION is not set, using default version") + testK8sVersion = "1.26.1" + } + if asserts := os.Getenv("KUBEBUILDER_ASSETS"); asserts == "" { + logf.Log.Info("KUBEBUILDER_ASSETS is not set, using default version " + testK8sVersion) + err := os.Setenv("KUBEBUILDER_ASSETS", filepath.Join("..", "..", "..", "bin", "k8s", fmt.Sprintf("%s-%s-%s", testK8sVersion, runtime.GOOS, runtime.GOARCH))) + if err != nil { + t.Errorf("Failed to set KUBEBUILDER_ASSETS") + } + } + + RegisterFailHandler(Fail) + + RunSpecs(t, "S3 Webhook Suite") +} + +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 := apimachineryruntime.NewScheme() + err = s3v1alpha1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + err = admissionv1.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, + WebhookServer: webhook.NewServer(webhook.Options{ + Host: webhookInstallOptions.LocalServingHost, + Port: webhookInstallOptions.LocalServingPort, + CertDir: webhookInstallOptions.LocalServingCertDir, + }), + LeaderElection: false, + Metrics: metricsserver.Options{BindAddress: "0"}, + }) + Expect(err).NotTo(HaveOccurred()) + + err = SetupS3ConnectionWebhookWithManager(mgr) + Expect(err).NotTo(HaveOccurred()) + + err = SetupS3BucketWebhookWithManager(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 + } + + return conn.Close() + }).Should(Succeed()) +}) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + cancel() + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +}) diff --git a/test/e2e/e2e_suite_test.go b/test/e2e/e2e_suite_test.go index 40063cd..c158a5c 100644 --- a/test/e2e/e2e_suite_test.go +++ b/test/e2e/e2e_suite_test.go @@ -95,6 +95,14 @@ var _ = BeforeSuite(func() { if !isCertManagerAlreadyInstalled { _, _ = fmt.Fprintf(GinkgoWriter, "Installing CertManager...\n") Expect(utils.InstallCertManager()).To(Succeed(), "Failed to install CertManager") + _, _ = fmt.Fprintf(GinkgoWriter, "Waiting for CertManager to be ready...\n") + // Expect(utils.WaitForCertManagerToBeReady()).To(Succeed(), "CertManager is not ready") + // When cert-manager crds with cert not ready, to create issuer, + // it will raise x509: certificate signed by unknown authority + // So, we need retry to wait for cert-manager to be ready + Eventually(func(g Gomega) { + g.Expect(utils.WaitForCertManagerToBeReady()).To(Succeed(), "CertManager is not ready") + }, "2m", "5s").Should(Succeed()) } else { _, _ = fmt.Fprintf(GinkgoWriter, "WARNING: CertManager is already installed. Skipping installation...\n") } diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 02cfcbb..e7456ab 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -58,6 +58,9 @@ var _ = Describe("Manager", Ordered, func() { _, err = utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "Failed to install CRDs") + By("delay 5 seconds to wait for the CRDs to be installed") + time.Sleep(5 * time.Second) + By("deploying the controller-manager") cmd = exec.Command("make", "deploy", fmt.Sprintf("IMG=%s", utils.GetProjectImg())) _, err = utils.Run(cmd) @@ -241,6 +244,44 @@ var _ = Describe("Manager", Ordered, func() { )) }) + It("should provisioned cert-manager", func() { + By("validating that cert-manager has the certificate Secret") + verifyCertManager := func(g Gomega) { + cmd := exec.Command("kubectl", "get", "secrets", "webhook-server-cert", "-n", namespace) + _, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + } + Eventually(verifyCertManager).Should(Succeed()) + }) + + It("should have CA injection for mutating webhooks", func() { + By("checking CA injection for mutating webhooks") + verifyCAInjection := func(g Gomega) { + cmd := exec.Command("kubectl", "get", + "mutatingwebhookconfigurations.admissionregistration.k8s.io", + "commons-operator-mutating-webhook-configuration", + "-o", "go-template={{ range .webhooks }}{{ .clientConfig.caBundle }}{{ end }}") + mwhOutput, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(len(mwhOutput)).To(BeNumerically(">", 10)) + } + Eventually(verifyCAInjection).Should(Succeed()) + }) + + It("should have CA injection for validating webhooks", func() { + By("checking CA injection for validating webhooks") + verifyCAInjection := func(g Gomega) { + cmd := exec.Command("kubectl", "get", + "validatingwebhookconfigurations.admissionregistration.k8s.io", + "commons-operator-validating-webhook-configuration", + "-o", "go-template={{ range .webhooks }}{{ .clientConfig.caBundle }}{{ end }}") + vwhOutput, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(len(vwhOutput)).To(BeNumerically(">", 10)) + } + Eventually(verifyCAInjection).Should(Succeed()) + }) + // +kubebuilder:scaffold:e2e-webhooks-checks // TODO: Customize the e2e test suite with scenarios specific to your project. diff --git a/test/utils/utils.go b/test/utils/utils.go index 928153b..3e004eb 100644 --- a/test/utils/utils.go +++ b/test/utils/utils.go @@ -19,9 +19,11 @@ package utils import ( "bufio" "bytes" + "encoding/json" "fmt" "os" "os/exec" + "path" "strings" . "github.com/onsi/ginkgo/v2" //nolint:golint,revive @@ -32,7 +34,7 @@ const ( prometheusOperatorURL = "https://github.com/prometheus-operator/prometheus-operator/" + "releases/download/%s/bundle.yaml" - certmanagerVersion = "v1.16.0" + certmanagerVersion = "v1.16.2" certmanagerURLTmpl = "https://github.com/jetstack/cert-manager/releases/download/%s/cert-manager.yaml" registry = "quay.io/zncdatadev" @@ -115,6 +117,7 @@ func UninstallCertManager() { if _, err := Run(cmd); err != nil { warnError(err) } + } // InstallCertManager installs the cert manager bundle. @@ -136,6 +139,80 @@ func InstallCertManager() error { return err } +// https://cert-manager.io/docs/concepts/webhook/#webhook-connection-problems-shortly-after-cert-manager-installation +// https://cert-manager.io/docs/installation/kubectl/#verify +func WaitForCertManagerToBeReady() error { + // init Issuer and Certificate + issuer := map[string]interface{}{ + "apiVersion": "cert-manager.io/v1", + "kind": "Issuer", + "metadata": map[string]interface{}{ + "name": "test-selfsigned-issuer", + "namespace": "cert-manager", + }, + "spec": map[string]interface{}{ + "selfSigned": map[string]interface{}{}, + }, + } + certificate := map[string]interface{}{ + "apiVersion": "cert-manager.io/v1", + "kind": "Certificate", + "metadata": map[string]interface{}{ + "name": "test-selfsigned-certificate", + "namespace": "cert-manager", + }, + "spec": map[string]interface{}{ + "dnsNames": []string{"test.example.com"}, + "secretName": "test-selfsigned-certificate", + "issuerRef": map[string]interface{}{ + "name": "test-selfsigned-issuer", + }, + }, + } + + issuerFile := path.Join(os.TempDir(), "test-issuer.yaml") + defer func() { + if err := os.Remove(issuerFile); err != nil { + warnError(err) + } + }() + + certificateFile := path.Join(os.TempDir(), "test-certificate.yaml") + defer func() { + if err := os.Remove(certificateFile); err != nil { + warnError(err) + } + }() + + if err := WriteResourceToJSON(issuer, issuerFile); err != nil { + return err + } + + if err := WriteResourceToJSON(certificate, certificateFile); err != nil { + return err + } + + // apply the resources + cmd := exec.Command("kubectl", "apply", "-f", issuerFile) + if _, err := Run(cmd); err != nil { + return err + } + cmd = exec.Command("kubectl", "apply", "-f", certificateFile) + if _, err := Run(cmd); err != nil { + return err + } + + // wait for the certificate to be ready + cmd = exec.Command("kubectl", "wait", "certificate", "test-selfsigned-certificate", + "--for", "condition=Ready", + "--namespace", "cert-manager", + "--timeout", "2s", + ) + + _, err := Run(cmd) + return err +} + // IsCertManagerCRDsInstalled checks if any Cert Manager CRDs are installed // by verifying the existence of key CRDs related to Cert Manager. func IsCertManagerCRDsInstalled() bool { @@ -261,3 +338,17 @@ func GetProjectImg() string { } return fmt.Sprintf("%s/%s:%s", registry, projectName, version) } + +func WriteResourceToJSON(resource map[string]interface{}, filename string) error { + jsonData, err := json.Marshal(resource) + if err != nil { + return fmt.Errorf("error marshaling resource to JSON: %w", err) + } + + err = os.WriteFile(filename, jsonData, 0644) + if err != nil { + return fmt.Errorf("error writing JSON to file: %w", err) + } + + return nil +}