From 45ca1ec27844459efdc0d1e70d3364c8341e56ef Mon Sep 17 00:00:00 2001 From: dwertent Date: Tue, 31 Dec 2024 15:42:17 -0500 Subject: [PATCH] Adding an admission controller Signed-off-by: dwertent --- operator/PROJECT | 12 ++ .../v1alpha1/paladinregistration_webhook.go | 54 ++++++ .../paladinregistration_webhook_test.go | 89 ++++++++++ .../api/v1alpha1/paladinregistry_webhook.go | 55 +++++++ .../v1alpha1/paladinregistry_webhook_test.go | 96 +++++++++++ .../smartcontractdeployment_webhook.go | 52 ++++++ .../smartcontractdeployment_webhook_test.go | 88 ++++++++++ .../api/v1alpha1/transactioninvoke_webhook.go | 54 ++++++ .../transactioninvoke_webhook_test.go | 85 ++++++++++ operator/api/v1alpha1/webhook_suite_test.go | 154 ++++++++++++++++++ .../api/v1alpha1/zz_generated.deepcopy.go | 2 +- .../templates/operator/certificate.yaml | 31 ++++ .../templates/operator/deployment.yaml | 22 ++- .../templates/operator/manifests.yaml | 100 ++++++++++++ .../templates/operator/service.yaml | 9 +- operator/charts/paladin-operator/values.yaml | 5 + operator/cmd/main.go | 24 +++ operator/config/certmanager/certificate.yaml | 35 ++++ .../config/certmanager/kustomization.yaml | 5 + .../config/certmanager/kustomizeconfig.yaml | 8 + operator/config/crd/kustomization.yaml | 8 +- .../cainjection_in_paladinregistrations.yaml | 7 + .../cainjection_in_paladinregistries.yaml | 7 + ...injection_in_smartcontractdeployments.yaml | 7 + .../cainjection_in_transactioninvokes.yaml | 7 + .../webhook_in_paladinregistrations.yaml | 16 ++ .../patches/webhook_in_paladinregistries.yaml | 16 ++ .../webhook_in_smartcontractdeployments.yaml | 16 ++ .../webhook_in_transactioninvokes.yaml | 16 ++ operator/config/default/kustomization.yaml | 4 +- .../config/default/manager_webhook_patch.yaml | 23 +++ .../default/webhookcainjection_patch.yaml | 25 +++ operator/config/webhook/kustomization.yaml | 6 + operator/config/webhook/kustomizeconfig.yaml | 22 +++ operator/config/webhook/manifests.yaml | 90 ++++++++++ operator/config/webhook/service.yaml | 15 ++ .../paladinregistration_controller.go | 2 - .../smartcontractdeployment_controller.go | 2 - .../transactioninvoke_controller.go | 2 - 39 files changed, 1256 insertions(+), 15 deletions(-) create mode 100644 operator/api/v1alpha1/paladinregistration_webhook.go create mode 100644 operator/api/v1alpha1/paladinregistration_webhook_test.go create mode 100644 operator/api/v1alpha1/paladinregistry_webhook.go create mode 100644 operator/api/v1alpha1/paladinregistry_webhook_test.go create mode 100644 operator/api/v1alpha1/smartcontractdeployment_webhook.go create mode 100644 operator/api/v1alpha1/smartcontractdeployment_webhook_test.go create mode 100644 operator/api/v1alpha1/transactioninvoke_webhook.go create mode 100644 operator/api/v1alpha1/transactioninvoke_webhook_test.go create mode 100644 operator/api/v1alpha1/webhook_suite_test.go create mode 100644 operator/charts/paladin-operator/templates/operator/certificate.yaml create mode 100644 operator/charts/paladin-operator/templates/operator/manifests.yaml create mode 100644 operator/config/certmanager/certificate.yaml create mode 100644 operator/config/certmanager/kustomization.yaml create mode 100644 operator/config/certmanager/kustomizeconfig.yaml create mode 100644 operator/config/crd/patches/cainjection_in_paladinregistrations.yaml create mode 100644 operator/config/crd/patches/cainjection_in_paladinregistries.yaml create mode 100644 operator/config/crd/patches/cainjection_in_smartcontractdeployments.yaml create mode 100644 operator/config/crd/patches/cainjection_in_transactioninvokes.yaml create mode 100644 operator/config/crd/patches/webhook_in_paladinregistrations.yaml create mode 100644 operator/config/crd/patches/webhook_in_paladinregistries.yaml create mode 100644 operator/config/crd/patches/webhook_in_smartcontractdeployments.yaml create mode 100644 operator/config/crd/patches/webhook_in_transactioninvokes.yaml create mode 100644 operator/config/default/manager_webhook_patch.yaml create mode 100644 operator/config/default/webhookcainjection_patch.yaml create mode 100644 operator/config/webhook/kustomization.yaml create mode 100644 operator/config/webhook/kustomizeconfig.yaml create mode 100644 operator/config/webhook/manifests.yaml create mode 100644 operator/config/webhook/service.yaml diff --git a/operator/PROJECT b/operator/PROJECT index 4b43ebc8b..bd7beafc6 100644 --- a/operator/PROJECT +++ b/operator/PROJECT @@ -29,6 +29,9 @@ resources: kind: SmartContractDeployment path: github.com/kaleido-io/paladin/operator/api/v1alpha1 version: v1alpha1 + webhooks: + validation: true + webhookVersion: v1 - api: crdVersion: v1 namespaced: true @@ -56,6 +59,9 @@ resources: kind: PaladinRegistry path: github.com/kaleido-io/paladin/operator/api/v1alpha1 version: v1alpha1 + webhooks: + validation: true + webhookVersion: v1 - api: crdVersion: v1 namespaced: true @@ -74,6 +80,9 @@ resources: kind: PaladinRegistration path: github.com/kaleido-io/paladin/operator/api/v1alpha1 version: v1alpha1 + webhooks: + validation: true + webhookVersion: v1 - api: crdVersion: v1 namespaced: true @@ -83,4 +92,7 @@ resources: kind: TransactionInvoke path: github.com/kaleido-io/paladin/operator/api/v1alpha1 version: v1alpha1 + webhooks: + validation: true + webhookVersion: v1 version: "3" diff --git a/operator/api/v1alpha1/paladinregistration_webhook.go b/operator/api/v1alpha1/paladinregistration_webhook.go new file mode 100644 index 000000000..8667ece61 --- /dev/null +++ b/operator/api/v1alpha1/paladinregistration_webhook.go @@ -0,0 +1,54 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package v1alpha1 + +import ( + "fmt" + + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +// SetupWebhookWithManager will setup the manager to manage the webhooks +func (r *PaladinRegistration) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(r). + Complete() +} + +// +kubebuilder:webhook:path=/validate-core-paladin-io-v1alpha1-paladinregistration,mutating=false,failurePolicy=fail,sideEffects=None,groups=core.paladin.io,resources=paladinregistrations,verbs=create;update;delete,versions=v1alpha1,name=vpaladinregistration.kb.io,admissionReviewVersions=v1 + +var _ webhook.Validator = &PaladinRegistration{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (r *PaladinRegistration) ValidateCreate() (admission.Warnings, error) { + // Allow creation + return nil, nil +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (r *PaladinRegistration) ValidateUpdate(old runtime.Object) (admission.Warnings, error) { + // Deny any update + return nil, fmt.Errorf("updates to PaladinRegistration resources are not allowed as they have already been submitted to the blockchain and cannot be modified") +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (r *PaladinRegistration) ValidateDelete() (admission.Warnings, error) { + // Deny any delete + return nil, fmt.Errorf("deletions of PaladinRegistration resources are not allowed as they have already been submitted to the blockchain and cannot be deleted") +} diff --git a/operator/api/v1alpha1/paladinregistration_webhook_test.go b/operator/api/v1alpha1/paladinregistration_webhook_test.go new file mode 100644 index 000000000..d94925e10 --- /dev/null +++ b/operator/api/v1alpha1/paladinregistration_webhook_test.go @@ -0,0 +1,89 @@ +/* +Copyright 2024. + +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" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +var _ = Describe("PaladinRegistration Webhook - Block Update & Delete", func() { + + ctx := context.Background() + + key := types.NamespacedName{ + Name: "resource", + Namespace: "default", + } + + pr := &PaladinRegistration{} + + BeforeEach(func() { + // Create a valid PaladinRegistration that should pass creation validation + err := k8sClient.Get(ctx, key, pr) + if err != nil && errors.IsNotFound(err) { + pr = &PaladinRegistration{ + ObjectMeta: metav1.ObjectMeta{ + Name: key.Name, + Namespace: key.Namespace, + }, + Spec: PaladinRegistrationSpec{ + Registry: "evm-registry", + RegistryAdminNode: "node1", + RegistryAdminKey: "deployKey", + Node: "node1", + NodeKey: "registryAdmin", + Transports: []string{"grpc"}, + }, + } + Expect(k8sClient.Create(ctx, pr)).To(Succeed()) + } + }) + + Context("When attempting to update an existing PaladinRegistration", func() { + It("Should block the update and return an error", func() { + // Fetch the newly created resource to ensure we have the latest version + fetched := &PaladinRegistration{} + err := k8sClient.Get(context.Background(), key, fetched) + Expect(err).NotTo(HaveOccurred()) + + fetched.Spec.Registry = "other-registry" + + // Attempt to update, expecting the webhook to block + By("Updating PaladinRegistration, expecting a webhook error") + err = k8sClient.Update(context.Background(), fetched) + Expect(err).To(HaveOccurred()) + // Check the error message from the webhook + Expect(err.Error()).To(ContainSubstring("updates to PaladinRegistration resources are not allowed")) + }) + }) + + Context("When attempting to delete an existing PaladinRegistration", func() { + It("Should block the delete and return an error", func() { + // Attempt to delete, expecting the webhook to block + By("Deleting PaladinRegistration, expecting a webhook error") + err := k8sClient.Delete(context.Background(), pr) + Expect(err).To(HaveOccurred()) + // Check the error message from the webhook + Expect(err.Error()).To(ContainSubstring("deletions of PaladinRegistration resources are not allowed")) + }) + }) +}) diff --git a/operator/api/v1alpha1/paladinregistry_webhook.go b/operator/api/v1alpha1/paladinregistry_webhook.go new file mode 100644 index 000000000..86193a423 --- /dev/null +++ b/operator/api/v1alpha1/paladinregistry_webhook.go @@ -0,0 +1,55 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + "fmt" + + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +// SetupWebhookWithManager will setup the manager to manage the webhooks +func (r *PaladinRegistry) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(r). + Complete() +} + +// +kubebuilder:webhook:path=/validate-core-paladin-io-v1alpha1-paladinregistry,mutating=false,failurePolicy=fail,sideEffects=None,groups=core.paladin.io,resources=paladinregistries,verbs=create;update;delete,versions=v1alpha1,name=vpaladinregistry.kb.io,admissionReviewVersions=v1 + +var _ webhook.Validator = &PaladinRegistry{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (r *PaladinRegistry) ValidateCreate() (admission.Warnings, error) { + // allow creation + return nil, nil +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (r *PaladinRegistry) ValidateUpdate(old runtime.Object) (admission.Warnings, error) { + // Deny any update + return nil, fmt.Errorf("updates to PaladinRegistry resources are not allowed as they have already been submitted to the blockchain and cannot be modified") +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (r *PaladinRegistry) ValidateDelete() (admission.Warnings, error) { + // Deny any delete + return nil, fmt.Errorf("deletions of PaladinRegistry resources are not allowed as they have already been submitted to the blockchain and cannot be deleted") +} diff --git a/operator/api/v1alpha1/paladinregistry_webhook_test.go b/operator/api/v1alpha1/paladinregistry_webhook_test.go new file mode 100644 index 000000000..afa402986 --- /dev/null +++ b/operator/api/v1alpha1/paladinregistry_webhook_test.go @@ -0,0 +1,96 @@ +/* +Copyright 2024. + +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" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +var _ = Describe("PaladinRegistry Webhook - Block Update & Delete", func() { + + ctx := context.Background() + + key := types.NamespacedName{ + Name: "resource", + Namespace: "default", + } + + registry := &PaladinRegistry{} + + BeforeEach(func() { + // Attempt to fetch the resource. If it doesn't exist, create a valid one. + err := k8sClient.Get(ctx, key, registry) + if err != nil && errors.IsNotFound(err) { + registry = &PaladinRegistry{ + ObjectMeta: metav1.ObjectMeta{ + Name: key.Name, + Namespace: key.Namespace, + }, + Spec: PaladinRegistrySpec{ + Type: RegistryTypeEVM, + EVM: EVMRegistryConfig{ + SmartContractDeployment: "registry", + }, + Plugin: PluginConfig{ + Type: "c-shared", + Library: "/app/registries/libevm.so", + }, + ConfigJSON: "{}", + }, + } + Expect(k8sClient.Create(ctx, registry)).To(Succeed()) + } else { + // If any other error occurred, fail the test immediately + Expect(err).NotTo(HaveOccurred()) + } + }) + + Context("When attempting to update an existing PaladinRegistry", func() { + It("Should block the update and return an error", func() { + fetched := &PaladinRegistry{} + // Fetch the resource to ensure we have the latest version + err := k8sClient.Get(ctx, key, fetched) + Expect(err).NotTo(HaveOccurred()) + + // Attempt to change something in the spec + fetched.Spec.ConfigJSON = "{\"some\":\"update\"}" + + // Update should fail due to the webhook blocking updates + By("Updating PaladinRegistry, expecting a webhook error") + err = k8sClient.Update(ctx, fetched) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("updates to PaladinRegistry resources are not allowed")) + }) + }) + + Context("When attempting to delete an existing PaladinRegistry", func() { + It("Should block the delete and return an error", func() { + // Attempt to delete, expecting the webhook to block + By("Deleting PaladinRegistry, expecting a webhook error") + err := k8sClient.Delete(ctx, registry) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("deletions of PaladinRegistry resources are not allowed")) + }) + }) +}) diff --git a/operator/api/v1alpha1/smartcontractdeployment_webhook.go b/operator/api/v1alpha1/smartcontractdeployment_webhook.go new file mode 100644 index 000000000..062d1feec --- /dev/null +++ b/operator/api/v1alpha1/smartcontractdeployment_webhook.go @@ -0,0 +1,52 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + "fmt" + + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +// SetupWebhookWithManager will setup the manager to manage the webhooks +func (r *SmartContractDeployment) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(r). + Complete() +} + +//+kubebuilder:webhook:path=/validate-core-paladin-io-v1alpha1-smartcontractdeployment,mutating=false,failurePolicy=fail,sideEffects=None,groups=core.paladin.io,resources=smartcontractdeployments,verbs=create;update;delete,versions=v1alpha1,name=vsmartcontractdeployment.kb.io,admissionReviewVersions=v1 + +var _ webhook.Validator = &SmartContractDeployment{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (r *SmartContractDeployment) ValidateCreate() (admission.Warnings, error) { + return nil, nil +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (r *SmartContractDeployment) ValidateUpdate(old runtime.Object) (admission.Warnings, error) { + return nil, fmt.Errorf("updates to SmartContractDeployment resources are not allowed as they have already been submitted to the blockchain and cannot be modified") +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (r *SmartContractDeployment) ValidateDelete() (admission.Warnings, error) { + return nil, fmt.Errorf("deletions of SmartContractDeployment resources are not allowed as they have already been submitted to the blockchain and cannot be deleted") +} diff --git a/operator/api/v1alpha1/smartcontractdeployment_webhook_test.go b/operator/api/v1alpha1/smartcontractdeployment_webhook_test.go new file mode 100644 index 000000000..1a922bdd0 --- /dev/null +++ b/operator/api/v1alpha1/smartcontractdeployment_webhook_test.go @@ -0,0 +1,88 @@ +/* +Copyright 2024. + +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" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +var _ = Describe("SmartContractDeployment Webhook - Block Update & Delete", func() { + + ctx := context.Background() + + key := types.NamespacedName{ + Name: "resource", + Namespace: "default", + } + + scd := &SmartContractDeployment{} + + BeforeEach(func() { + // Attempt to fetch the resource. If it doesn't exist, create a valid one. + err := k8sClient.Get(ctx, key, scd) + if err != nil && errors.IsNotFound(err) { + scd = &SmartContractDeployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: key.Name, + Namespace: key.Namespace, + }, + Spec: SmartContractDeploymentSpec{ + TxType: "public", + }, + } + Expect(k8sClient.Create(ctx, scd)).To(Succeed()) + } else { + // If there's a real error, fail + Expect(err).NotTo(HaveOccurred()) + } + }) + + Context("When attempting to update an existing SmartContractDeployment", func() { + It("Should block the update and return an error", func() { + fetched := &SmartContractDeployment{} + By("Fetching the resource to ensure we have the latest version") + err := k8sClient.Get(ctx, key, fetched) + Expect(err).NotTo(HaveOccurred()) + + // Make some change to the spec + fetched.Spec.TxType = "private" + + By("Attempting to update SmartContractDeployment, expecting the webhook to block") + err = k8sClient.Update(ctx, fetched) + Expect(err).To(HaveOccurred()) + // Check for the expected error message from the webhook + Expect(err.Error()).To(ContainSubstring("updates to SmartContractDeployment resources are not allowed")) + }) + }) + + Context("When attempting to delete an existing SmartContractDeployment", func() { + It("Should block the delete and return an error", func() { + By("Deleting SmartContractDeployment, expecting a webhook error") + err := k8sClient.Delete(ctx, scd) + Expect(err).To(HaveOccurred()) + // Check for the expected error message + Expect(err.Error()).To(ContainSubstring("deletions of SmartContractDeployment resources are not allowed")) + }) + }) +}) diff --git a/operator/api/v1alpha1/transactioninvoke_webhook.go b/operator/api/v1alpha1/transactioninvoke_webhook.go new file mode 100644 index 000000000..b2ab7d402 --- /dev/null +++ b/operator/api/v1alpha1/transactioninvoke_webhook.go @@ -0,0 +1,54 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + "fmt" + + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +// SetupWebhookWithManager will setup the manager to manage the webhooks +func (r *TransactionInvoke) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(r). + Complete() +} + +//+kubebuilder:webhook:path=/validate-core-paladin-io-v1alpha1-transactioninvoke,mutating=false,failurePolicy=fail,sideEffects=None,groups=core.paladin.io,resources=transactioninvokes,verbs=create;update;delete,versions=v1alpha1,name=vtransactioninvoke.kb.io,admissionReviewVersions=v1 + +var _ webhook.Validator = &TransactionInvoke{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (r *TransactionInvoke) ValidateCreate() (admission.Warnings, error) { + return nil, nil +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (r *TransactionInvoke) ValidateUpdate(old runtime.Object) (admission.Warnings, error) { + // Deny any update + return nil, fmt.Errorf("updates to TransactionInvoke resources are not allowed as they have already been submitted to the blockchain and cannot be modified") +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (r *TransactionInvoke) ValidateDelete() (admission.Warnings, error) { + // Deny any delete + return nil, fmt.Errorf("deletions of TransactionInvoke resources are not allowed as they have already been submitted to the blockchain and cannot be deleted") +} diff --git a/operator/api/v1alpha1/transactioninvoke_webhook_test.go b/operator/api/v1alpha1/transactioninvoke_webhook_test.go new file mode 100644 index 000000000..660c3ec61 --- /dev/null +++ b/operator/api/v1alpha1/transactioninvoke_webhook_test.go @@ -0,0 +1,85 @@ +/* +Copyright 2024. + +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" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +var _ = Describe("TransactionInvoke Webhook - Block Update & Delete", func() { + + ctx := context.Background() + + key := types.NamespacedName{ + Name: "resource", + Namespace: "default", + } + + ti := &TransactionInvoke{} + + BeforeEach(func() { + // Attempt to get the resource. If it doesn't exist, create a valid one. + err := k8sClient.Get(ctx, key, ti) + if err != nil && errors.IsNotFound(err) { + resource := &TransactionInvoke{ + ObjectMeta: metav1.ObjectMeta{ + Name: key.Name, + Namespace: key.Namespace, + }, + Spec: TransactionInvokeSpec{ + TxType: "public", + }, + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + } else { + Expect(err).NotTo(HaveOccurred()) + } + }) + + Context("When attempting to update an existing TransactionInvoke", func() { + It("Should block the update and return an error", func() { + fetched := &TransactionInvoke{} + By("Fetching the TransactionInvoke to ensure we have the latest version") + err := k8sClient.Get(ctx, key, fetched) + Expect(err).NotTo(HaveOccurred()) + + // Modify a field in the spec to simulate an update + fetched.Spec.TxType = "private" + + By("Updating TransactionInvoke, expecting a webhook error") + err = k8sClient.Update(ctx, fetched) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("updates to TransactionInvoke resources are not allowed")) + }) + }) + + Context("When attempting to delete an existing TransactionInvoke", func() { + It("Should block the delete and return an error", func() { + By("Deleting TransactionInvoke, expecting a webhook error") + err := k8sClient.Delete(ctx, ti) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("deletions of TransactionInvoke resources are not allowed")) + }) + }) +}) diff --git a/operator/api/v1alpha1/webhook_suite_test.go b/operator/api/v1alpha1/webhook_suite_test.go new file mode 100644 index 000000000..759516c3b --- /dev/null +++ b/operator/api/v1alpha1/webhook_suite_test.go @@ -0,0 +1,154 @@ +/* +Copyright 2024. + +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 = (&SmartContractDeployment{}).SetupWebhookWithManager(mgr) + Expect(err).NotTo(HaveOccurred()) + + err = (&TransactionInvoke{}).SetupWebhookWithManager(mgr) + Expect(err).NotTo(HaveOccurred()) + + err = (&PaladinRegistry{}).SetupWebhookWithManager(mgr) + Expect(err).NotTo(HaveOccurred()) + + err = (&PaladinRegistration{}).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/operator/api/v1alpha1/zz_generated.deepcopy.go b/operator/api/v1alpha1/zz_generated.deepcopy.go index 1eab36cc1..855ecada0 100644 --- a/operator/api/v1alpha1/zz_generated.deepcopy.go +++ b/operator/api/v1alpha1/zz_generated.deepcopy.go @@ -23,7 +23,7 @@ package v1alpha1 import ( "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - runtime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. diff --git a/operator/charts/paladin-operator/templates/operator/certificate.yaml b/operator/charts/paladin-operator/templates/operator/certificate.yaml new file mode 100644 index 000000000..501453a08 --- /dev/null +++ b/operator/charts/paladin-operator/templates/operator/certificate.yaml @@ -0,0 +1,31 @@ +{{- if .Values.webhook.enabled }} +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + labels: + app.kubernetes.io/name: {{ .Values.operator.name }} + name: {{ .Values.operator.name }} + namespace: {{ .Values.operator.namespace }} +spec: + selfSigned: {} +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + labels: + app.kubernetes.io/name: {{ .Values.operator.name }} + app.kubernetes.io/instance: {{ .Values.operator.name }} + app.kubernetes.io/component: certificate + app.kubernetes.io/part-of: paladin + name: {{ .Values.operator.name }} + namespace: {{ .Values.operator.namespace }} +spec: + dnsNames: + - {{ .Values.operator.name }}.{{ .Values.operator.namespace }}.svc + - {{ .Values.operator.name }}.{{ .Values.operator.namespace }}.svc.cluster.local + issuerRef: + kind: Issuer + name: {{ .Values.operator.name }} + secretName: webhook-server-cert + +{{- end }} diff --git a/operator/charts/paladin-operator/templates/operator/deployment.yaml b/operator/charts/paladin-operator/templates/operator/deployment.yaml index 0411536a0..73c764503 100644 --- a/operator/charts/paladin-operator/templates/operator/deployment.yaml +++ b/operator/charts/paladin-operator/templates/operator/deployment.yaml @@ -39,11 +39,18 @@ spec: - containerPort: {{ .Values.operator.service.port }} name: http protocol: TCP + {{- if .Values.webhook.enabled }} + - containerPort: 9443 + name: webhook + protocol: TCP + {{- end }} env: - name: WATCH_NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace + - name: ENABLE_WEBHOOKS + value: "{{ .Values.webhook.enabled }}" {{- range .Values.operator.env }} - name: {{ .name }} value: {{ .value | quote }} @@ -59,8 +66,19 @@ spec: capabilities: drop: ["ALL"] volumeMounts: - {{- toYaml .Values.operator.volumeMounts | nindent 12 }} + {{- if .Values.webhook.enabled }} + - mountPath: /tmp/k8s-webhook-server/serving-certs + name: cert + readOnly: true + {{- end }} + {{- toYaml .Values.operator.volumeMounts | nindent 10 }} volumes: + {{- if .Values.webhook.enabled }} + - name: cert + secret: + defaultMode: 420 + secretName: webhook-server-cert + {{- end }} {{- toYaml .Values.operator.volumes | nindent 8 }} {{- with .Values.operator.nodeSelector }} nodeSelector: @@ -73,4 +91,4 @@ spec: {{- with .Values.operator.tolerations }} tolerations: {{- toYaml . | nindent 8 }} - {{- end }} + {{- end }} \ No newline at end of file diff --git a/operator/charts/paladin-operator/templates/operator/manifests.yaml b/operator/charts/paladin-operator/templates/operator/manifests.yaml new file mode 100644 index 000000000..bef7ef463 --- /dev/null +++ b/operator/charts/paladin-operator/templates/operator/manifests.yaml @@ -0,0 +1,100 @@ +{{- if .Values.webhook.enabled }} +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + name: {{ .Values.operator.name }} + labels: + app.kubernetes.io/name: {{ .Values.operator.name }} + app.kubernetes.io/instance: {{ .Values.operator.name }} + app.kubernetes.io/component: webhook + app.kubernetes.io/created-by: {{ .Values.operator.name }} + app.kubernetes.io/part-of: paladin + annotations: + cert-manager.io/inject-ca-from: {{ .Values.operator.namespace }}/{{ .Values.operator.name }} +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: {{ .Values.operator.name }} + namespace: {{ .Values.operator.namespace }} + path: /validate-core-paladin-io-v1alpha1-paladinregistration + failurePolicy: Fail + name: vpaladinregistration.kb.io + rules: + - apiGroups: + - core.paladin.io + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + - DELETE + resources: + - paladinregistrations + sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: {{ .Values.operator.name }} + namespace: {{ .Values.operator.namespace }} + path: /validate-core-paladin-io-v1alpha1-paladinregistry + failurePolicy: Fail + name: vpaladinregistry.kb.io + rules: + - apiGroups: + - core.paladin.io + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + - DELETE + resources: + - paladinregistries + sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: {{ .Values.operator.name }} + namespace: {{ .Values.operator.namespace }} + path: /validate-core-paladin-io-v1alpha1-smartcontractdeployment + failurePolicy: Fail + name: vsmartcontractdeployment.kb.io + rules: + - apiGroups: + - core.paladin.io + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + - DELETE + resources: + - smartcontractdeployments + sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: {{ .Values.operator.name }} + namespace: {{ .Values.operator.namespace }} + path: /validate-core-paladin-io-v1alpha1-transactioninvoke + failurePolicy: Fail + name: vtransactioninvoke.kb.io + rules: + - apiGroups: + - core.paladin.io + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + - DELETE + resources: + - transactioninvokes + sideEffects: None + +{{- end }} \ No newline at end of file diff --git a/operator/charts/paladin-operator/templates/operator/service.yaml b/operator/charts/paladin-operator/templates/operator/service.yaml index ca2b85a5e..b14df88d4 100644 --- a/operator/charts/paladin-operator/templates/operator/service.yaml +++ b/operator/charts/paladin-operator/templates/operator/service.yaml @@ -11,7 +11,6 @@ metadata: annotations: {{- toYaml . | nindent 4 }} {{- end }} - spec: type: {{ .Values.operator.service.type }} ports: @@ -19,6 +18,12 @@ spec: targetPort: http protocol: TCP name: http + {{- if .Values.webhook.enabled }} + - port: 443 + protocol: TCP + targetPort: 9443 + name: webhook + {{- end }} selector: app.kubernetes.io/name: {{ .Values.operator.name }} - + \ No newline at end of file diff --git a/operator/charts/paladin-operator/values.yaml b/operator/charts/paladin-operator/values.yaml index f0a5992b4..a7cf16cbb 100644 --- a/operator/charts/paladin-operator/values.yaml +++ b/operator/charts/paladin-operator/values.yaml @@ -56,6 +56,7 @@ operator: configMap: name: paladin-operator + volumeMounts: - name: config mountPath: /etc/config/config.json # Where the file will be mounted in the container @@ -77,6 +78,10 @@ operator: configMap: # Referring to the external config.yaml file dataFile: assets/config.json + +webhook: + enabled: true + # Prometheus dependencies prometheus: enabled: false diff --git a/operator/cmd/main.go b/operator/cmd/main.go index 2f8f33f8f..5dbf8618e 100644 --- a/operator/cmd/main.go +++ b/operator/cmd/main.go @@ -184,6 +184,30 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "TransactionInvoke") os.Exit(1) } + if os.Getenv("ENABLE_WEBHOOKS") != "false" { + if err = (&corev1alpha1.PaladinRegistration{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "PaladinRegistration") + os.Exit(1) + } + } + if os.Getenv("ENABLE_WEBHOOKS") != "false" { + if err = (&corev1alpha1.SmartContractDeployment{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "SmartContractDeployment") + os.Exit(1) + } + } + if os.Getenv("ENABLE_WEBHOOKS") != "false" { + if err = (&corev1alpha1.TransactionInvoke{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "TransactionInvoke") + os.Exit(1) + } + } + if os.Getenv("ENABLE_WEBHOOKS") != "false" { + if err = (&corev1alpha1.PaladinRegistry{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "PaladinRegistry") + os.Exit(1) + } + } //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/operator/config/certmanager/certificate.yaml b/operator/config/certmanager/certificate.yaml new file mode 100644 index 000000000..aed4df31b --- /dev/null +++ b/operator/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: operator-go + 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: operator-go + app.kubernetes.io/part-of: operator-go + 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/operator/config/certmanager/kustomization.yaml b/operator/config/certmanager/kustomization.yaml new file mode 100644 index 000000000..bebea5a59 --- /dev/null +++ b/operator/config/certmanager/kustomization.yaml @@ -0,0 +1,5 @@ +resources: +- certificate.yaml + +configurations: +- kustomizeconfig.yaml diff --git a/operator/config/certmanager/kustomizeconfig.yaml b/operator/config/certmanager/kustomizeconfig.yaml new file mode 100644 index 000000000..cf6f89e88 --- /dev/null +++ b/operator/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/operator/config/crd/kustomization.yaml b/operator/config/crd/kustomization.yaml index 439c10de2..a2cdff0ea 100644 --- a/operator/config/crd/kustomization.yaml +++ b/operator/config/crd/kustomization.yaml @@ -15,6 +15,10 @@ resources: 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_paladinregistrations.yaml +- path: patches/webhook_in_smartcontractdeployments.yaml +- path: patches/webhook_in_transactioninvokes.yaml +- path: patches/webhook_in_paladinregistries.yaml #+kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. @@ -32,5 +36,5 @@ patches: # [WEBHOOK] To enable webhook, uncomment the following section # the following config is for teaching kustomize how to do kustomization for CRDs. -#configurations: -#- kustomizeconfig.yaml +configurations: +- kustomizeconfig.yaml diff --git a/operator/config/crd/patches/cainjection_in_paladinregistrations.yaml b/operator/config/crd/patches/cainjection_in_paladinregistrations.yaml new file mode 100644 index 000000000..33e2d9271 --- /dev/null +++ b/operator/config/crd/patches/cainjection_in_paladinregistrations.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME + name: paladinregistrations.core.paladin.io diff --git a/operator/config/crd/patches/cainjection_in_paladinregistries.yaml b/operator/config/crd/patches/cainjection_in_paladinregistries.yaml new file mode 100644 index 000000000..0c785100f --- /dev/null +++ b/operator/config/crd/patches/cainjection_in_paladinregistries.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME + name: paladinregistries.core.paladin.io diff --git a/operator/config/crd/patches/cainjection_in_smartcontractdeployments.yaml b/operator/config/crd/patches/cainjection_in_smartcontractdeployments.yaml new file mode 100644 index 000000000..eee03082f --- /dev/null +++ b/operator/config/crd/patches/cainjection_in_smartcontractdeployments.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME + name: smartcontractdeployments.core.paladin.io diff --git a/operator/config/crd/patches/cainjection_in_transactioninvokes.yaml b/operator/config/crd/patches/cainjection_in_transactioninvokes.yaml new file mode 100644 index 000000000..4dcfacd0c --- /dev/null +++ b/operator/config/crd/patches/cainjection_in_transactioninvokes.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME + name: transactioninvokes.core.paladin.io diff --git a/operator/config/crd/patches/webhook_in_paladinregistrations.yaml b/operator/config/crd/patches/webhook_in_paladinregistrations.yaml new file mode 100644 index 000000000..c548823aa --- /dev/null +++ b/operator/config/crd/patches/webhook_in_paladinregistrations.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: paladinregistrations.core.paladin.io +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/operator/config/crd/patches/webhook_in_paladinregistries.yaml b/operator/config/crd/patches/webhook_in_paladinregistries.yaml new file mode 100644 index 000000000..aeac1f2c4 --- /dev/null +++ b/operator/config/crd/patches/webhook_in_paladinregistries.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: paladinregistries.core.paladin.io +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/operator/config/crd/patches/webhook_in_smartcontractdeployments.yaml b/operator/config/crd/patches/webhook_in_smartcontractdeployments.yaml new file mode 100644 index 000000000..8421a5b9e --- /dev/null +++ b/operator/config/crd/patches/webhook_in_smartcontractdeployments.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: smartcontractdeployments.core.paladin.io +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/operator/config/crd/patches/webhook_in_transactioninvokes.yaml b/operator/config/crd/patches/webhook_in_transactioninvokes.yaml new file mode 100644 index 000000000..a185ef297 --- /dev/null +++ b/operator/config/crd/patches/webhook_in_transactioninvokes.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: transactioninvokes.core.paladin.io +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/operator/config/default/kustomization.yaml b/operator/config/default/kustomization.yaml index 39d899019..856679c28 100644 --- a/operator/config/default/kustomization.yaml +++ b/operator/config/default/kustomization.yaml @@ -20,7 +20,7 @@ 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 # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. @@ -34,7 +34,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/operator/config/default/manager_webhook_patch.yaml b/operator/config/default/manager_webhook_patch.yaml new file mode 100644 index 000000000..738de350b --- /dev/null +++ b/operator/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/operator/config/default/webhookcainjection_patch.yaml b/operator/config/default/webhookcainjection_patch.yaml new file mode 100644 index 000000000..da12bdea3 --- /dev/null +++ b/operator/config/default/webhookcainjection_patch.yaml @@ -0,0 +1,25 @@ +# 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: operator-go + 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: operator-go + app.kubernetes.io/part-of: operator-go + app.kubernetes.io/managed-by: kustomize + name: validating-webhook-configuration + annotations: + cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME diff --git a/operator/config/webhook/kustomization.yaml b/operator/config/webhook/kustomization.yaml new file mode 100644 index 000000000..9cf26134e --- /dev/null +++ b/operator/config/webhook/kustomization.yaml @@ -0,0 +1,6 @@ +resources: +- manifests.yaml +- service.yaml + +configurations: +- kustomizeconfig.yaml diff --git a/operator/config/webhook/kustomizeconfig.yaml b/operator/config/webhook/kustomizeconfig.yaml new file mode 100644 index 000000000..206316e54 --- /dev/null +++ b/operator/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/operator/config/webhook/manifests.yaml b/operator/config/webhook/manifests.yaml new file mode 100644 index 000000000..60d44158d --- /dev/null +++ b/operator/config/webhook/manifests.yaml @@ -0,0 +1,90 @@ +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + name: validating-webhook-configuration +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-core-paladin-io-v1alpha1-paladinregistration + failurePolicy: Fail + name: vpaladinregistration.kb.io + rules: + - apiGroups: + - core.paladin.io + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + - DELETE + resources: + - paladinregistrations + sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-core-paladin-io-v1alpha1-paladinregistry + failurePolicy: Fail + name: vpaladinregistry.kb.io + rules: + - apiGroups: + - core.paladin.io + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + - DELETE + resources: + - paladinregistries + sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-core-paladin-io-v1alpha1-smartcontractdeployment + failurePolicy: Fail + name: vsmartcontractdeployment.kb.io + rules: + - apiGroups: + - core.paladin.io + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + - DELETE + resources: + - smartcontractdeployments + sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-core-paladin-io-v1alpha1-transactioninvoke + failurePolicy: Fail + name: vtransactioninvoke.kb.io + rules: + - apiGroups: + - core.paladin.io + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + - DELETE + resources: + - transactioninvokes + sideEffects: None diff --git a/operator/config/webhook/service.yaml b/operator/config/webhook/service.yaml new file mode 100644 index 000000000..631096f70 --- /dev/null +++ b/operator/config/webhook/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/name: operator-go + 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/operator/internal/controller/paladinregistration_controller.go b/operator/internal/controller/paladinregistration_controller.go index 73e666095..0b6ab9109 100644 --- a/operator/internal/controller/paladinregistration_controller.go +++ b/operator/internal/controller/paladinregistration_controller.go @@ -77,8 +77,6 @@ var PaladinRegistrationCRMap = CRMap[corev1alpha1.PaladinRegistration, *corev1al func (r *PaladinRegistrationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { log := log.FromContext(ctx) - // TODO: Add an admission webhook to make the bytecode and ABI immutable - // Fetch the PaladinRegistration instance var reg corev1alpha1.PaladinRegistration if err := r.Get(ctx, req.NamespacedName, ®); err != nil { diff --git a/operator/internal/controller/smartcontractdeployment_controller.go b/operator/internal/controller/smartcontractdeployment_controller.go index 0f6bd00d0..392700f36 100644 --- a/operator/internal/controller/smartcontractdeployment_controller.go +++ b/operator/internal/controller/smartcontractdeployment_controller.go @@ -57,8 +57,6 @@ var SmartContractDeploymentCRMap = CRMap[corev1alpha1.SmartContractDeployment, * func (r *SmartContractDeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { log := log.FromContext(ctx) - // TODO: Add an admission webhook to make the bytecode and ABI immutable - // Fetch the SmartContractDeployment instance var scd corev1alpha1.SmartContractDeployment if err := r.Get(ctx, req.NamespacedName, &scd); err != nil { diff --git a/operator/internal/controller/transactioninvoke_controller.go b/operator/internal/controller/transactioninvoke_controller.go index ec4e0b12d..1355fa616 100644 --- a/operator/internal/controller/transactioninvoke_controller.go +++ b/operator/internal/controller/transactioninvoke_controller.go @@ -63,8 +63,6 @@ var TransactionInvokeCRMap = CRMap[corev1alpha1.TransactionInvoke, *corev1alpha1 func (r *TransactionInvokeReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { log := log.FromContext(ctx) - // TODO: Add an admission webhook to make the contract deps, bytecode and ABIs immutable - // Fetch the TransactionInvoke instance var txi corev1alpha1.TransactionInvoke if err := r.Get(ctx, req.NamespacedName, &txi); err != nil {