From 0c99500ed2619e4025bf41f0cb8c013d5ffc1dcd Mon Sep 17 00:00:00 2001 From: Cindy Bang Date: Mon, 29 Apr 2024 09:00:00 -0400 Subject: [PATCH 1/9] fixup! Rename csi-driver to better short-name. (#158) --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index c1f9bc20e..c62f657eb 100644 --- a/Makefile +++ b/Makefile @@ -292,7 +292,7 @@ $(LOCALBIN): ##@ Tooling Binaries: # setup-envtest does not have devbox support so always use CACHE_BIN -KUBECTL ?= kubectl +KUBECTL ?= $(LOCALBIN)/kubectl KUSTOMIZE ?= $(LOCALBIN)/kustomize CTLPTL ?= $(LOCALBIN)/ctlptl CLUSTERCTL ?= $(LOCALBIN)/clusterctl From cba93cfb5911dd36811c31e126c7d4ad6c25da9f Mon Sep 17 00:00:00 2001 From: Cindy Bang Date: Mon, 29 Apr 2024 09:00:00 -0400 Subject: [PATCH 2/9] chore: add kubebuilder --- Makefile | 8 ++++++++ devbox.json | 3 ++- devbox.lock | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index c62f657eb..b3109b500 100644 --- a/Makefile +++ b/Makefile @@ -296,6 +296,7 @@ KUBECTL ?= $(LOCALBIN)/kubectl KUSTOMIZE ?= $(LOCALBIN)/kustomize CTLPTL ?= $(LOCALBIN)/ctlptl CLUSTERCTL ?= $(LOCALBIN)/clusterctl +KUBEBUILDER ?= $(LOCALBIN)/kubebuilder CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen TILT ?= $(LOCALBIN)/tilt KIND ?= $(LOCALBIN)/kind @@ -310,6 +311,7 @@ MOCKGEN ?= $(LOCALBIN)/mockgen KUSTOMIZE_VERSION ?= v5.1.1 CTLPTL_VERSION ?= v0.8.25 CLUSTERCTL_VERSION ?= v1.5.3 +KUBEBUILDER_VERSION ?= v3.14.1 CONTROLLER_TOOLS_VERSION ?= v0.14.0 TILT_VERSION ?= 0.33.6 KIND_VERSION ?= 0.20.0 @@ -339,6 +341,12 @@ $(CLUSTERCTL): $(LOCALBIN) curl -fsSL https://github.com/kubernetes-sigs/cluster-api/releases/download/$(CLUSTERCTL_VERSION)/clusterctl-$(OS)-$(ARCH_SHORT) -o $(CLUSTERCTL) chmod +x $(CLUSTERCTL) +.PHONY: kubebuilder +kubebuilder: $(KUBEBUILDER) ## Download kubebuilder locally if necessary. +$(KUBEBUILDER): $(LOCALBIN) + curl -L -o $(LOCALBIN)/kubebuilder https://github.com/kubernetes-sigs/kubebuilder/releases/download/$(KUBEBUILDER_VERSION)/kubebuilder_$(OS)_$(ARCH_SHORT) + chmod +x $(LOCALBIN)/kubebuilder + .PHONY: controller-gen controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. $(CONTROLLER_GEN): $(LOCALBIN) diff --git a/devbox.json b/devbox.json index e85c3b7ab..3185cd1f6 100644 --- a/devbox.json +++ b/devbox.json @@ -19,7 +19,8 @@ "mockgen@latest", "kyverno-chainsaw@latest", "kubernetes-helm@latest", - "kubectl@latest" + "kubectl@latest", + "kubebuilder@latest" ], "shell": { "init_hook": [ diff --git a/devbox.lock b/devbox.lock index 62596c450..3d51a84aa 100644 --- a/devbox.lock +++ b/devbox.lock @@ -293,6 +293,54 @@ } } }, + "kubebuilder@latest": { + "last_modified": "2024-04-19T17:36:04-04:00", + "resolved": "github:NixOS/nixpkgs/92d295f588631b0db2da509f381b4fb1e74173c5#kubebuilder", + "source": "devbox-search", + "version": "3.14.1", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/0amj5zw2rka2zjvq076ljfn80pch4dsa-kubebuilder-3.14.1", + "default": true + } + ], + "store_path": "/nix/store/0amj5zw2rka2zjvq076ljfn80pch4dsa-kubebuilder-3.14.1" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/ckx2xzgy6h4qq1pqr7z8k39yg6s0spin-kubebuilder-3.14.1", + "default": true + } + ], + "store_path": "/nix/store/ckx2xzgy6h4qq1pqr7z8k39yg6s0spin-kubebuilder-3.14.1" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/4zwb2jklg7wy2whkhp9wg5gnlqldms3a-kubebuilder-3.14.1", + "default": true + } + ], + "store_path": "/nix/store/4zwb2jklg7wy2whkhp9wg5gnlqldms3a-kubebuilder-3.14.1" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/fswxi79rrm0qdqsv6njwmygyc8mzz6sy-kubebuilder-3.14.1", + "default": true + } + ], + "store_path": "/nix/store/fswxi79rrm0qdqsv6njwmygyc8mzz6sy-kubebuilder-3.14.1" + } + } + }, "kubectl@latest": { "last_modified": "2024-03-22T11:26:23Z", "resolved": "github:NixOS/nixpkgs/a3ed7406349a9335cb4c2a71369b697cecd9d351#kubectl", From 1ce1fb414f898b780c1af3e8ed24d3be3d6fd476 Mon Sep 17 00:00:00 2001 From: Cindy Bang Date: Mon, 29 Apr 2024 09:00:00 -0400 Subject: [PATCH 3/9] fixup! shorten cluster-api-provider-linode to capl, add release process (#124) --- config/default/kustomization.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml index e45d9b5ac..c6e870a70 100644 --- a/config/default/kustomization.yaml +++ b/config/default/kustomization.yaml @@ -28,12 +28,12 @@ patches: # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # crd/kustomization.yaml -#- manager_webhook_patch.yaml +#- path: manager_webhook_patch.yaml # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. # 'CERTMANAGER' needs to be enabled to use ca injection -#- webhookcainjection_patch.yaml +#- path: webhookcainjection_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 From 7d1356f2579234964bf3661ee06bafc6c8137e2b Mon Sep 17 00:00:00 2001 From: Cindy Bang Date: Mon, 29 Apr 2024 09:00:00 -0400 Subject: [PATCH 4/9] feat: linodemachine: kubebuilder create webhook Scaffold a validating admission webhook for the LinodeMachine resource with Kubebuider via the command: kubebuilder create webhook --group infrastructure --version v1alpha1 --kind LinodeMachine --programmatic-validation --- PROJECT | 3 + api/v1alpha1/linodemachine_webhook.go | 66 +++++++++ api/v1alpha1/linodemachine_webhook_test.go | 39 +++++ api/v1alpha1/webhook_suite_test.go | 145 +++++++++++++++++++ api/v1alpha1/zz_generated.deepcopy.go | 2 +- cmd/main.go | 6 + config/certmanager/certificate.yaml | 39 +++++ config/certmanager/kustomization.yaml | 5 + config/certmanager/kustomizeconfig.yaml | 8 + config/crd/kustomization.yaml | 4 +- config/default/kustomization.yaml | 4 +- config/default/manager_webhook_patch.yaml | 23 +++ config/default/webhookcainjection_patch.yaml | 29 ++++ config/webhook/kustomization.yaml | 6 + config/webhook/kustomizeconfig.yaml | 22 +++ config/webhook/manifests.yaml | 26 ++++ config/webhook/service.yaml | 15 ++ 17 files changed, 437 insertions(+), 5 deletions(-) create mode 100644 api/v1alpha1/linodemachine_webhook.go create mode 100644 api/v1alpha1/linodemachine_webhook_test.go create mode 100644 api/v1alpha1/webhook_suite_test.go create mode 100644 config/certmanager/certificate.yaml create mode 100644 config/certmanager/kustomization.yaml create mode 100644 config/certmanager/kustomizeconfig.yaml create mode 100644 config/default/manager_webhook_patch.yaml create mode 100644 config/default/webhookcainjection_patch.yaml create mode 100644 config/webhook/kustomization.yaml create mode 100644 config/webhook/kustomizeconfig.yaml create mode 100644 config/webhook/manifests.yaml create mode 100644 config/webhook/service.yaml diff --git a/PROJECT b/PROJECT index 320515e01..2249a9dab 100644 --- a/PROJECT +++ b/PROJECT @@ -26,6 +26,9 @@ resources: kind: LinodeMachine path: github.com/linode/cluster-api-provider-linode/api/v1alpha1 version: v1alpha1 + webhooks: + validation: true + webhookVersion: v1 - api: crdVersion: v1 namespaced: true diff --git a/api/v1alpha1/linodemachine_webhook.go b/api/v1alpha1/linodemachine_webhook.go new file mode 100644 index 000000000..af8740573 --- /dev/null +++ b/api/v1alpha1/linodemachine_webhook.go @@ -0,0 +1,66 @@ +/* +Copyright 2023 Akamai Technologies, Inc. + +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 ( + "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" +) + +// log is for logging in this package. +var linodemachinelog = logf.Log.WithName("linodemachine-resource") + +// SetupWebhookWithManager will setup the manager to manage the webhooks +func (r *LinodeMachine) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(r). + Complete() +} + +// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! + +// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. +//+kubebuilder:webhook:path=/validate-infrastructure-cluster-x-k8s-io-v1alpha1-linodemachine,mutating=false,failurePolicy=fail,sideEffects=None,groups=infrastructure.cluster.x-k8s.io,resources=linodemachines,verbs=create;update,versions=v1alpha1,name=vlinodemachine.kb.io,admissionReviewVersions=v1 + +var _ webhook.Validator = &LinodeMachine{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (r *LinodeMachine) ValidateCreate() (admission.Warnings, error) { + linodemachinelog.Info("validate create", "name", r.Name) + + // TODO(user): fill in your validation logic upon object creation. + return nil, nil +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (r *LinodeMachine) ValidateUpdate(old runtime.Object) (admission.Warnings, error) { + linodemachinelog.Info("validate update", "name", r.Name) + + // TODO(user): fill in your validation logic upon object update. + return nil, nil +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (r *LinodeMachine) ValidateDelete() (admission.Warnings, error) { + linodemachinelog.Info("validate delete", "name", r.Name) + + // TODO(user): fill in your validation logic upon object deletion. + return nil, nil +} diff --git a/api/v1alpha1/linodemachine_webhook_test.go b/api/v1alpha1/linodemachine_webhook_test.go new file mode 100644 index 000000000..024267f80 --- /dev/null +++ b/api/v1alpha1/linodemachine_webhook_test.go @@ -0,0 +1,39 @@ +/* +Copyright 2023 Akamai Technologies, Inc. + +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" +) + +var _ = Describe("LinodeMachine Webhook", func() { + + Context("When creating LinodeMachine under Validating Webhook", func() { + It("Should deny if a required field is empty", func() { + + // TODO(user): Add your logic here + + }) + + It("Should admit if all required fields are provided", func() { + + // TODO(user): Add your logic here + + }) + }) + +}) diff --git a/api/v1alpha1/webhook_suite_test.go b/api/v1alpha1/webhook_suite_test.go new file mode 100644 index 000000000..5e5177b37 --- /dev/null +++ b/api/v1alpha1/webhook_suite_test.go @@ -0,0 +1,145 @@ +/* +Copyright 2023 Akamai Technologies, Inc. + +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" + "runtime" + "testing" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + admissionv1 "k8s.io/api/admission/v1" + //+kubebuilder:scaffold:imports + 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 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) + + RunSpecs(t, "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, + + // The BinaryAssetsDirectory is only required if you want to run the tests directly + // without call the makefile target test. If not informed it will look for the + // default path defined in controller-runtime which is /usr/local/kubebuilder/. + // Note that you must have the required binaries setup under the bin directory to perform + // the tests directly. When we run make test it will be setup and used automatically. + BinaryAssetsDirectory: filepath.Join("..", "..", "bin", "k8s", + fmt.Sprintf("1.29.0-%s-%s", runtime.GOOS, runtime.GOARCH)), + + 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 = 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 = (&LinodeMachine{}).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 + } + return conn.Close() + }).Should(Succeed()) + +}) + +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 82d35615e..0e04ddf42 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -23,7 +23,7 @@ package v1alpha1 import ( "github.com/linode/linodego" "k8s.io/api/core/v1" - runtime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/cluster-api/api/v1beta1" "sigs.k8s.io/cluster-api/errors" ) diff --git a/cmd/main.go b/cmd/main.go index 2131b43b2..0dc1d692c 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -149,6 +149,12 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "LinodeObjectStorageBucket") os.Exit(1) } + if os.Getenv("ENABLE_WEBHOOKS") != "false" { + if err = (&infrastructurev1alpha1.LinodeMachine{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "LinodeMachine") + os.Exit(1) + } + } // +kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/config/certmanager/certificate.yaml b/config/certmanager/certificate.yaml new file mode 100644 index 000000000..464d99dad --- /dev/null +++ b/config/certmanager/certificate.yaml @@ -0,0 +1,39 @@ +# The following manifests contain a self-signed issuer CR and a certificate CR. +# More document can be found at https://docs.cert-manager.io +# WARNING: Targets CertManager v1.0. Check https://cert-manager.io/docs/installation/upgrading/ for breaking changes. +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + labels: + app.kubernetes.io/name: certificate + app.kubernetes.io/instance: serving-cert + app.kubernetes.io/component: certificate + app.kubernetes.io/created-by: cluster-api-provider-linode + app.kubernetes.io/part-of: cluster-api-provider-linode + 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: cluster-api-provider-linode + app.kubernetes.io/part-of: cluster-api-provider-linode + 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 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..cf6f89e88 --- /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/crd/kustomization.yaml b/config/crd/kustomization.yaml index 4df4bb7f9..425932932 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -22,7 +22,7 @@ patches: # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. # patches here are for enabling the conversion webhook for each CRD #- path: patches/webhook_in_linodeclusters.yaml -#- path: patches/webhook_in_linodemachines.yaml +- path: patches/webhook_in_linodemachines.yaml #- path: patches/webhook_in_linodemachinetemplates.yaml #- path: patches/webhook_in_linodeclustertemplates.yaml #- path: patches/webhook_in_linodeobjectstoragebuckets.yaml @@ -31,7 +31,7 @@ patches: # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. # patches here are for enabling the CA injection for each CRD #- path: patches/cainjection_in_linodeclusters.yaml -#- path: patches/cainjection_in_linodemachines.yaml +- path: patches/cainjection_in_linodemachines.yaml #- path: patches/cainjection_in_linodemachinetemplates.yaml #- path: patches/cainjection_in_linodeclustertemplates.yaml #- path: patches/cainjection_in_linodeobjectstoragebuckets.yaml diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml index c6e870a70..f51527a19 100644 --- a/config/default/kustomization.yaml +++ b/config/default/kustomization.yaml @@ -15,7 +15,7 @@ resources: - linode-token-secret.yaml # [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 # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. @@ -28,7 +28,7 @@ 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'. # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. diff --git a/config/default/manager_webhook_patch.yaml b/config/default/manager_webhook_patch.yaml new file mode 100644 index 000000000..738de350b --- /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: webhook-server-cert diff --git a/config/default/webhookcainjection_patch.yaml b/config/default/webhookcainjection_patch.yaml new file mode 100644 index 000000000..52ea2ff9f --- /dev/null +++ b/config/default/webhookcainjection_patch.yaml @@ -0,0 +1,29 @@ +# This patch add annotation to admission webhook config and +# CERTIFICATE_NAMESPACE and CERTIFICATE_NAME will be substituted by kustomize +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + labels: + app.kubernetes.io/name: mutatingwebhookconfiguration + app.kubernetes.io/instance: mutating-webhook-configuration + app.kubernetes.io/component: webhook + app.kubernetes.io/created-by: cluster-api-provider-linode + app.kubernetes.io/part-of: cluster-api-provider-linode + app.kubernetes.io/managed-by: kustomize + name: mutating-webhook-configuration + annotations: + cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + labels: + app.kubernetes.io/name: validatingwebhookconfiguration + app.kubernetes.io/instance: validating-webhook-configuration + app.kubernetes.io/component: webhook + app.kubernetes.io/created-by: cluster-api-provider-linode + app.kubernetes.io/part-of: cluster-api-provider-linode + app.kubernetes.io/managed-by: kustomize + name: validating-webhook-configuration + annotations: + cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME diff --git a/config/webhook/kustomization.yaml b/config/webhook/kustomization.yaml new file mode 100644 index 000000000..9cf26134e --- /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 000000000..206316e54 --- /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 000000000..f07c9a056 --- /dev/null +++ b/config/webhook/manifests.yaml @@ -0,0 +1,26 @@ +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + name: validating-webhook-configuration +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-infrastructure-cluster-x-k8s-io-v1alpha1-linodemachine + failurePolicy: Fail + name: vlinodemachine.kb.io + rules: + - apiGroups: + - infrastructure.cluster.x-k8s.io + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - linodemachines + sideEffects: None diff --git a/config/webhook/service.yaml b/config/webhook/service.yaml new file mode 100644 index 000000000..650b99985 --- /dev/null +++ b/config/webhook/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/name: cluster-api-provider-linode + app.kubernetes.io/managed-by: kustomize + name: webhook-service + namespace: system +spec: + ports: + - port: 443 + protocol: TCP + targetPort: 9443 + selector: + control-plane: controller-manager From 832bd00ec8fe32440e9c4adac13f538ae29af9df Mon Sep 17 00:00:00 2001 From: Cindy Bang Date: Mon, 29 Apr 2024 09:00:00 -0400 Subject: [PATCH 5/9] fixup! feat: linodemachine: kubebuilder create webhook --- api/v1alpha1/webhook_suite_test.go | 7 +- config/default/kustomization.yaml | 198 +++++++++---------- config/default/webhookcainjection_patch.yaml | 28 +-- 3 files changed, 118 insertions(+), 115 deletions(-) diff --git a/api/v1alpha1/webhook_suite_test.go b/api/v1alpha1/webhook_suite_test.go index 5e5177b37..c82d4626a 100644 --- a/api/v1alpha1/webhook_suite_test.go +++ b/api/v1alpha1/webhook_suite_test.go @@ -26,9 +26,6 @@ import ( "testing" "time" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - admissionv1 "k8s.io/api/admission/v1" //+kubebuilder:scaffold:imports apimachineryruntime "k8s.io/apimachinery/pkg/runtime" @@ -40,6 +37,9 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log/zap" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" "sigs.k8s.io/controller-runtime/pkg/webhook" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" ) // These tests use Ginkgo (BDD-style Go testing framework). Refer to @@ -51,6 +51,7 @@ var testEnv *envtest.Environment var ctx context.Context var cancel context.CancelFunc +//nolint:paralleltest // Ginkgo bootstrap func TestAPIs(t *testing.T) { RegisterFailHandler(Fail) diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml index f51527a19..4117f8227 100644 --- a/config/default/kustomization.yaml +++ b/config/default/kustomization.yaml @@ -17,7 +17,7 @@ resources: # crd/kustomization.yaml - ../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 @@ -33,104 +33,104 @@ patches: # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. # 'CERTMANAGER' needs to be enabled to use ca injection -#- path: webhookcainjection_patch.yaml +- path: webhookcainjection_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: # Add cert-manager annotation to ValidatingWebhookConfiguration, MutatingWebhookConfiguration and CRDs -# 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 -# - select: -# kind: MutatingWebhookConfiguration -# fieldPaths: -# - .metadata.annotations.[cert-manager.io/inject-ca-from] -# options: -# delimiter: '/' -# index: 0 -# create: true -# - 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: ValidatingWebhookConfiguration -# fieldPaths: -# - .metadata.annotations.[cert-manager.io/inject-ca-from] -# options: -# delimiter: '/' -# index: 1 -# create: true -# - select: -# kind: MutatingWebhookConfiguration -# fieldPaths: -# - .metadata.annotations.[cert-manager.io/inject-ca-from] -# options: -# delimiter: '/' -# index: 1 -# create: true -# - select: -# kind: CustomResourceDefinition -# fieldPaths: -# - .metadata.annotations.[cert-manager.io/inject-ca-from] -# options: -# delimiter: '/' -# index: 1 -# create: true -# - source: # Add cert-manager annotation to the webhook Service -# kind: Service -# version: v1 -# name: webhook-service -# fieldPath: .metadata.name # namespace 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 +replacements: + - source: # Add cert-manager annotation to ValidatingWebhookConfiguration, MutatingWebhookConfiguration and CRDs + 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 + - select: + kind: MutatingWebhookConfiguration + fieldPaths: + - .metadata.annotations.[cert-manager.io/inject-ca-from] + options: + delimiter: '/' + index: 0 + create: true + - 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: ValidatingWebhookConfiguration + fieldPaths: + - .metadata.annotations.[cert-manager.io/inject-ca-from] + options: + delimiter: '/' + index: 1 + create: true + - select: + kind: MutatingWebhookConfiguration + fieldPaths: + - .metadata.annotations.[cert-manager.io/inject-ca-from] + options: + delimiter: '/' + index: 1 + create: true + - select: + kind: CustomResourceDefinition + fieldPaths: + - .metadata.annotations.[cert-manager.io/inject-ca-from] + options: + delimiter: '/' + index: 1 + create: true + - source: # Add cert-manager annotation to the webhook Service + kind: Service + version: v1 + name: webhook-service + fieldPath: .metadata.name # namespace 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 diff --git a/config/default/webhookcainjection_patch.yaml b/config/default/webhookcainjection_patch.yaml index 52ea2ff9f..b61287bfa 100644 --- a/config/default/webhookcainjection_patch.yaml +++ b/config/default/webhookcainjection_patch.yaml @@ -1,18 +1,20 @@ # This patch add annotation to admission webhook config and # CERTIFICATE_NAMESPACE and CERTIFICATE_NAME will be substituted by kustomize -apiVersion: admissionregistration.k8s.io/v1 -kind: MutatingWebhookConfiguration -metadata: - labels: - app.kubernetes.io/name: mutatingwebhookconfiguration - app.kubernetes.io/instance: mutating-webhook-configuration - app.kubernetes.io/component: webhook - app.kubernetes.io/created-by: cluster-api-provider-linode - app.kubernetes.io/part-of: cluster-api-provider-linode - app.kubernetes.io/managed-by: kustomize - name: mutating-webhook-configuration - annotations: - cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME +# TODO: To enable mutating admission webhooks, uncomment the following +# MutatingWebhookConfiguration patch. +# apiVersion: admissionregistration.k8s.io/v1 +# kind: MutatingWebhookConfiguration +# metadata: +# labels: +# app.kubernetes.io/name: mutatingwebhookconfiguration +# app.kubernetes.io/instance: mutating-webhook-configuration +# app.kubernetes.io/component: webhook +# app.kubernetes.io/created-by: cluster-api-provider-linode +# app.kubernetes.io/part-of: cluster-api-provider-linode +# app.kubernetes.io/managed-by: kustomize +# name: mutating-webhook-configuration +# annotations: +# cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME --- apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingWebhookConfiguration From 066138efbefced18b6898cc09928507d989d5a00 Mon Sep 17 00:00:00 2001 From: Cindy Bang Date: Mon, 29 Apr 2024 09:00:00 -0400 Subject: [PATCH 6/9] tiltfile: add validation webhook --- Tiltfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Tiltfile b/Tiltfile index 947b612fa..37cb500a6 100644 --- a/Tiltfile +++ b/Tiltfile @@ -128,6 +128,9 @@ k8s_resource( "capl-manager-rolebinding:clusterrolebinding", "capl-proxy-rolebinding:clusterrolebinding", "capl-manager-credentials:secret", + "capl-serving-cert:certificate", + "capl-selfsigned-issuer:issuer", + "capl-validating-webhook-configuration:validatingwebhookconfiguration", ], resource_deps=["capi-controller-manager"], labels=["CAPL"], From 4ab0d88f2478313438adf1f732d0eaa8abdb7d9c Mon Sep 17 00:00:00 2001 From: Cindy Bang Date: Tue, 7 May 2024 16:56:56 -0400 Subject: [PATCH 7/9] chore: add webhook toggle Webhooks can be enabled or disabled by setting the `ENABLE_WEBHOOKS` environment variable during deployment. --- Makefile | 2 ++ Tiltfile | 3 +++ config/default/.gitignore | 1 + config/default/kustomization.yaml | 20 +++++++++++++++++++- config/default/manager-configmap.yaml | 7 +++++++ config/default/manager_webhook_patch.yaml | 6 ++++++ 6 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 config/default/manager-configmap.yaml diff --git a/Makefile b/Makefile index b3109b500..f44e5f047 100644 --- a/Makefile +++ b/Makefile @@ -152,6 +152,7 @@ e2etest: generate local-release local-deploy chainsaw local-deploy: kind ctlptl tilt kustomize clusterctl @echo -n "LINODE_TOKEN=$(LINODE_TOKEN)" > config/default/.env.linode + @echo -n "ENABLE_WEBHOOKS=$(ENABLE_WEBHOOKS)" > config/default/.env.manager $(CTLPTL) apply -f .tilt/ctlptl-config.yaml $(TILT) ci -f Tiltfile @@ -204,6 +205,7 @@ endif .PHONY: tilt-cluster tilt-cluster: ctlptl tilt kind clusterctl @echo -n "LINODE_TOKEN=$(LINODE_TOKEN)" > config/default/.env.linode + @echo -n "ENABLE_WEBHOOKS=$(ENABLE_WEBHOOKS)" > config/default/.env.manager $(CTLPTL) apply -f .tilt/ctlptl-config.yaml $(TILT) up --stream diff --git a/Tiltfile b/Tiltfile index 37cb500a6..7898164d4 100644 --- a/Tiltfile +++ b/Tiltfile @@ -98,6 +98,8 @@ for resource in manager_yaml: resource["stringData"]["apiToken"] = os.getenv("LINODE_TOKEN") if resource["kind"] == "CustomResourceDefinition" and resource["spec"]["group"] == "infrastructure.cluster.x-k8s.io": resource["metadata"]["labels"]["clusterctl.cluster.x-k8s.io"] = "" + if resource["metadata"]["name"] == "capl-manager-config": + resource["data"]["ENABLE_WEBHOOKS"] = os.getenv("ENABLE_WEBHOOKS", "true") k8s_yaml(encode_yaml_stream(manager_yaml)) if os.getenv("SKIP_DOCKER_BUILD", "false") != "true": @@ -128,6 +130,7 @@ k8s_resource( "capl-manager-rolebinding:clusterrolebinding", "capl-proxy-rolebinding:clusterrolebinding", "capl-manager-credentials:secret", + "capl-manager-config:configmap", "capl-serving-cert:certificate", "capl-selfsigned-issuer:issuer", "capl-validating-webhook-configuration:validatingwebhookconfiguration", diff --git a/config/default/.gitignore b/config/default/.gitignore index 49686d40f..9ee9538a6 100644 --- a/config/default/.gitignore +++ b/config/default/.gitignore @@ -1 +1,2 @@ .env.linode +.env.manager diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml index 4117f8227..3cbb5398c 100644 --- a/config/default/kustomization.yaml +++ b/config/default/kustomization.yaml @@ -13,6 +13,7 @@ resources: - ../rbac - ../manager - linode-token-secret.yaml +- manager-configmap.yaml # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # crd/kustomization.yaml - ../webhook @@ -35,9 +36,26 @@ patches: # 'CERTMANAGER' needs to be enabled to use ca injection - path: webhookcainjection_patch.yaml +replacements: + +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in +# crd/kustomization.yaml + - source: + kind: ConfigMap + version: v1 + name: manager-config # this name should match the one in manager-configmap.yaml + fieldPath: .metadata.name + targets: + - select: + kind: Deployment + group: apps + version: v1 + name: controller-manager + fieldPaths: + - .spec.template.spec.containers.[name=manager].env.[name=ENABLE_WEBHOOKS].valueFrom.configMapKeyRef.name + # [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: # Add cert-manager annotation to ValidatingWebhookConfiguration, MutatingWebhookConfiguration and CRDs kind: Certificate group: cert-manager.io diff --git a/config/default/manager-configmap.yaml b/config/default/manager-configmap.yaml new file mode 100644 index 000000000..f5b12264d --- /dev/null +++ b/config/default/manager-configmap.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: manager-config +data: + ENABLE_WEBHOOKS: ${ENABLE_WEBHOOKS} diff --git a/config/default/manager_webhook_patch.yaml b/config/default/manager_webhook_patch.yaml index 738de350b..f0a67f1c7 100644 --- a/config/default/manager_webhook_patch.yaml +++ b/config/default/manager_webhook_patch.yaml @@ -8,6 +8,12 @@ spec: spec: containers: - name: manager + env: + - name: ENABLE_WEBHOOKS + valueFrom: + configMapKeyRef: + name: manager-config + key: ENABLE_WEBHOOKS ports: - containerPort: 9443 name: webhook-server From 9cb9c2ee05199f4facf72cbc8983ab406f867bc9 Mon Sep 17 00:00:00 2001 From: Cindy Bang Date: Mon, 29 Apr 2024 09:00:00 -0400 Subject: [PATCH 8/9] api: linodemachine: add create validation --- api/v1alpha1/linodemachine_webhook.go | 136 ++++++++++++++++++++- api/v1alpha1/linodemachine_webhook_test.go | 130 +++++++++++++++++--- api/v1alpha1/webhook_helpers.go | 40 ++++++ clients/clients.go | 1 + config/webhook/manifests.yaml | 1 - mock/client.go | 30 +++++ 6 files changed, 319 insertions(+), 19 deletions(-) create mode 100644 api/v1alpha1/webhook_helpers.go diff --git a/api/v1alpha1/linodemachine_webhook.go b/api/v1alpha1/linodemachine_webhook.go index af8740573..8c818bb0a 100644 --- a/api/v1alpha1/linodemachine_webhook.go +++ b/api/v1alpha1/linodemachine_webhook.go @@ -17,11 +17,37 @@ limitations under the License. package v1alpha1 import ( + "context" + "fmt" + "slices" + + "github.com/linode/linodego" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/validation/field" ctrl "sigs.k8s.io/controller-runtime" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/webhook" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + . "github.com/linode/cluster-api-provider-linode/clients" +) + +var ( + // The list of valid device slots that data device disks may attach to. + // NOTE: sda is reserved for the OS device disk. + LinodeMachineDevicePaths = []string{"sdb", "sdc", "sdd", "sde", "sdf", "sdg", "sdh"} + + // The maximum number of device disks allowed per [Configuration Profile per Linode’s Instance]. + // + // [Configuration Profile per Linode’s Instance]: https://www.linode.com/docs/api/linode-instances/#configuration-profile-view + LinodeMachineMaxDisk = 8 + + // The maximum number of data device disks allowed in a Linode’s Instance's configuration profile. + // NOTE: The first device disk is reserved for the OS disk + LinodeMachineMaxDataDisk = LinodeMachineMaxDisk - 1 ) // log is for logging in this package. @@ -36,8 +62,8 @@ func (r *LinodeMachine) SetupWebhookWithManager(mgr ctrl.Manager) error { // TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! -// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. -//+kubebuilder:webhook:path=/validate-infrastructure-cluster-x-k8s-io-v1alpha1-linodemachine,mutating=false,failurePolicy=fail,sideEffects=None,groups=infrastructure.cluster.x-k8s.io,resources=linodemachines,verbs=create;update,versions=v1alpha1,name=vlinodemachine.kb.io,admissionReviewVersions=v1 +// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable updation and deletion validation. +//+kubebuilder:webhook:path=/validate-infrastructure-cluster-x-k8s-io-v1alpha1-linodemachine,mutating=false,failurePolicy=fail,sideEffects=None,groups=infrastructure.cluster.x-k8s.io,resources=linodemachines,verbs=create,versions=v1alpha1,name=vlinodemachine.kb.io,admissionReviewVersions=v1 var _ webhook.Validator = &LinodeMachine{} @@ -45,8 +71,10 @@ var _ webhook.Validator = &LinodeMachine{} func (r *LinodeMachine) ValidateCreate() (admission.Warnings, error) { linodemachinelog.Info("validate create", "name", r.Name) - // TODO(user): fill in your validation logic upon object creation. - return nil, nil + ctx, cancel := context.WithTimeout(context.Background(), defaultWebhookTimeout) + defer cancel() + + return nil, r.validateLinodeMachine(ctx, &defaultLinodeClient) } // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type @@ -64,3 +92,103 @@ func (r *LinodeMachine) ValidateDelete() (admission.Warnings, error) { // TODO(user): fill in your validation logic upon object deletion. return nil, nil } + +func (r *LinodeMachine) validateLinodeMachine(ctx context.Context, client LinodeClient) error { + var errs field.ErrorList + + if err := r.validateLinodeMachineSpec(ctx, client); err != nil { + errs = slices.Concat(errs, err) + } + + if len(errs) == 0 { + return nil + } + return apierrors.NewInvalid( + schema.GroupKind{Group: "infrastructure.cluster.x-k8s.io", Kind: "LinodeMachine"}, + r.Name, errs) +} + +func (r *LinodeMachine) validateLinodeMachineSpec(ctx context.Context, client LinodeClient) field.ErrorList { + var errs field.ErrorList + + if err := validateRegion(ctx, client, r.Spec.Region, field.NewPath("spec").Child("region")); err != nil { + errs = append(errs, err) + } + plan, err := validateLinodeType(ctx, client, r.Spec.Type, field.NewPath("spec").Child("type")) + if err != nil { + errs = append(errs, err) + } + if err := r.validateLinodeMachineDisks(plan); err != nil { + errs = append(errs, err) + } + + if len(errs) == 0 { + return nil + } + return errs +} + +func (r *LinodeMachine) validateLinodeMachineDisks(plan *linodego.LinodeType) *field.Error { + // The Linode plan information is required to perform disk validation + if plan == nil { + return nil + } + + var ( + // The Linode API represents storage sizes in megabytes (MB) + // https://www.linode.com/docs/api/linode-types/#type-view + planSize = resource.MustParse(fmt.Sprintf("%d%s", plan.Disk, "M")) + remainSize = &resource.Quantity{} + err *field.Error + ) + planSize.DeepCopyInto(remainSize) + + if remainSize, err = validateDisk(r.Spec.OSDisk, field.NewPath("spec").Child("osDisk"), remainSize, &planSize); err != nil { + return err + } + if _, err := validateDataDisks(r.Spec.DataDisks, field.NewPath("spec").Child("dataDisks"), remainSize, &planSize); err != nil { + return err + } + + return nil +} + +func validateDataDisks(disks map[string]*InstanceDisk, path *field.Path, remainSize, planSize *resource.Quantity) (*resource.Quantity, *field.Error) { + devs := []string{} + + for dev, disk := range disks { + if !slices.Contains(LinodeMachineDevicePaths, dev) { + return nil, field.Forbidden(path.Child(dev), fmt.Sprintf("allowed device paths: %v", LinodeMachineDevicePaths)) + } + if slices.Contains(devs, dev) { + return nil, field.Duplicate(path.Child(dev), "duplicate device path") + } + devs = append(devs, dev) + if len(devs) > LinodeMachineMaxDataDisk { + return nil, field.TooMany(path, len(devs), LinodeMachineMaxDataDisk) + } + + var err *field.Error + if remainSize, err = validateDisk(disk, path.Child(dev), remainSize, planSize); err != nil { + return nil, err + } + } + return remainSize, nil +} + +func validateDisk(disk *InstanceDisk, path *field.Path, remainSize, planSize *resource.Quantity) (*resource.Quantity, *field.Error) { + if disk == nil { + return remainSize, nil + } + + if disk.Size.Sign() < 1 { + return nil, field.Invalid(path, disk.Size.String(), "invalid size") + } + if remainSize.Cmp(disk.Size) == -1 { + return nil, field.Invalid(path, disk.Size.String(), fmt.Sprintf("sum disk sizes exceeds plan storage: %s", planSize.String())) + } + + // Decrement the remaining amount of space available + remainSize.Sub(disk.Size) + return remainSize, nil +} diff --git a/api/v1alpha1/linodemachine_webhook_test.go b/api/v1alpha1/linodemachine_webhook_test.go index 024267f80..6fd51a2d1 100644 --- a/api/v1alpha1/linodemachine_webhook_test.go +++ b/api/v1alpha1/linodemachine_webhook_test.go @@ -17,23 +17,125 @@ limitations under the License. package v1alpha1 import ( - . "github.com/onsi/ginkgo/v2" -) - -var _ = Describe("LinodeMachine Webhook", func() { + "context" + "errors" + "math" + "strconv" + "testing" - Context("When creating LinodeMachine under Validating Webhook", func() { - It("Should deny if a required field is empty", func() { + "github.com/linode/linodego" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - // TODO(user): Add your logic here + "github.com/linode/cluster-api-provider-linode/mock" - }) - - It("Should admit if all required fields are provided", func() { + . "github.com/linode/cluster-api-provider-linode/mock/mocktest" +) - // TODO(user): Add your logic here +func TestValidateLinodeMachine(t *testing.T) { + t.Parallel() - }) - }) + var ( + machine = LinodeMachine{ + ObjectMeta: metav1.ObjectMeta{ + Name: "example", + Namespace: "example", + }, + Spec: LinodeMachineSpec{ + Region: "example", + Type: "example", + }, + } + disk = InstanceDisk{Size: resource.MustParse("1G")} + disk_zero = InstanceDisk{Size: *resource.NewQuantity(0, resource.BinarySI)} + plan = linodego.LinodeType{Disk: 2 * int(disk.Size.ScaledValue(resource.Mega))} + plan_zero = linodego.LinodeType{Disk: 0} + plan_max = linodego.LinodeType{Disk: math.MaxInt} + ) -}) + NewSuite(t, mock.MockLinodeClient{}).Run( + OneOf( + Path( + Call("valid", func(ctx context.Context, mck Mock) { + mck.LinodeClient.EXPECT().GetRegion(gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes() + mck.LinodeClient.EXPECT().GetType(gomock.Any(), gomock.Any()).Return(&plan_max, nil).AnyTimes() + }), + Result("success", func(ctx context.Context, mck Mock) { + assert.NoError(t, machine.validateLinodeMachine(ctx, mck.LinodeClient)) + }), + Call("valid with disks", func(ctx context.Context, mck Mock) { + mck.LinodeClient.EXPECT().GetRegion(gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes() + mck.LinodeClient.EXPECT().GetType(gomock.Any(), gomock.Any()).Return(&plan_max, nil).AnyTimes() + }), + Result("success", func(ctx context.Context, mck Mock) { + machine := machine + machine.Spec.OSDisk = disk.DeepCopy() + machine.Spec.DataDisks = map[string]*InstanceDisk{"sdb": disk.DeepCopy()} + assert.NoError(t, machine.validateLinodeMachine(ctx, mck.LinodeClient)) + }), + ), + ), + OneOf( + Path(Call("invalid region", func(ctx context.Context, mck Mock) { + mck.LinodeClient.EXPECT().GetRegion(gomock.Any(), gomock.Any()).Return(nil, errors.New("invalid region")).AnyTimes() + mck.LinodeClient.EXPECT().GetType(gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes() + })), + Path(Call("invalid type", func(ctx context.Context, mck Mock) { + mck.LinodeClient.EXPECT().GetRegion(gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes() + mck.LinodeClient.EXPECT().GetType(gomock.Any(), gomock.Any()).Return(nil, errors.New("invalid type")).AnyTimes() + })), + ), + Result("error", func(ctx context.Context, mck Mock) { + assert.Error(t, machine.validateLinodeMachine(ctx, mck.LinodeClient)) + }), + OneOf( + Path( + Call("exceed plan storage", func(ctx context.Context, mck Mock) { + mck.LinodeClient.EXPECT().GetRegion(gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes() + mck.LinodeClient.EXPECT().GetType(gomock.Any(), gomock.Any()).Return(&plan_zero, nil).AnyTimes() + }), + Result("os disk too large", func(ctx context.Context, mck Mock) { + machine := machine + machine.Spec.OSDisk = disk.DeepCopy() + assert.ErrorContains(t, machine.validateLinodeMachine(ctx, mck.LinodeClient), strconv.Itoa(plan_zero.Disk)) + }), + ), + Path( + Call("exceed plan storage", func(ctx context.Context, mck Mock) { + mck.LinodeClient.EXPECT().GetRegion(gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes() + mck.LinodeClient.EXPECT().GetType(gomock.Any(), gomock.Any()).Return(&plan, nil).AnyTimes() + }), + Result("data disk too large", func(ctx context.Context, mck Mock) { + machine := machine + machine.Spec.OSDisk = disk.DeepCopy() + machine.Spec.DataDisks = map[string]*InstanceDisk{"sdb": disk.DeepCopy(), "sdc": disk.DeepCopy()} + assert.Error(t, machine.validateLinodeMachine(ctx, mck.LinodeClient)) + }), + ), + Path( + Call("data disk invalid path", func(ctx context.Context, mck Mock) { + mck.LinodeClient.EXPECT().GetRegion(gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes() + mck.LinodeClient.EXPECT().GetType(gomock.Any(), gomock.Any()).Return(&plan_max, nil).AnyTimes() + }), + Result("error", func(ctx context.Context, mck Mock) { + machine := machine + machine.Spec.DataDisks = map[string]*InstanceDisk{"sda": disk.DeepCopy()} + assert.Error(t, machine.validateLinodeMachine(ctx, mck.LinodeClient)) + }), + ), + Path( + Call("invalid disk size", func(ctx context.Context, mck Mock) { + mck.LinodeClient.EXPECT().GetRegion(gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes() + mck.LinodeClient.EXPECT().GetType(gomock.Any(), gomock.Any()).Return(&plan_max, nil).AnyTimes() + }), + Result("error", func(ctx context.Context, mck Mock) { + machine := machine + machine.Spec.OSDisk = disk_zero.DeepCopy() + assert.Error(t, machine.validateLinodeMachine(ctx, mck.LinodeClient)) + }), + ), + ), + ) +} diff --git a/api/v1alpha1/webhook_helpers.go b/api/v1alpha1/webhook_helpers.go new file mode 100644 index 000000000..eb9e40610 --- /dev/null +++ b/api/v1alpha1/webhook_helpers.go @@ -0,0 +1,40 @@ +package v1alpha1 + +import ( + "context" + "net/http" + "time" + + "github.com/linode/linodego" + "k8s.io/apimachinery/pkg/util/validation/field" + + . "github.com/linode/cluster-api-provider-linode/clients" +) + +const ( + // defaultWebhookTimeout is the default timeout for an admission request + defaultWebhookTimeout = time.Minute +) + +var ( + // defaultLinodeClient is an unauthenticated Linode client + defaultLinodeClient = linodego.NewClient(http.DefaultClient) +) + +func validateRegion(ctx context.Context, client LinodeClient, id string, path *field.Path) *field.Error { + _, err := client.GetRegion(ctx, id) + if err != nil { + return field.NotFound(path, id) + } + + return nil +} + +func validateLinodeType(ctx context.Context, client LinodeClient, id string, path *field.Path) (*linodego.LinodeType, *field.Error) { + plan, err := client.GetType(ctx, id) + if err != nil { + return nil, field.NotFound(path, id) + } + + return plan, nil +} diff --git a/clients/clients.go b/clients/clients.go index 771f21352..7784cad22 100644 --- a/clients/clients.go +++ b/clients/clients.go @@ -34,6 +34,7 @@ type LinodeInstanceClient interface { GetImage(ctx context.Context, imageID string) (*linodego.Image, error) CreateStackscript(ctx context.Context, opts linodego.StackscriptCreateOptions) (*linodego.Stackscript, error) ListStackscripts(ctx context.Context, opts *linodego.ListOptions) ([]linodego.Stackscript, error) + GetType(ctx context.Context, typeID string) (*linodego.LinodeType, error) } // LinodeVPCClient defines the methods that interact with Linode's VPC service. diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml index f07c9a056..6f148c7fb 100644 --- a/config/webhook/manifests.yaml +++ b/config/webhook/manifests.yaml @@ -20,7 +20,6 @@ webhooks: - v1alpha1 operations: - CREATE - - UPDATE resources: - linodemachines sideEffects: None diff --git a/mock/client.go b/mock/client.go index aed1a3ed8..10f339af0 100644 --- a/mock/client.go +++ b/mock/client.go @@ -368,6 +368,21 @@ func (mr *MockLinodeClientMockRecorder) GetRegion(ctx, regionID any) *gomock.Cal return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRegion", reflect.TypeOf((*MockLinodeClient)(nil).GetRegion), ctx, regionID) } +// GetType mocks base method. +func (m *MockLinodeClient) GetType(ctx context.Context, typeID string) (*linodego.LinodeType, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetType", ctx, typeID) + ret0, _ := ret[0].(*linodego.LinodeType) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetType indicates an expected call of GetType. +func (mr *MockLinodeClientMockRecorder) GetType(ctx, typeID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetType", reflect.TypeOf((*MockLinodeClient)(nil).GetType), ctx, typeID) +} + // GetVPC mocks base method. func (m *MockLinodeClient) GetVPC(ctx context.Context, vpcID int) (*linodego.VPC, error) { m.ctrl.T.Helper() @@ -658,6 +673,21 @@ func (mr *MockLinodeInstanceClientMockRecorder) GetRegion(ctx, regionID any) *go return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRegion", reflect.TypeOf((*MockLinodeInstanceClient)(nil).GetRegion), ctx, regionID) } +// GetType mocks base method. +func (m *MockLinodeInstanceClient) GetType(ctx context.Context, typeID string) (*linodego.LinodeType, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetType", ctx, typeID) + ret0, _ := ret[0].(*linodego.LinodeType) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetType indicates an expected call of GetType. +func (mr *MockLinodeInstanceClientMockRecorder) GetType(ctx, typeID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetType", reflect.TypeOf((*MockLinodeInstanceClient)(nil).GetType), ctx, typeID) +} + // ListInstanceConfigs mocks base method. func (m *MockLinodeInstanceClient) ListInstanceConfigs(ctx context.Context, linodeID int, opts *linodego.ListOptions) ([]linodego.InstanceConfig, error) { m.ctrl.T.Helper() From 3016eec07f5d2e77d9ffc5b066dc86694f5149ab Mon Sep 17 00:00:00 2001 From: Cindy Bang Date: Mon, 29 Apr 2024 09:00:00 -0400 Subject: [PATCH 9/9] chore: e2e: use 6th Generation Linode plans Use the latest 6th Generation Linode plans (announced in May 2018). The 5th Generation plans are publicly deprecated. See: https://www.linode.com/blog/linode/updated-linode-plans-new-larger-linodes/ --- .../minimal-linodemachine/assert-linodemachine.yaml | 2 +- .../minimal-linodemachine/create-linodemachine.yaml | 2 +- .../vpc-integration/assert-linodemachine.yaml | 2 +- .../vpc-integration/create-linodemachine.yaml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/e2e/linodemachine-controller/minimal-linodemachine/assert-linodemachine.yaml b/e2e/linodemachine-controller/minimal-linodemachine/assert-linodemachine.yaml index 0d1c16160..d71378e52 100644 --- a/e2e/linodemachine-controller/minimal-linodemachine/assert-linodemachine.yaml +++ b/e2e/linodemachine-controller/minimal-linodemachine/assert-linodemachine.yaml @@ -6,7 +6,7 @@ metadata: cluster.x-k8s.io/cluster-name: ($cluster) spec: region: us-sea - type: g5-nanode-1 + type: g6-nanode-1 status: ready: true instanceState: running diff --git a/e2e/linodemachine-controller/minimal-linodemachine/create-linodemachine.yaml b/e2e/linodemachine-controller/minimal-linodemachine/create-linodemachine.yaml index 67b11b6fd..af9b6d719 100644 --- a/e2e/linodemachine-controller/minimal-linodemachine/create-linodemachine.yaml +++ b/e2e/linodemachine-controller/minimal-linodemachine/create-linodemachine.yaml @@ -28,5 +28,5 @@ spec: template: spec: region: us-sea - type: g5-nanode-1 + type: g6-nanode-1 diff --git a/e2e/linodemachine-controller/vpc-integration/assert-linodemachine.yaml b/e2e/linodemachine-controller/vpc-integration/assert-linodemachine.yaml index 0d1c16160..d71378e52 100644 --- a/e2e/linodemachine-controller/vpc-integration/assert-linodemachine.yaml +++ b/e2e/linodemachine-controller/vpc-integration/assert-linodemachine.yaml @@ -6,7 +6,7 @@ metadata: cluster.x-k8s.io/cluster-name: ($cluster) spec: region: us-sea - type: g5-nanode-1 + type: g6-nanode-1 status: ready: true instanceState: running diff --git a/e2e/linodemachine-controller/vpc-integration/create-linodemachine.yaml b/e2e/linodemachine-controller/vpc-integration/create-linodemachine.yaml index 0cdbaf44a..8b5c27f2b 100644 --- a/e2e/linodemachine-controller/vpc-integration/create-linodemachine.yaml +++ b/e2e/linodemachine-controller/vpc-integration/create-linodemachine.yaml @@ -28,4 +28,4 @@ spec: template: spec: region: us-sea - type: g5-nanode-1 + type: g6-nanode-1