diff --git a/charts/kargo/templates/webhooks/webhooks.yaml b/charts/kargo/templates/webhooks/webhooks.yaml index 47200022a4..2dec7d37f8 100644 --- a/charts/kargo/templates/webhooks/webhooks.yaml +++ b/charts/kargo/templates/webhooks/webhooks.yaml @@ -39,6 +39,21 @@ webhooks: resources: ["promotions"] operations: ["CREATE", "UPDATE"] failurePolicy: Fail +- name: promotiontask.kargo.akuity.io + admissionReviewVersions: [ "v1" ] + sideEffects: None + clientConfig: + service: + namespace: {{ .Release.Namespace }} + name: kargo-webhooks-server + path: /validate-kargo-akuity-io-v1alpha1-promotiontask + rules: + - scope: Namespaced + apiGroups: ["kargo.akuity.io"] + apiVersions: ["v1alpha1"] + resources: ["promotiontasks"] + operations: ["CREATE"] + failurePolicy: Fail - name: stage.kargo.akuity.io admissionReviewVersions: ["v1"] sideEffects: None diff --git a/cmd/controlplane/webhooks.go b/cmd/controlplane/webhooks.go index 2d50bffe9c..0d67806511 100644 --- a/cmd/controlplane/webhooks.go +++ b/cmd/controlplane/webhooks.go @@ -24,6 +24,7 @@ import ( "github.com/akuity/kargo/internal/webhook/freight" "github.com/akuity/kargo/internal/webhook/project" "github.com/akuity/kargo/internal/webhook/promotion" + "github.com/akuity/kargo/internal/webhook/promotiontask" "github.com/akuity/kargo/internal/webhook/stage" "github.com/akuity/kargo/internal/webhook/warehouse" ) @@ -135,6 +136,9 @@ func (o *webhooksServerOptions) run(ctx context.Context) error { if err = promotion.SetupWebhookWithManager(ctx, webhookCfg, mgr); err != nil { return fmt.Errorf("setup Promotion webhook: %w", err) } + if err = promotiontask.SetupWebhookWithManager(mgr); err != nil { + return fmt.Errorf("setup PromotionTask webhook: %w", err) + } if err = stage.SetupWebhookWithManager(webhookCfg, mgr); err != nil { return fmt.Errorf("setup Stage webhook: %w", err) } diff --git a/internal/webhook/promotiontask/webhook.go b/internal/webhook/promotiontask/webhook.go new file mode 100644 index 0000000000..3b91a22e57 --- /dev/null +++ b/internal/webhook/promotiontask/webhook.go @@ -0,0 +1,51 @@ +package promotiontask + +import ( + "context" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + kargoapi "github.com/akuity/kargo/api/v1alpha1" + libWebhook "github.com/akuity/kargo/internal/webhook" +) + +var promotionTaskGroupKind = schema.GroupKind{ + Group: kargoapi.GroupVersion.Group, + Kind: "PromotionTask", +} + +type webhook struct { + client client.Client +} + +func SetupWebhookWithManager( + mgr ctrl.Manager, +) error { + w := &webhook{ + client: mgr.GetClient(), + } + return ctrl.NewWebhookManagedBy(mgr). + For(&kargoapi.PromotionTask{}). + WithValidator(w). + Complete() +} + +func (w *webhook) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + template := obj.(*kargoapi.PromotionTask) // nolint: forcetypeassert + if err := libWebhook.ValidateProject(ctx, w.client, promotionTaskGroupKind, template); err != nil { + return nil, err + } + return nil, nil +} + +func (w *webhook) ValidateUpdate(context.Context, runtime.Object, runtime.Object) (admission.Warnings, error) { + return nil, nil +} + +func (w *webhook) ValidateDelete(context.Context, runtime.Object) (admission.Warnings, error) { + return nil, nil +} diff --git a/internal/webhook/promotiontask/webhook_test.go b/internal/webhook/promotiontask/webhook_test.go new file mode 100644 index 0000000000..4c3cf10465 --- /dev/null +++ b/internal/webhook/promotiontask/webhook_test.go @@ -0,0 +1,94 @@ +package promotiontask + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + kargoapi "github.com/akuity/kargo/api/v1alpha1" +) + +func Test_webhook_ValidateCreate(t *testing.T) { + scheme := runtime.NewScheme() + require.NoError(t, corev1.AddToScheme(scheme)) + require.NoError(t, kargoapi.AddToScheme(scheme)) + + tests := []struct { + name string + objects []client.Object + template *kargoapi.PromotionTask + assertions func(*testing.T, admission.Warnings, error) + }{ + { + name: "project does not exist", + objects: []client.Object{ + &corev1.Namespace{ + ObjectMeta: v1.ObjectMeta{ + Name: "fake-project", + }, + }, + }, + template: &kargoapi.PromotionTask{ + ObjectMeta: v1.ObjectMeta{ + Name: "fake-template", + Namespace: "fake-project", + }, + }, + assertions: func(t *testing.T, warnings admission.Warnings, err error) { + assert.Empty(t, warnings) + assert.ErrorContains(t, err, "namespace \"fake-project\" is not a project") + }, + }, + { + name: "project exists", + objects: []client.Object{ + &corev1.Namespace{ + ObjectMeta: v1.ObjectMeta{ + Name: "fake-project", + Labels: map[string]string{ + kargoapi.ProjectLabelKey: kargoapi.LabelTrueValue, + }, + }, + }, + &kargoapi.Project{ + ObjectMeta: v1.ObjectMeta{ + Name: "fake-project", + }, + }, + }, + template: &kargoapi.PromotionTask{ + ObjectMeta: v1.ObjectMeta{ + Name: "fake-template", + Namespace: "fake-project", + }, + }, + assertions: func(t *testing.T, warnings admission.Warnings, err error) { + assert.Empty(t, warnings) + assert.NoError(t, err) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := fake.NewClientBuilder(). + WithObjects(tt.objects...). + WithScheme(scheme). + Build() + + w := &webhook{ + client: c, + } + + got, err := w.ValidateCreate(context.Background(), tt.template) + tt.assertions(t, got, err) + }) + } +}