diff --git a/api/authorization/user_client_factory.go b/api/authorization/user_client_factory.go index 576079be5..c4a33be6c 100644 --- a/api/authorization/user_client_factory.go +++ b/api/authorization/user_client_factory.go @@ -9,7 +9,7 @@ import ( k8sclient "k8s.io/client-go/kubernetes" apierrors "code.cloudfoundry.org/korifi/api/errors" - "code.cloudfoundry.org/korifi/controllers/webhooks" + "code.cloudfoundry.org/korifi/controllers/webhooks/validation" "code.cloudfoundry.org/korifi/tools/k8s" k8serrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" @@ -83,7 +83,7 @@ func isForbidden(err error) bool { return false } - if _, isValidationErr := webhooks.WebhookErrorToValidationError(err); isValidationErr { + if _, isValidationErr := validation.WebhookErrorToValidationError(err); isValidationErr { return false } diff --git a/api/errors/errors.go b/api/errors/errors.go index 1ae735000..4fc009224 100644 --- a/api/errors/errors.go +++ b/api/errors/errors.go @@ -7,8 +7,7 @@ import ( "reflect" "strings" - "code.cloudfoundry.org/korifi/controllers/webhooks" - + "code.cloudfoundry.org/korifi/controllers/webhooks/validation" "github.com/go-logr/logr" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -372,7 +371,7 @@ func NewRollingDeployNotSupportedError(runnerName string) RollingDeployNotSuppor } func FromK8sError(err error, resourceType string) error { - if webhookValidationError, ok := webhooks.WebhookErrorToValidationError(err); ok { + if webhookValidationError, ok := validation.WebhookErrorToValidationError(err); ok { return NewUnprocessableEntityError(err, webhookValidationError.GetMessage()) } diff --git a/api/repositories/app_repository.go b/api/repositories/app_repository.go index 21784e188..2b852899f 100644 --- a/api/repositories/app_repository.go +++ b/api/repositories/app_repository.go @@ -12,7 +12,7 @@ import ( apierrors "code.cloudfoundry.org/korifi/api/errors" korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" "code.cloudfoundry.org/korifi/controllers/controllers/workloads/env" - "code.cloudfoundry.org/korifi/controllers/webhooks" + "code.cloudfoundry.org/korifi/controllers/webhooks/validation" "code.cloudfoundry.org/korifi/tools/k8s" "github.com/google/uuid" @@ -247,8 +247,8 @@ func (f *AppRepo) CreateApp(ctx context.Context, authInfo authorization.Info, ap cfApp := appCreateMessage.toCFApp() err = userClient.Create(ctx, &cfApp) if err != nil { - if validationError, ok := webhooks.WebhookErrorToValidationError(err); ok { - if validationError.Type == webhooks.DuplicateNameErrorType { + if validationError, ok := validation.WebhookErrorToValidationError(err); ok { + if validationError.Type == validation.DuplicateNameErrorType { return AppRecord{}, apierrors.NewUniquenessError(err, validationError.GetMessage()) } } diff --git a/api/repositories/package_repository.go b/api/repositories/package_repository.go index 9a35bcb79..66f7580fa 100644 --- a/api/repositories/package_repository.go +++ b/api/repositories/package_repository.go @@ -8,7 +8,7 @@ import ( "code.cloudfoundry.org/korifi/api/authorization" apierrors "code.cloudfoundry.org/korifi/api/errors" korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" - "code.cloudfoundry.org/korifi/controllers/controllers/workloads" + "code.cloudfoundry.org/korifi/controllers/controllers/workloads/packages" "code.cloudfoundry.org/korifi/tools/dockercfg" "code.cloudfoundry.org/korifi/tools/k8s" @@ -187,7 +187,7 @@ func (r *PackageRepo) CreatePackage(ctx context.Context, authInfo authorization. } } - cfPackage, err = r.awaiter.AwaitCondition(ctx, userClient, cfPackage, workloads.InitializedConditionType) + cfPackage, err = r.awaiter.AwaitCondition(ctx, userClient, cfPackage, packages.InitializedConditionType) if err != nil { return PackageRecord{}, fmt.Errorf("failed waiting for Initialized condition: %w", err) } diff --git a/api/repositories/service_binding_repository.go b/api/repositories/service_binding_repository.go index 84a7682fe..f2a071395 100644 --- a/api/repositories/service_binding_repository.go +++ b/api/repositories/service_binding_repository.go @@ -11,8 +11,8 @@ import ( "code.cloudfoundry.org/korifi/api/authorization" apierrors "code.cloudfoundry.org/korifi/api/errors" korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" - "code.cloudfoundry.org/korifi/controllers/webhooks" - "code.cloudfoundry.org/korifi/controllers/webhooks/services" + "code.cloudfoundry.org/korifi/controllers/webhooks/services/bindings" + "code.cloudfoundry.org/korifi/controllers/webhooks/validation" "code.cloudfoundry.org/korifi/tools/k8s" "github.com/google/uuid" @@ -135,8 +135,8 @@ func (r *ServiceBindingRepo) CreateServiceBinding(ctx context.Context, authInfo err = userClient.Create(ctx, cfServiceBinding) if err != nil { - if validationError, ok := webhooks.WebhookErrorToValidationError(err); ok { - if validationError.Type == services.ServiceBindingErrorType { + if validationError, ok := validation.WebhookErrorToValidationError(err); ok { + if validationError.Type == bindings.ServiceBindingErrorType { return ServiceBindingRecord{}, apierrors.NewUniquenessError(err, validationError.GetMessage()) } } diff --git a/api/repositories/task_repository.go b/api/repositories/task_repository.go index e0fa0454b..229e983e8 100644 --- a/api/repositories/task_repository.go +++ b/api/repositories/task_repository.go @@ -8,7 +8,7 @@ import ( "code.cloudfoundry.org/korifi/api/authorization" apierrors "code.cloudfoundry.org/korifi/api/errors" korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" - "code.cloudfoundry.org/korifi/controllers/controllers/workloads" + "code.cloudfoundry.org/korifi/controllers/controllers/workloads/tasks" "code.cloudfoundry.org/korifi/tools/k8s" "github.com/google/uuid" v1 "k8s.io/api/core/v1" @@ -273,7 +273,7 @@ func taskToRecord(task *korifiv1alpha1.CFTask) TaskRecord { if failedCond != nil && failedCond.Status == metav1.ConditionTrue { taskRecord.FailureReason = failedCond.Message - if failedCond.Reason == workloads.TaskCanceledReason { + if failedCond.Reason == tasks.TaskCanceledReason { taskRecord.FailureReason = "task was cancelled" } } diff --git a/controllers/api/v1alpha1/cfapp_webhook_test.go b/controllers/api/v1alpha1/cfapp_webhook_test.go index 33164c915..6eec97577 100644 --- a/controllers/api/v1alpha1/cfapp_webhook_test.go +++ b/controllers/api/v1alpha1/cfapp_webhook_test.go @@ -2,8 +2,8 @@ package v1alpha1_test import ( korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" - . "code.cloudfoundry.org/korifi/controllers/controllers/workloads/testutils" + "github.com/google/uuid" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -20,7 +20,7 @@ var _ = Describe("CFAppMutatingWebhook", func() { BeforeEach(func() { cfApp = &korifiv1alpha1.CFApp{ ObjectMeta: metav1.ObjectMeta{ - Name: GenerateGUID(), + Name: uuid.NewString(), Namespace: namespace, Labels: map[string]string{ "anotherLabel": "app-label", @@ -30,7 +30,7 @@ var _ = Describe("CFAppMutatingWebhook", func() { }, }, Spec: korifiv1alpha1.CFAppSpec{ - DisplayName: GenerateGUID(), + DisplayName: uuid.NewString(), DesiredState: "STARTED", Lifecycle: korifiv1alpha1.Lifecycle{ Type: "buildpack", diff --git a/controllers/api/v1alpha1/cfbuild_webhook_test.go b/controllers/api/v1alpha1/cfbuild_webhook_test.go index aa932d1fb..f327330b4 100644 --- a/controllers/api/v1alpha1/cfbuild_webhook_test.go +++ b/controllers/api/v1alpha1/cfbuild_webhook_test.go @@ -2,8 +2,8 @@ package v1alpha1_test import ( korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" - . "code.cloudfoundry.org/korifi/controllers/controllers/workloads/testutils" + "github.com/google/uuid" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" v1 "k8s.io/api/core/v1" @@ -24,9 +24,9 @@ var _ = Describe("CFBuildMutatingWebhook", func() { ) BeforeEach(func() { - cfAppGUID = GenerateGUID() - cfPackageGUID = GenerateGUID() - cfBuildGUID = GenerateGUID() + cfAppGUID = uuid.NewString() + cfPackageGUID = uuid.NewString() + cfBuildGUID = uuid.NewString() cfBuild = &korifiv1alpha1.CFBuild{ ObjectMeta: metav1.ObjectMeta{ diff --git a/controllers/api/v1alpha1/cfpackage_webhook_test.go b/controllers/api/v1alpha1/cfpackage_webhook_test.go index a1a7edf84..34d00c364 100644 --- a/controllers/api/v1alpha1/cfpackage_webhook_test.go +++ b/controllers/api/v1alpha1/cfpackage_webhook_test.go @@ -2,8 +2,8 @@ package v1alpha1_test import ( korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" - . "code.cloudfoundry.org/korifi/controllers/controllers/workloads/testutils" + "github.com/google/uuid" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" v1 "k8s.io/api/core/v1" @@ -17,11 +17,11 @@ var _ = Describe("CFPackageMutatingWebhook", func() { ) BeforeEach(func() { - cfAppGUID = GenerateGUID() + cfAppGUID = uuid.NewString() cfPackage = &korifiv1alpha1.CFPackage{ ObjectMeta: metav1.ObjectMeta{ - Name: GenerateGUID(), + Name: uuid.NewString(), Namespace: namespace, Labels: map[string]string{"foo": "bar"}, }, diff --git a/controllers/api/v1alpha1/cfprocess_webhook_test.go b/controllers/api/v1alpha1/cfprocess_webhook_test.go index 438b94e25..5d07c5f5e 100644 --- a/controllers/api/v1alpha1/cfprocess_webhook_test.go +++ b/controllers/api/v1alpha1/cfprocess_webhook_test.go @@ -4,9 +4,9 @@ import ( "context" korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" - . "code.cloudfoundry.org/korifi/controllers/controllers/workloads/testutils" "code.cloudfoundry.org/korifi/tools" + "github.com/google/uuid" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/onsi/gomega/gstruct" @@ -27,8 +27,8 @@ var _ = Describe("CFProcessMutatingWebhook", func() { ) BeforeEach(func() { - cfAppGUID = GenerateGUID() - cfProcessGUID = GenerateGUID() + cfAppGUID = uuid.NewString() + cfProcessGUID = uuid.NewString() cfProcess = &korifiv1alpha1.CFProcess{ ObjectMeta: metav1.ObjectMeta{ Name: cfProcessGUID, diff --git a/controllers/api/v1alpha1/cfroute_webhook_test.go b/controllers/api/v1alpha1/cfroute_webhook_test.go index 7c073b4ba..c122debe9 100644 --- a/controllers/api/v1alpha1/cfroute_webhook_test.go +++ b/controllers/api/v1alpha1/cfroute_webhook_test.go @@ -2,7 +2,6 @@ package v1alpha1_test import ( korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" - . "code.cloudfoundry.org/korifi/controllers/controllers/workloads/testutils" "github.com/google/uuid" . "github.com/onsi/ginkgo/v2" @@ -26,7 +25,7 @@ var _ = Describe("CFRouteMutatingWebhook Integration Tests", func() { BeforeEach(func() { cfDomain = &korifiv1alpha1.CFDomain{ ObjectMeta: metav1.ObjectMeta{ - Name: GenerateGUID(), + Name: uuid.NewString(), Namespace: namespace, }, Spec: korifiv1alpha1.CFDomainSpec{ @@ -37,7 +36,7 @@ var _ = Describe("CFRouteMutatingWebhook Integration Tests", func() { cfRoute = &korifiv1alpha1.CFRoute{ ObjectMeta: metav1.ObjectMeta{ - Name: GenerateGUID(), + Name: uuid.NewString(), Namespace: namespace, Labels: map[string]string{"foo": "bar"}, }, diff --git a/controllers/api/v1alpha1/webhook_suite_test.go b/controllers/api/v1alpha1/webhook_suite_test.go index 575a9dc1a..99d7f840a 100644 --- a/controllers/api/v1alpha1/webhook_suite_test.go +++ b/controllers/api/v1alpha1/webhook_suite_test.go @@ -25,15 +25,19 @@ import ( korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" "code.cloudfoundry.org/korifi/controllers/controllers/shared" - "code.cloudfoundry.org/korifi/controllers/controllers/workloads/testutils" "code.cloudfoundry.org/korifi/controllers/coordination" - "code.cloudfoundry.org/korifi/controllers/webhooks" "code.cloudfoundry.org/korifi/controllers/webhooks/finalizer" - "code.cloudfoundry.org/korifi/controllers/webhooks/networking" + "code.cloudfoundry.org/korifi/controllers/webhooks/networking/domains" + "code.cloudfoundry.org/korifi/controllers/webhooks/networking/routes" + "code.cloudfoundry.org/korifi/controllers/webhooks/validation" "code.cloudfoundry.org/korifi/controllers/webhooks/version" - "code.cloudfoundry.org/korifi/controllers/webhooks/workloads" + "code.cloudfoundry.org/korifi/controllers/webhooks/workloads/apps" + "code.cloudfoundry.org/korifi/controllers/webhooks/workloads/orgs" + packageswebhook "code.cloudfoundry.org/korifi/controllers/webhooks/workloads/packages" + "code.cloudfoundry.org/korifi/controllers/webhooks/workloads/spaces" "code.cloudfoundry.org/korifi/tests/helpers" + "github.com/google/uuid" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" admissionv1beta1 "k8s.io/api/admission/v1beta1" @@ -83,7 +87,7 @@ var _ = BeforeSuite(func() { }, } - namespace = testutils.PrefixedGUID("webhooks-test-ns") + namespace = uuid.NewString() _, err := testEnv.Start() Expect(err).NotTo(HaveOccurred()) @@ -100,18 +104,18 @@ var _ = BeforeSuite(func() { uncachedClient := helpers.NewUncachedClient(k8sManager.GetConfig()) Expect((&korifiv1alpha1.CFApp{}).SetupWebhookWithManager(k8sManager)).To(Succeed()) - Expect(workloads.NewCFAppValidator( - webhooks.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, workloads.AppEntityType)), + Expect(apps.NewValidator( + validation.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, apps.AppEntityType)), ).SetupWebhookWithManager(k8sManager)).To(Succeed()) Expect((&korifiv1alpha1.CFRoute{}).SetupWebhookWithManager(k8sManager)).To(Succeed()) - Expect(networking.NewCFRouteValidator( - webhooks.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, networking.RouteEntityType)), + Expect(routes.NewValidator( + validation.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, routes.RouteEntityType)), namespace, uncachedClient, ).SetupWebhookWithManager(k8sManager)).To(Succeed()) - Expect(networking.NewCFDomainValidator(uncachedClient).SetupWebhookWithManager(k8sManager)).To(Succeed()) + Expect(domains.NewValidator(uncachedClient).SetupWebhookWithManager(k8sManager)).To(Succeed()) Expect((&korifiv1alpha1.CFPackage{}).SetupWebhookWithManager(k8sManager)).To(Succeed()) @@ -120,16 +124,16 @@ var _ = BeforeSuite(func() { Expect((&korifiv1alpha1.CFBuild{}).SetupWebhookWithManager(k8sManager)).To(Succeed()) - orgNameDuplicateValidator := webhooks.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, workloads.CFOrgEntityType)) - orgPlacementValidator := webhooks.NewPlacementValidator(uncachedClient, namespace) - Expect(workloads.NewCFOrgValidator(orgNameDuplicateValidator, orgPlacementValidator).SetupWebhookWithManager(k8sManager)).To(Succeed()) + orgNameDuplicateValidator := validation.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, orgs.CFOrgEntityType)) + orgPlacementValidator := validation.NewPlacementValidator(uncachedClient, namespace) + Expect(orgs.NewValidator(orgNameDuplicateValidator, orgPlacementValidator).SetupWebhookWithManager(k8sManager)).To(Succeed()) - spaceNameDuplicateValidator := webhooks.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, workloads.CFSpaceEntityType)) - spacePlacementValidator := webhooks.NewPlacementValidator(uncachedClient, namespace) - Expect(workloads.NewCFSpaceValidator(spaceNameDuplicateValidator, spacePlacementValidator).SetupWebhookWithManager(k8sManager)).To(Succeed()) + spaceNameDuplicateValidator := validation.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, spaces.CFSpaceEntityType)) + spacePlacementValidator := validation.NewPlacementValidator(uncachedClient, namespace) + Expect(spaces.NewValidator(spaceNameDuplicateValidator, spacePlacementValidator).SetupWebhookWithManager(k8sManager)).To(Succeed()) version.NewVersionWebhook("some-version").SetupWebhookWithManager(k8sManager) finalizer.NewControllersFinalizerWebhook().SetupWebhookWithManager(k8sManager) - Expect(workloads.NewCFPackageValidator().SetupWebhookWithManager(k8sManager)).To(Succeed()) + Expect(packageswebhook.NewValidator().SetupWebhookWithManager(k8sManager)).To(Succeed()) Expect(adminClient.Create(ctx, &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ diff --git a/controllers/cleanup/build_cleaner_test.go b/controllers/cleanup/build_cleaner_test.go index 36f282a3d..e7d3f5a09 100644 --- a/controllers/cleanup/build_cleaner_test.go +++ b/controllers/cleanup/build_cleaner_test.go @@ -5,8 +5,8 @@ import ( korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" "code.cloudfoundry.org/korifi/controllers/cleanup" - . "code.cloudfoundry.org/korifi/controllers/controllers/workloads/testutils" "code.cloudfoundry.org/korifi/statefulset-runner/controllers" + "github.com/google/uuid" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" @@ -28,14 +28,14 @@ var _ = Describe("BuildCleaner", func() { BeforeEach(func() { cleaner = cleanup.NewBuildCleaner(controllersClient, 1) - namespace = GenerateGUID() + namespace = uuid.NewString() Expect(k8sClient.Create(ctx, &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: namespace, }, })).To(Succeed()) - appGUID = GenerateGUID() + appGUID = uuid.NewString() // sleeps are needed as creation timestamps can't be manipulated // directly, and they have a 1 second granularity diff --git a/controllers/cleanup/package_cleaner_test.go b/controllers/cleanup/package_cleaner_test.go index a898ad479..8a349f167 100644 --- a/controllers/cleanup/package_cleaner_test.go +++ b/controllers/cleanup/package_cleaner_test.go @@ -5,9 +5,9 @@ import ( korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" "code.cloudfoundry.org/korifi/controllers/cleanup" - . "code.cloudfoundry.org/korifi/controllers/controllers/workloads/testutils" "code.cloudfoundry.org/korifi/statefulset-runner/controllers" + "github.com/google/uuid" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" @@ -29,15 +29,15 @@ var _ = Describe("PackageCleaner", func() { BeforeEach(func() { cleaner = cleanup.NewPackageCleaner(controllersClient, 1) - namespace = GenerateGUID() + namespace = uuid.NewString() Expect(k8sClient.Create(ctx, &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: namespace, }, })).To(Succeed()) - appGUID = GenerateGUID() - buildGUID := GenerateGUID() + appGUID = uuid.NewString() + buildGUID := uuid.NewString() cfApp = &korifiv1alpha1.CFApp{ ObjectMeta: metav1.ObjectMeta{ diff --git a/controllers/controllers/workloads/apps/controller_test.go b/controllers/controllers/workloads/apps/controller_test.go index d6b4ed5e8..a4cf3170a 100644 --- a/controllers/controllers/workloads/apps/controller_test.go +++ b/controllers/controllers/workloads/apps/controller_test.go @@ -2,7 +2,6 @@ package apps_test import ( korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" - . "code.cloudfoundry.org/korifi/controllers/controllers/workloads/testutils" . "code.cloudfoundry.org/korifi/tests/matchers" "code.cloudfoundry.org/korifi/tools" "code.cloudfoundry.org/korifi/tools/k8s" @@ -461,7 +460,7 @@ var _ = Describe("CFAppReconciler Integration Tests", func() { cfServiceBinding := korifiv1alpha1.CFServiceBinding{ ObjectMeta: metav1.ObjectMeta{ - Name: PrefixedGUID("service-binding"), + Name: uuid.NewString(), Namespace: testNamespace, }, Spec: korifiv1alpha1.CFServiceBindingSpec{ diff --git a/controllers/controllers/workloads/cf_buildpack_build_controller.go b/controllers/controllers/workloads/build/buildpack/controller.go similarity index 98% rename from controllers/controllers/workloads/cf_buildpack_build_controller.go rename to controllers/controllers/workloads/build/buildpack/controller.go index b49dbb1dd..ae371f53e 100644 --- a/controllers/controllers/workloads/cf_buildpack_build_controller.go +++ b/controllers/controllers/workloads/build/buildpack/controller.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package workloads +package buildpack import ( "context" @@ -46,7 +46,7 @@ type BuildpackEnvBuilder interface { Build(context.Context, *korifiv1alpha1.CFApp) ([]corev1.EnvVar, error) } -func NewCFBuildpackBuildReconciler( +func NewReconciler( k8sClient client.Client, buildCleaner build.BuildCleaner, scheme *runtime.Scheme, @@ -57,7 +57,7 @@ func NewCFBuildpackBuildReconciler( return k8s.NewPatchingReconciler[korifiv1alpha1.CFBuild, *korifiv1alpha1.CFBuild]( log, k8sClient, - build.NewCFBuildReconciler( + build.NewReconciler( log, k8sClient, scheme, @@ -276,7 +276,7 @@ func (r *buildpackBuildReconciler) prepareBuildServices(ctx context.Context, nam for _, serviceBinding := range serviceBindingsList.Items { if serviceBinding.Status.Binding.Name == "" { log.Info("binding secret name is empty") - return nil, err + return nil, fmt.Errorf("binding secret not availble for binding %q'", serviceBinding.Name) } objRef := corev1.ObjectReference{ diff --git a/controllers/controllers/workloads/cf_buildpack_build_controller_test.go b/controllers/controllers/workloads/build/buildpack/controller_test.go similarity index 62% rename from controllers/controllers/workloads/cf_buildpack_build_controller_test.go rename to controllers/controllers/workloads/build/buildpack/controller_test.go index bdbecb71c..49b582d2b 100644 --- a/controllers/controllers/workloads/cf_buildpack_build_controller_test.go +++ b/controllers/controllers/workloads/build/buildpack/controller_test.go @@ -1,10 +1,9 @@ -package workloads_test +package buildpack_test import ( "context" korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" - . "code.cloudfoundry.org/korifi/controllers/controllers/workloads/testutils" "code.cloudfoundry.org/korifi/tools" "code.cloudfoundry.org/korifi/tools/k8s" "sigs.k8s.io/controller-runtime/pkg/client" @@ -20,12 +19,7 @@ import ( ) var _ = Describe("CFBuildpackBuildReconciler Integration Tests", func() { - const ( - wellFormedRegistryCredentialsSecret = "image-registry-credentials" - ) - var ( - cfSpace *korifiv1alpha1.CFSpace cfApp *korifiv1alpha1.CFApp cfPackage *korifiv1alpha1.CFPackage cfBuild *korifiv1alpha1.CFBuild @@ -36,20 +30,51 @@ var _ = Describe("CFBuildpackBuildReconciler Integration Tests", func() { Eventually(func(g Gomega) { workload := new(korifiv1alpha1.BuildWorkload) - lookupKey := types.NamespacedName{Name: cfBuild.Name, Namespace: cfSpace.Status.GUID} + lookupKey := types.NamespacedName{Name: cfBuild.Name, Namespace: testNamespace} g.Expect(adminClient.Get(context.Background(), lookupKey, workload)).To(Succeed()) assertion(workload, g) }).Should(Succeed()) } BeforeEach(func() { - cfSpace = createSpace(testOrg) + envSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testNamespace, + Name: uuid.NewString(), + }, + StringData: map[string]string{ + "a_key": "a-val", + "b_key": "b-val", + }, + } + Expect(adminClient.Create(ctx, envSecret)).To(Succeed()) + + vcapApplicationSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testNamespace, + Name: uuid.NewString(), + }, + StringData: map[string]string{ + "VCAP_APPLICATION": "{}", + }, + } + Expect(adminClient.Create(ctx, vcapApplicationSecret)).To(Succeed()) + + vcapServicesSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testNamespace, + Name: uuid.NewString(), + }, + StringData: map[string]string{ + "VCAP_SERVICES": "{}", + }, + } + Expect(adminClient.Create(ctx, vcapServicesSecret)).To(Succeed()) - cfAppGUID := uuid.NewString() cfApp = &korifiv1alpha1.CFApp{ ObjectMeta: metav1.ObjectMeta{ - Name: cfAppGUID, - Namespace: cfSpace.Status.GUID, + Name: uuid.NewString(), + Namespace: testNamespace, }, Spec: korifiv1alpha1.CFAppSpec{ DisplayName: "test-app-name", @@ -57,57 +82,64 @@ var _ = Describe("CFBuildpackBuildReconciler Integration Tests", func() { Lifecycle: korifiv1alpha1.Lifecycle{ Type: "buildpack", }, - EnvSecretName: cfAppGUID + "-env", + EnvSecretName: envSecret.Name, }, } Expect(adminClient.Create(ctx, cfApp)).To(Succeed()) - - Eventually(func(g Gomega) { - g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(cfApp), cfApp)).To(Succeed()) - g.Expect(cfApp.Status.VCAPServicesSecretName).NotTo(BeEmpty()) - }).Should(Succeed()) - - envVarSecret := BuildCFAppEnvVarsSecret(cfApp.Name, cfSpace.Status.GUID, map[string]string{ - "a_key": "a-val", - "b_key": "b-val", - }) - Expect(adminClient.Create(context.Background(), envVarSecret)).To(Succeed()) - - dockerRegistrySecret := BuildDockerRegistrySecret(wellFormedRegistryCredentialsSecret, cfSpace.Status.GUID) - Expect(adminClient.Create(ctx, dockerRegistrySecret)).To(Succeed()) - - registryServiceAccountName := "kpack-service-account" - registryServiceAccount := BuildServiceAccount(registryServiceAccountName, cfSpace.Status.GUID, wellFormedRegistryCredentialsSecret) - Expect(adminClient.Create(ctx, registryServiceAccount)).To(Succeed()) + Expect(k8s.Patch(ctx, adminClient, cfApp, func() { + cfApp.Status.VCAPServicesSecretName = vcapServicesSecret.Name + cfApp.Status.VCAPApplicationSecretName = vcapApplicationSecret.Name + })).To(Succeed()) cfPackage = &korifiv1alpha1.CFPackage{ ObjectMeta: metav1.ObjectMeta{ Name: uuid.NewString(), - Namespace: cfSpace.Status.GUID, + Namespace: testNamespace, }, Spec: korifiv1alpha1.CFPackageSpec{ Type: "bits", AppRef: corev1.LocalObjectReference{ - Name: cfAppGUID, + Name: cfApp.Name, + }, + Source: korifiv1alpha1.PackageSource{ + Registry: korifiv1alpha1.Registry{ + Image: "ref", + ImagePullSecrets: []corev1.LocalObjectReference{{Name: "source-registry-image-pull-secret"}}, + }, }, }, } Expect(adminClient.Create(ctx, cfPackage)).To(Succeed()) - kpackSecret := BuildDockerRegistrySecret("source-registry-image-pull-secret", cfSpace.Status.GUID) - Expect(adminClient.Create(ctx, kpackSecret)).To(Succeed()) + cfBuild = &korifiv1alpha1.CFBuild{ + ObjectMeta: metav1.ObjectMeta{ + Name: uuid.NewString(), + Namespace: testNamespace, + }, + Spec: korifiv1alpha1.CFBuildSpec{ + PackageRef: corev1.LocalObjectReference{ + Name: cfPackage.Name, + }, + AppRef: corev1.LocalObjectReference{ + Name: cfApp.Name, + }, + Lifecycle: korifiv1alpha1.Lifecycle{ + Type: "buildpack", + Data: korifiv1alpha1.LifecycleData{ + Buildpacks: []string{"first-buildpack", "second-buildpack"}, + }, + }, + }, + } }) JustBeforeEach(func() { - cfBuild = BuildCFBuildObject(uuid.NewString(), cfSpace.Status.GUID, cfPackage.Name, cfApp.Name) - cfBuild.Spec.Lifecycle.Data.Buildpacks = []string{"first-buildpack", "second-buildpack"} Expect(adminClient.Create(context.Background(), cfBuild)).To(Succeed()) }) It("creates a BuildWorkload with the buildRef, source, env, and buildpacks set", func() { - Expect(adminClient.Get(context.Background(), client.ObjectKeyFromObject(cfApp), cfApp)).To(Succeed()) - eventuallyBuildWorkloadShould(func(workload *korifiv1alpha1.BuildWorkload, g Gomega) { + g.Expect(workload.Spec.BuilderName).To(Equal("buildpack-builder-name")) g.Expect(workload.Spec.BuildRef.Name).To(Equal(cfBuild.Name)) g.Expect(workload.Spec.Source).To(Equal(cfPackage.Spec.Source)) g.Expect(workload.Spec.Env).To(ConsistOf( @@ -169,57 +201,33 @@ var _ = Describe("CFBuildpackBuildReconciler Integration Tests", func() { }) It("sets the 'build-running' status conditions on CFBuild", func() { - lookupKey := types.NamespacedName{Name: cfBuild.Name, Namespace: cfSpace.Status.GUID} Eventually(func(g Gomega) { - createdCFBuild := new(korifiv1alpha1.CFBuild) - g.Expect(adminClient.Get(context.Background(), lookupKey, createdCFBuild)).To(Succeed()) + g.Expect(adminClient.Get(context.Background(), client.ObjectKeyFromObject(cfBuild), cfBuild)).To(Succeed()) - stagingCondition := meta.FindStatusCondition(createdCFBuild.Status.Conditions, korifiv1alpha1.StagingConditionType) + stagingCondition := meta.FindStatusCondition(cfBuild.Status.Conditions, korifiv1alpha1.StagingConditionType) g.Expect(stagingCondition).NotTo(BeNil()) g.Expect(stagingCondition.Status).To(Equal(metav1.ConditionTrue)) g.Expect(stagingCondition.Reason).To(Equal("BuildRunning")) - g.Expect(stagingCondition.ObservedGeneration).To(Equal(createdCFBuild.Generation)) + g.Expect(stagingCondition.ObservedGeneration).To(Equal(cfBuild.Generation)) - succeededCondition := meta.FindStatusCondition(createdCFBuild.Status.Conditions, korifiv1alpha1.SucceededConditionType) + succeededCondition := meta.FindStatusCondition(cfBuild.Status.Conditions, korifiv1alpha1.SucceededConditionType) g.Expect(succeededCondition).NotTo(BeNil()) g.Expect(succeededCondition.Status).To(Equal(metav1.ConditionUnknown)) - g.Expect(succeededCondition.ObservedGeneration).To(Equal(createdCFBuild.Generation)) + g.Expect(succeededCondition.ObservedGeneration).To(Equal(cfBuild.Generation)) }).Should(Succeed()) }) - When("the referenced app has a ServiceBinding and Secret", func() { + When("the referenced app has a ServiceBinding", func() { BeforeEach(func() { - Expect(adminClient.Create(ctx, &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "service-secret", - Namespace: cfSpace.Status.GUID, - }, - })).To(Succeed()) - - Expect(adminClient.Create(ctx, &korifiv1alpha1.CFServiceInstance{ - ObjectMeta: metav1.ObjectMeta{ - Name: "service-instance-guid", - Namespace: cfSpace.Status.GUID, - }, - Spec: korifiv1alpha1.CFServiceInstanceSpec{ - SecretName: "service-secret", - Type: "user-provided", - }, - })).To(Succeed()) - serviceBinding := &korifiv1alpha1.CFServiceBinding{ ObjectMeta: metav1.ObjectMeta{ Name: "service-binding-guid", - Namespace: cfSpace.Status.GUID, + Namespace: testNamespace, Labels: map[string]string{ korifiv1alpha1.CFAppGUIDLabelKey: cfApp.Name, }, }, Spec: korifiv1alpha1.CFServiceBindingSpec{ - Service: corev1.ObjectReference{ - Kind: "ServiceInstance", - Name: "service-instance-guid", - }, AppRef: corev1.LocalObjectReference{ Name: cfApp.Name, }, @@ -229,16 +237,10 @@ var _ = Describe("CFBuildpackBuildReconciler Integration Tests", func() { Expect(k8s.Patch(ctx, adminClient, serviceBinding, func() { serviceBinding.Status.Binding.Name = "service-secret" - meta.SetStatusCondition(&serviceBinding.Status.Conditions, metav1.Condition{ - Type: "BindingSecretAvailable", - Status: metav1.ConditionTrue, - Reason: "SecretFound", - Message: "", - }) })).To(Succeed()) }) - It("creates a BuildWorkload with the underlying secret mapped onto it", func() { + It("adds the binding secret to the workload services", func() { eventuallyBuildWorkloadShould(func(workload *korifiv1alpha1.BuildWorkload, g Gomega) { g.Expect(workload.Spec.Services).To(ConsistOf( MatchFields(IgnoreExtras, Fields{ @@ -249,42 +251,16 @@ var _ = Describe("CFBuildpackBuildReconciler Integration Tests", func() { )) }) }) - - It("sets the VCAP_SERVICES env var in the image", func() { - createdCFApp := &korifiv1alpha1.CFApp{} - Expect(adminClient.Get(context.Background(), types.NamespacedName{Name: cfApp.Name, Namespace: cfSpace.Status.GUID}, createdCFApp)).To(Succeed()) - - eventuallyBuildWorkloadShould(func(workload *korifiv1alpha1.BuildWorkload, g Gomega) { - g.Expect(workload.Spec.Env).To(ContainElements( - MatchFields(IgnoreExtras, Fields{ - "Name": Equal("VCAP_SERVICES"), - "ValueFrom": PointTo(MatchFields(IgnoreExtras, Fields{ - "SecretKeyRef": PointTo(MatchFields(IgnoreExtras, Fields{ - "Key": Equal("VCAP_SERVICES"), - "LocalObjectReference": MatchFields(IgnoreExtras, Fields{ - "Name": Equal(createdCFApp.Status.VCAPServicesSecretName), - }), - })), - })), - }), - )) - }) - }) }) When("a BuildWorkload with CFBuild GUID already exists", func() { - var ( - newCFBuildGUID string - existingBuildWorkload *korifiv1alpha1.BuildWorkload - newCFBuild *korifiv1alpha1.CFBuild - ) + var existingBuildWorkload *korifiv1alpha1.BuildWorkload BeforeEach(func() { - newCFBuildGUID = PrefixedGUID("new-cf-build") existingBuildWorkload = &korifiv1alpha1.BuildWorkload{ ObjectMeta: metav1.ObjectMeta{ - Name: newCFBuildGUID, - Namespace: cfSpace.Status.GUID, + Name: cfBuild.Name, + Namespace: testNamespace, }, Spec: korifiv1alpha1.BuildWorkloadSpec{ Source: korifiv1alpha1.PackageSource{ @@ -295,40 +271,34 @@ var _ = Describe("CFBuildpackBuildReconciler Integration Tests", func() { }, }, } - newCFBuild = BuildCFBuildObject(newCFBuildGUID, cfSpace.Status.GUID, cfPackage.Name, cfApp.Name) - Expect(adminClient.Create(ctx, existingBuildWorkload)).To(Succeed()) - Expect(adminClient.Create(ctx, newCFBuild)).To(Succeed()) }) - It("sets the status conditions on CFBuild", func() { - lookupKey := types.NamespacedName{Name: newCFBuildGUID, Namespace: cfSpace.Status.GUID} + It("sets the status conditions on the CFBuild to running", func() { Eventually(func(g Gomega) { - createdCFBuild := new(korifiv1alpha1.CFBuild) - g.Expect(adminClient.Get(context.Background(), lookupKey, createdCFBuild)).To(Succeed()) + g.Expect(adminClient.Get(context.Background(), client.ObjectKeyFromObject(cfBuild), cfBuild)).To(Succeed()) - stagingCondition := meta.FindStatusCondition(createdCFBuild.Status.Conditions, korifiv1alpha1.StagingConditionType) + stagingCondition := meta.FindStatusCondition(cfBuild.Status.Conditions, korifiv1alpha1.StagingConditionType) g.Expect(stagingCondition).NotTo(BeNil()) g.Expect(stagingCondition.Status).To(Equal(metav1.ConditionTrue)) g.Expect(stagingCondition.Reason).To(Equal("BuildRunning")) - g.Expect(stagingCondition.ObservedGeneration).To(Equal(createdCFBuild.Generation)) + g.Expect(stagingCondition.ObservedGeneration).To(Equal(cfBuild.Generation)) - succeededCondition := meta.FindStatusCondition(createdCFBuild.Status.Conditions, korifiv1alpha1.SucceededConditionType) + succeededCondition := meta.FindStatusCondition(cfBuild.Status.Conditions, korifiv1alpha1.SucceededConditionType) g.Expect(succeededCondition).NotTo(BeNil()) g.Expect(succeededCondition.Status).To(Equal(metav1.ConditionUnknown)) - g.Expect(succeededCondition.ObservedGeneration).To(Equal(createdCFBuild.Generation)) + g.Expect(succeededCondition.ObservedGeneration).To(Equal(cfBuild.Generation)) }).Should(Succeed()) }) }) When("the BuildWorkload failed", func() { JustBeforeEach(func() { - testCtx := context.Background() - lookupKey := types.NamespacedName{Name: cfBuild.Name, Namespace: cfSpace.Status.GUID} + lookupKey := types.NamespacedName{Name: cfBuild.Name, Namespace: testNamespace} Eventually(func(g Gomega) { workload := new(korifiv1alpha1.BuildWorkload) - g.Expect(adminClient.Get(testCtx, lookupKey, workload)).To(Succeed()) - g.Expect(k8s.Patch(testCtx, adminClient, workload, func() { + g.Expect(adminClient.Get(ctx, lookupKey, workload)).To(Succeed()) + g.Expect(k8s.Patch(ctx, adminClient, workload, func() { meta.SetStatusCondition(&workload.Status.Conditions, metav1.Condition{ Type: korifiv1alpha1.SucceededConditionType, Status: metav1.ConditionFalse, @@ -339,22 +309,20 @@ var _ = Describe("CFBuildpackBuildReconciler Integration Tests", func() { }) It("sets the CFBuild status condition Succeeded = False", func() { - lookupKey := types.NamespacedName{Name: cfBuild.Name, Namespace: cfSpace.Status.GUID} - createdCFBuild := new(korifiv1alpha1.CFBuild) Eventually(func(g Gomega) { - g.Expect(adminClient.Get(context.Background(), lookupKey, createdCFBuild)).To(Succeed()) + g.Expect(adminClient.Get(context.Background(), client.ObjectKeyFromObject(cfBuild), cfBuild)).To(Succeed()) - stagingStatusCondition := meta.FindStatusCondition(createdCFBuild.Status.Conditions, korifiv1alpha1.StagingConditionType) + stagingStatusCondition := meta.FindStatusCondition(cfBuild.Status.Conditions, korifiv1alpha1.StagingConditionType) g.Expect(stagingStatusCondition).NotTo(BeNil()) g.Expect(stagingStatusCondition.Status).To(Equal(metav1.ConditionFalse)) g.Expect(stagingStatusCondition.Reason).To(Equal("BuildNotRunning")) - g.Expect(stagingStatusCondition.ObservedGeneration).To(Equal(createdCFBuild.Generation)) + g.Expect(stagingStatusCondition.ObservedGeneration).To(Equal(cfBuild.Generation)) - succeededStatusCondition := meta.FindStatusCondition(createdCFBuild.Status.Conditions, korifiv1alpha1.SucceededConditionType) + succeededStatusCondition := meta.FindStatusCondition(cfBuild.Status.Conditions, korifiv1alpha1.SucceededConditionType) g.Expect(succeededStatusCondition).NotTo(BeNil()) g.Expect(succeededStatusCondition.Status).To(Equal(metav1.ConditionFalse)) g.Expect(succeededStatusCondition.Reason).To(Equal("BuildFailed")) - g.Expect(succeededStatusCondition.ObservedGeneration).To(Equal(createdCFBuild.Generation)) + g.Expect(succeededStatusCondition.ObservedGeneration).To(Equal(cfBuild.Generation)) }).Should(Succeed()) }) }) @@ -366,17 +334,8 @@ var _ = Describe("CFBuildpackBuildReconciler Integration Tests", func() { buildStack = "cflinuxfs3" ) - var returnedProcessTypes []korifiv1alpha1.ProcessType - JustBeforeEach(func() { - returnedProcessTypes = []korifiv1alpha1.ProcessType{ - { - Type: "web", - Command: "run-stuff", - }, - } - - lookupKey := types.NamespacedName{Name: cfBuild.Name, Namespace: cfSpace.Status.GUID} + lookupKey := types.NamespacedName{Name: cfBuild.Name, Namespace: testNamespace} Eventually(func(g Gomega) { workload := new(korifiv1alpha1.BuildWorkload) g.Expect(adminClient.Get(ctx, lookupKey, workload)).To(Succeed()) @@ -391,9 +350,12 @@ var _ = Describe("CFBuildpackBuildReconciler Integration Tests", func() { Image: buildImageRef, ImagePullSecrets: []corev1.LocalObjectReference{{Name: imagePullSecretName}}, }, - Stack: buildStack, - Ports: []int32{42}, - ProcessTypes: returnedProcessTypes, + Stack: buildStack, + Ports: []int32{42}, + ProcessTypes: []korifiv1alpha1.ProcessType{{ + Type: "web", + Command: "run-stuff", + }}, } })).To(Succeed()) }).Should(Succeed()) @@ -423,7 +385,10 @@ var _ = Describe("CFBuildpackBuildReconciler Integration Tests", func() { g.Expect(cfBuild.Status.Droplet.Registry.Image).To(Equal(buildImageRef)) g.Expect(cfBuild.Status.Droplet.Registry.ImagePullSecrets).To(ConsistOf(corev1.LocalObjectReference{Name: imagePullSecretName})) g.Expect(cfBuild.Status.Droplet.Stack).To(Equal(buildStack)) - g.Expect(cfBuild.Status.Droplet.ProcessTypes).To(Equal(returnedProcessTypes)) + g.Expect(cfBuild.Status.Droplet.ProcessTypes).To(ConsistOf(korifiv1alpha1.ProcessType{ + Type: "web", + Command: "run-stuff", + })) g.Expect(cfBuild.Status.Droplet.Ports).To(ConsistOf(BeEquivalentTo(42))) }).Should(Succeed()) }) diff --git a/controllers/controllers/workloads/build/buildpack/suite_test.go b/controllers/controllers/workloads/build/buildpack/suite_test.go new file mode 100644 index 000000000..46ab31ac6 --- /dev/null +++ b/controllers/controllers/workloads/build/buildpack/suite_test.go @@ -0,0 +1,102 @@ +package buildpack_test + +import ( + "context" + "path/filepath" + "testing" + "time" + + korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" + "code.cloudfoundry.org/korifi/controllers/config" + "code.cloudfoundry.org/korifi/controllers/controllers/shared" + "code.cloudfoundry.org/korifi/controllers/controllers/workloads/build/buildpack" + buildfake "code.cloudfoundry.org/korifi/controllers/controllers/workloads/build/fake" + "code.cloudfoundry.org/korifi/controllers/controllers/workloads/env" + "code.cloudfoundry.org/korifi/tests/helpers" + + "github.com/google/uuid" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "go.uber.org/zap/zapcore" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/scheme" + 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" +) + +var ( + ctx context.Context + stopManager context.CancelFunc + stopClientCache context.CancelFunc + testEnv *envtest.Environment + adminClient client.Client + testNamespace string +) + +func TestWorkloadsControllers(t *testing.T) { + SetDefaultEventuallyTimeout(10 * time.Second) + SetDefaultEventuallyPollingInterval(250 * time.Millisecond) + + RegisterFailHandler(Fail) + RunSpecs(t, "Buildpack CFBuild Controllers Integration Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true), zap.Level(zapcore.DebugLevel))) + + ctx = context.Background() + + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{ + filepath.Join("..", "..", "..", "..", "..", "helm", "korifi", "controllers", "crds"), + }, + ErrorIfCRDPathMissing: true, + } + + _, err := testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + + Expect(korifiv1alpha1.AddToScheme(scheme.Scheme)).To(Succeed()) + Expect(corev1.AddToScheme(scheme.Scheme)).To(Succeed()) + + k8sManager := helpers.NewK8sManager(testEnv, filepath.Join("helm", "korifi", "controllers", "role.yaml")) + Expect(shared.SetupIndexWithManager(k8sManager)).To(Succeed()) + + adminClient, stopClientCache = helpers.NewCachedClient(testEnv.Config) + + controllerConfig := &config.ControllerConfig{ + BuilderName: "buildpack-builder-name", + } + + cfBuildpackBuildReconciler := buildpack.NewReconciler( + k8sManager.GetClient(), + new(buildfake.BuildCleaner), + k8sManager.GetScheme(), + ctrl.Log.WithName("controllers").WithName("CFBuildpackBuild"), + controllerConfig, + env.NewAppEnvBuilder(k8sManager.GetClient()), + ) + err = (cfBuildpackBuildReconciler).SetupWithManager(k8sManager) + Expect(err).NotTo(HaveOccurred()) + + stopManager = helpers.StartK8sManager(k8sManager) +}) + +var _ = BeforeEach(func() { + testNamespace = uuid.NewString() + Expect(adminClient.Create(ctx, &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: testNamespace, + }, + })).To(Succeed()) +}) + +var _ = AfterSuite(func() { + stopManager() + stopClientCache() + Expect(testEnv.Stop()).To(Succeed()) +}) diff --git a/controllers/controllers/workloads/build/cf_build_controller.go b/controllers/controllers/workloads/build/controller.go similarity index 89% rename from controllers/controllers/workloads/build/cf_build_controller.go rename to controllers/controllers/workloads/build/controller.go index 2bb2fb88a..d9d8dbcf4 100644 --- a/controllers/controllers/workloads/build/cf_build_controller.go +++ b/controllers/controllers/workloads/build/controller.go @@ -24,19 +24,19 @@ type BuildCleaner interface { Clean(ctx context.Context, app types.NamespacedName) error } -//counterfeiter:generate -o fake -fake-name BuildReconciler . BuildReconciler +//counterfeiter:generate -o fake -fake-name DelegateReconciler . DelegateReconciler -type BuildReconciler interface { +type DelegateReconciler interface { ReconcileBuild(context.Context, *korifiv1alpha1.CFBuild, *korifiv1alpha1.CFApp, *korifiv1alpha1.CFPackage) (ctrl.Result, error) SetupWithManager(ctrl.Manager) *builder.Builder } -type CFBuildReconciler struct { +type Reconciler struct { log logr.Logger k8sClient client.Client scheme *runtime.Scheme buildCleaner BuildCleaner - delegate BuildReconciler + delegate DelegateReconciler } var packageTypeToLifecycleType = map[korifiv1alpha1.PackageType]korifiv1alpha1.LifecycleType{ @@ -44,14 +44,14 @@ var packageTypeToLifecycleType = map[korifiv1alpha1.PackageType]korifiv1alpha1.L "docker": "docker", } -func NewCFBuildReconciler( +func NewReconciler( log logr.Logger, k8sClient client.Client, scheme *runtime.Scheme, buildCleaner BuildCleaner, - delegate BuildReconciler, -) *CFBuildReconciler { - return &CFBuildReconciler{ + delegate DelegateReconciler, +) *Reconciler { + return &Reconciler{ log: log, k8sClient: k8sClient, scheme: scheme, @@ -60,11 +60,11 @@ func NewCFBuildReconciler( } } -func (r *CFBuildReconciler) SetupWithManager(mgr ctrl.Manager) *builder.Builder { +func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) *builder.Builder { return r.delegate.SetupWithManager(mgr) } -func (r *CFBuildReconciler) ReconcileResource(ctx context.Context, cfBuild *korifiv1alpha1.CFBuild) (ctrl.Result, error) { +func (r *Reconciler) ReconcileResource(ctx context.Context, cfBuild *korifiv1alpha1.CFBuild) (ctrl.Result, error) { log := logr.FromContextOrDiscard(ctx) cfBuild.Status.ObservedGeneration = cfBuild.Generation diff --git a/controllers/controllers/workloads/build/cf_build_controller_test.go b/controllers/controllers/workloads/build/controller_test.go similarity index 100% rename from controllers/controllers/workloads/build/cf_build_controller_test.go rename to controllers/controllers/workloads/build/controller_test.go diff --git a/controllers/controllers/workloads/cf_docker_build_controller.go b/controllers/controllers/workloads/build/docker/controller.go similarity index 98% rename from controllers/controllers/workloads/cf_docker_build_controller.go rename to controllers/controllers/workloads/build/docker/controller.go index cef85a0c7..3fcdad3a5 100644 --- a/controllers/controllers/workloads/cf_docker_build_controller.go +++ b/controllers/controllers/workloads/build/docker/controller.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package workloads +package docker import ( "context" @@ -40,7 +40,7 @@ type ImageConfigGetter interface { Config(context.Context, image.Creds, string) (image.Config, error) } -func NewCFDockerBuildReconciler( +func NewReconciler( k8sClient client.Client, buildCleaner build.BuildCleaner, imageConfigGetter ImageConfigGetter, @@ -50,7 +50,7 @@ func NewCFDockerBuildReconciler( return k8s.NewPatchingReconciler[korifiv1alpha1.CFBuild, *korifiv1alpha1.CFBuild]( log, k8sClient, - build.NewCFBuildReconciler( + build.NewReconciler( log, k8sClient, scheme, diff --git a/controllers/controllers/workloads/cf_docker_build_controller_test.go b/controllers/controllers/workloads/build/docker/controller_test.go similarity index 92% rename from controllers/controllers/workloads/cf_docker_build_controller_test.go rename to controllers/controllers/workloads/build/docker/controller_test.go index a5e45fefb..6ff045a7f 100644 --- a/controllers/controllers/workloads/cf_docker_build_controller_test.go +++ b/controllers/controllers/workloads/build/docker/controller_test.go @@ -1,8 +1,7 @@ -package workloads_test +package docker_test import ( korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" - . "code.cloudfoundry.org/korifi/controllers/controllers/workloads/testutils" "code.cloudfoundry.org/korifi/tools/dockercfg" "code.cloudfoundry.org/korifi/tools/k8s" corev1 "k8s.io/api/core/v1" @@ -11,6 +10,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/uuid" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/onsi/gomega/gstruct" @@ -22,7 +22,6 @@ var _ = Describe("CFDockerBuildReconciler Integration Tests", func() { imageConfig *v1.ConfigFile imageRef string - cfSpace *korifiv1alpha1.CFSpace cfApp *korifiv1alpha1.CFApp cfPackage *korifiv1alpha1.CFPackage cfBuild *korifiv1alpha1.CFBuild @@ -36,12 +35,10 @@ var _ = Describe("CFDockerBuildReconciler Integration Tests", func() { }, } - cfSpace = createSpace(testOrg) - var err error imageSecret, err = dockercfg.CreateDockerConfigSecret( - cfSpace.Status.GUID, - PrefixedGUID("image-secret"), + testNamespace, + uuid.NewString(), dockercfg.DockerServerConfig{ Server: containerRegistry.URL(), Username: "user", @@ -53,11 +50,11 @@ var _ = Describe("CFDockerBuildReconciler Integration Tests", func() { cfApp = &korifiv1alpha1.CFApp{ ObjectMeta: metav1.ObjectMeta{ - Name: PrefixedGUID("cf-app"), - Namespace: cfSpace.Status.GUID, + Name: uuid.NewString(), + Namespace: testNamespace, }, Spec: korifiv1alpha1.CFAppSpec{ - DisplayName: PrefixedGUID("cf-app-display-name"), + DisplayName: uuid.NewString(), DesiredState: "STOPPED", Lifecycle: korifiv1alpha1.Lifecycle{ Type: "docker", @@ -69,8 +66,8 @@ var _ = Describe("CFDockerBuildReconciler Integration Tests", func() { cfPackage = &korifiv1alpha1.CFPackage{ ObjectMeta: metav1.ObjectMeta{ - Name: PrefixedGUID("cf-package"), - Namespace: cfSpace.Status.GUID, + Name: uuid.NewString(), + Namespace: testNamespace, }, Spec: korifiv1alpha1.CFPackageSpec{ Type: "docker", @@ -89,8 +86,8 @@ var _ = Describe("CFDockerBuildReconciler Integration Tests", func() { cfBuild = &korifiv1alpha1.CFBuild{ ObjectMeta: metav1.ObjectMeta{ - Name: PrefixedGUID("cf-build"), - Namespace: cfSpace.Status.GUID, + Name: uuid.NewString(), + Namespace: testNamespace, }, Spec: korifiv1alpha1.CFBuildSpec{ PackageRef: corev1.LocalObjectReference{ diff --git a/controllers/controllers/workloads/build/docker/suite_test.go b/controllers/controllers/workloads/build/docker/suite_test.go new file mode 100644 index 000000000..dbe00f22a --- /dev/null +++ b/controllers/controllers/workloads/build/docker/suite_test.go @@ -0,0 +1,103 @@ +package docker_test + +import ( + "context" + "path/filepath" + "testing" + "time" + + korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" + "code.cloudfoundry.org/korifi/controllers/controllers/shared" + "code.cloudfoundry.org/korifi/controllers/controllers/workloads/build/docker" + buildfake "code.cloudfoundry.org/korifi/controllers/controllers/workloads/build/fake" + "code.cloudfoundry.org/korifi/tests/helpers" + "code.cloudfoundry.org/korifi/tests/helpers/oci" + "code.cloudfoundry.org/korifi/tools/image" + + "github.com/google/uuid" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "go.uber.org/zap/zapcore" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + k8sclient "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/scheme" + 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" +) + +var ( + ctx context.Context + stopManager context.CancelFunc + stopClientCache context.CancelFunc + testEnv *envtest.Environment + adminClient client.Client + testNamespace string + containerRegistry *oci.Registry +) + +func TestWorkloadsControllers(t *testing.T) { + SetDefaultEventuallyTimeout(10 * time.Second) + SetDefaultEventuallyPollingInterval(250 * time.Millisecond) + + RegisterFailHandler(Fail) + RunSpecs(t, "Docker CFBuild Controllers Integration Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true), zap.Level(zapcore.DebugLevel))) + + ctx = context.Background() + + containerRegistry = oci.NewContainerRegistry("user", "password") + + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{ + filepath.Join("..", "..", "..", "..", "..", "helm", "korifi", "controllers", "crds"), + }, + ErrorIfCRDPathMissing: true, + } + + _, err := testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + + Expect(korifiv1alpha1.AddToScheme(scheme.Scheme)).To(Succeed()) + Expect(corev1.AddToScheme(scheme.Scheme)).To(Succeed()) + + k8sManager := helpers.NewK8sManager(testEnv, filepath.Join("helm", "korifi", "controllers", "role.yaml")) + Expect(shared.SetupIndexWithManager(k8sManager)).To(Succeed()) + + adminClient, stopClientCache = helpers.NewCachedClient(testEnv.Config) + + k8sClient, err := k8sclient.NewForConfig(k8sManager.GetConfig()) + Expect(err).NotTo(HaveOccurred()) + + err = docker.NewReconciler( + k8sManager.GetClient(), + new(buildfake.BuildCleaner), + image.NewClient(k8sClient), + k8sManager.GetScheme(), + ctrl.Log.WithName("controllers").WithName("CFDockerBuild"), + ).SetupWithManager(k8sManager) + Expect(err).NotTo(HaveOccurred()) + + stopManager = helpers.StartK8sManager(k8sManager) +}) + +var _ = BeforeEach(func() { + testNamespace = uuid.NewString() + Expect(adminClient.Create(ctx, &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: testNamespace, + }, + })).To(Succeed()) +}) + +var _ = AfterSuite(func() { + stopManager() + stopClientCache() + Expect(testEnv.Stop()).To(Succeed()) +}) diff --git a/controllers/controllers/workloads/build/fake/build_reconciler.go b/controllers/controllers/workloads/build/fake/delegate_reconciler.go similarity index 77% rename from controllers/controllers/workloads/build/fake/build_reconciler.go rename to controllers/controllers/workloads/build/fake/delegate_reconciler.go index 4832ba018..292c33dd2 100644 --- a/controllers/controllers/workloads/build/fake/build_reconciler.go +++ b/controllers/controllers/workloads/build/fake/delegate_reconciler.go @@ -12,7 +12,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" ) -type BuildReconciler struct { +type DelegateReconciler struct { ReconcileBuildStub func(context.Context, *v1alpha1.CFBuild, *v1alpha1.CFApp, *v1alpha1.CFPackage) (reconcile.Result, error) reconcileBuildMutex sync.RWMutex reconcileBuildArgsForCall []struct { @@ -44,7 +44,7 @@ type BuildReconciler struct { invocationsMutex sync.RWMutex } -func (fake *BuildReconciler) ReconcileBuild(arg1 context.Context, arg2 *v1alpha1.CFBuild, arg3 *v1alpha1.CFApp, arg4 *v1alpha1.CFPackage) (reconcile.Result, error) { +func (fake *DelegateReconciler) ReconcileBuild(arg1 context.Context, arg2 *v1alpha1.CFBuild, arg3 *v1alpha1.CFApp, arg4 *v1alpha1.CFPackage) (reconcile.Result, error) { fake.reconcileBuildMutex.Lock() ret, specificReturn := fake.reconcileBuildReturnsOnCall[len(fake.reconcileBuildArgsForCall)] fake.reconcileBuildArgsForCall = append(fake.reconcileBuildArgsForCall, struct { @@ -66,26 +66,26 @@ func (fake *BuildReconciler) ReconcileBuild(arg1 context.Context, arg2 *v1alpha1 return fakeReturns.result1, fakeReturns.result2 } -func (fake *BuildReconciler) ReconcileBuildCallCount() int { +func (fake *DelegateReconciler) ReconcileBuildCallCount() int { fake.reconcileBuildMutex.RLock() defer fake.reconcileBuildMutex.RUnlock() return len(fake.reconcileBuildArgsForCall) } -func (fake *BuildReconciler) ReconcileBuildCalls(stub func(context.Context, *v1alpha1.CFBuild, *v1alpha1.CFApp, *v1alpha1.CFPackage) (reconcile.Result, error)) { +func (fake *DelegateReconciler) ReconcileBuildCalls(stub func(context.Context, *v1alpha1.CFBuild, *v1alpha1.CFApp, *v1alpha1.CFPackage) (reconcile.Result, error)) { fake.reconcileBuildMutex.Lock() defer fake.reconcileBuildMutex.Unlock() fake.ReconcileBuildStub = stub } -func (fake *BuildReconciler) ReconcileBuildArgsForCall(i int) (context.Context, *v1alpha1.CFBuild, *v1alpha1.CFApp, *v1alpha1.CFPackage) { +func (fake *DelegateReconciler) ReconcileBuildArgsForCall(i int) (context.Context, *v1alpha1.CFBuild, *v1alpha1.CFApp, *v1alpha1.CFPackage) { fake.reconcileBuildMutex.RLock() defer fake.reconcileBuildMutex.RUnlock() argsForCall := fake.reconcileBuildArgsForCall[i] return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4 } -func (fake *BuildReconciler) ReconcileBuildReturns(result1 reconcile.Result, result2 error) { +func (fake *DelegateReconciler) ReconcileBuildReturns(result1 reconcile.Result, result2 error) { fake.reconcileBuildMutex.Lock() defer fake.reconcileBuildMutex.Unlock() fake.ReconcileBuildStub = nil @@ -95,7 +95,7 @@ func (fake *BuildReconciler) ReconcileBuildReturns(result1 reconcile.Result, res }{result1, result2} } -func (fake *BuildReconciler) ReconcileBuildReturnsOnCall(i int, result1 reconcile.Result, result2 error) { +func (fake *DelegateReconciler) ReconcileBuildReturnsOnCall(i int, result1 reconcile.Result, result2 error) { fake.reconcileBuildMutex.Lock() defer fake.reconcileBuildMutex.Unlock() fake.ReconcileBuildStub = nil @@ -111,7 +111,7 @@ func (fake *BuildReconciler) ReconcileBuildReturnsOnCall(i int, result1 reconcil }{result1, result2} } -func (fake *BuildReconciler) SetupWithManager(arg1 manager.Manager) *builder.Builder { +func (fake *DelegateReconciler) SetupWithManager(arg1 manager.Manager) *builder.Builder { fake.setupWithManagerMutex.Lock() ret, specificReturn := fake.setupWithManagerReturnsOnCall[len(fake.setupWithManagerArgsForCall)] fake.setupWithManagerArgsForCall = append(fake.setupWithManagerArgsForCall, struct { @@ -130,26 +130,26 @@ func (fake *BuildReconciler) SetupWithManager(arg1 manager.Manager) *builder.Bui return fakeReturns.result1 } -func (fake *BuildReconciler) SetupWithManagerCallCount() int { +func (fake *DelegateReconciler) SetupWithManagerCallCount() int { fake.setupWithManagerMutex.RLock() defer fake.setupWithManagerMutex.RUnlock() return len(fake.setupWithManagerArgsForCall) } -func (fake *BuildReconciler) SetupWithManagerCalls(stub func(manager.Manager) *builder.Builder) { +func (fake *DelegateReconciler) SetupWithManagerCalls(stub func(manager.Manager) *builder.Builder) { fake.setupWithManagerMutex.Lock() defer fake.setupWithManagerMutex.Unlock() fake.SetupWithManagerStub = stub } -func (fake *BuildReconciler) SetupWithManagerArgsForCall(i int) manager.Manager { +func (fake *DelegateReconciler) SetupWithManagerArgsForCall(i int) manager.Manager { fake.setupWithManagerMutex.RLock() defer fake.setupWithManagerMutex.RUnlock() argsForCall := fake.setupWithManagerArgsForCall[i] return argsForCall.arg1 } -func (fake *BuildReconciler) SetupWithManagerReturns(result1 *builder.Builder) { +func (fake *DelegateReconciler) SetupWithManagerReturns(result1 *builder.Builder) { fake.setupWithManagerMutex.Lock() defer fake.setupWithManagerMutex.Unlock() fake.SetupWithManagerStub = nil @@ -158,7 +158,7 @@ func (fake *BuildReconciler) SetupWithManagerReturns(result1 *builder.Builder) { }{result1} } -func (fake *BuildReconciler) SetupWithManagerReturnsOnCall(i int, result1 *builder.Builder) { +func (fake *DelegateReconciler) SetupWithManagerReturnsOnCall(i int, result1 *builder.Builder) { fake.setupWithManagerMutex.Lock() defer fake.setupWithManagerMutex.Unlock() fake.SetupWithManagerStub = nil @@ -172,7 +172,7 @@ func (fake *BuildReconciler) SetupWithManagerReturnsOnCall(i int, result1 *build }{result1} } -func (fake *BuildReconciler) Invocations() map[string][][]interface{} { +func (fake *DelegateReconciler) Invocations() map[string][][]interface{} { fake.invocationsMutex.RLock() defer fake.invocationsMutex.RUnlock() fake.reconcileBuildMutex.RLock() @@ -186,7 +186,7 @@ func (fake *BuildReconciler) Invocations() map[string][][]interface{} { return copiedInvocations } -func (fake *BuildReconciler) recordInvocation(key string, args []interface{}) { +func (fake *DelegateReconciler) recordInvocation(key string, args []interface{}) { fake.invocationsMutex.Lock() defer fake.invocationsMutex.Unlock() if fake.invocations == nil { @@ -198,4 +198,4 @@ func (fake *BuildReconciler) recordInvocation(key string, args []interface{}) { fake.invocations[key] = append(fake.invocations[key], args) } -var _ build.BuildReconciler = new(BuildReconciler) +var _ build.DelegateReconciler = new(DelegateReconciler) diff --git a/controllers/controllers/workloads/build/suite_test.go b/controllers/controllers/workloads/build/suite_test.go index 7321a197d..184de92eb 100644 --- a/controllers/controllers/workloads/build/suite_test.go +++ b/controllers/controllers/workloads/build/suite_test.go @@ -11,16 +11,11 @@ import ( "code.cloudfoundry.org/korifi/controllers/controllers/shared" "code.cloudfoundry.org/korifi/controllers/controllers/workloads/build" "code.cloudfoundry.org/korifi/controllers/controllers/workloads/build/fake" - "code.cloudfoundry.org/korifi/controllers/controllers/workloads/testutils" - "code.cloudfoundry.org/korifi/controllers/coordination" - "code.cloudfoundry.org/korifi/controllers/webhooks" - "code.cloudfoundry.org/korifi/controllers/webhooks/finalizer" - "code.cloudfoundry.org/korifi/controllers/webhooks/version" - "code.cloudfoundry.org/korifi/controllers/webhooks/workloads" "code.cloudfoundry.org/korifi/tests/helpers" "code.cloudfoundry.org/korifi/tools/k8s" "github.com/go-logr/logr" + "github.com/google/uuid" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "go.uber.org/zap/zapcore" @@ -69,9 +64,6 @@ var _ = BeforeSuite(func() { CRDDirectoryPaths: []string{ filepath.Join("..", "..", "..", "..", "helm", "korifi", "controllers", "crds"), }, - WebhookInstallOptions: envtest.WebhookInstallOptions{ - Paths: []string{filepath.Join("..", "..", "..", "..", "helm", "korifi", "controllers", "manifests.yaml")}, - }, ErrorIfCRDPathMissing: true, } @@ -83,21 +75,9 @@ var _ = BeforeSuite(func() { k8sManager := helpers.NewK8sManager(testEnv, filepath.Join("helm", "korifi", "controllers", "role.yaml")) Expect(shared.SetupIndexWithManager(k8sManager)).To(Succeed()) - uncachedClient := helpers.NewUncachedClient(k8sManager.GetConfig()) - Expect((&korifiv1alpha1.CFApp{}).SetupWebhookWithManager(k8sManager)).To(Succeed()) - finalizer.NewControllersFinalizerWebhook().SetupWebhookWithManager(k8sManager) - version.NewVersionWebhook("some-version").SetupWebhookWithManager(k8sManager) - Expect(workloads.NewCFAppValidator( - webhooks.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, workloads.AppEntityType)), - ).SetupWebhookWithManager(k8sManager)).To(Succeed()) - Expect((&korifiv1alpha1.CFPackage{}).SetupWebhookWithManager(k8sManager)).To(Succeed()) - Expect((&korifiv1alpha1.CFBuild{}).SetupWebhookWithManager(k8sManager)).To(Succeed()) - adminClient, stopClientCache = helpers.NewCachedClient(testEnv.Config) - testNamespace = testutils.PrefixedGUID("test-namespace") - - delegateReconciler := new(fake.BuildReconciler) + delegateReconciler := new(fake.DelegateReconciler) delegateReconciler.SetupWithManagerStub = func(mgr ctrl.Manager) *builder.Builder { return ctrl.NewControllerManagedBy(mgr). For(&korifiv1alpha1.CFBuild{}) @@ -140,7 +120,7 @@ var _ = BeforeSuite(func() { Expect(k8s.NewPatchingReconciler[korifiv1alpha1.CFBuild, *korifiv1alpha1.CFBuild]( ctrl.Log.WithName("controllers").WithName("CFBuild"), k8sManager.GetClient(), - build.NewCFBuildReconciler( + build.NewReconciler( ctrl.Log.WithName("controllers").WithName("CFBuild"), k8sManager.GetClient(), scheme.Scheme, @@ -150,8 +130,15 @@ var _ = BeforeSuite(func() { ).SetupWithManager(k8sManager)).To(Succeed()) stopManager = helpers.StartK8sManager(k8sManager) +}) - createNamespace(testNamespace) +var _ = BeforeEach(func() { + testNamespace = uuid.NewString() + Expect(adminClient.Create(ctx, &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: testNamespace, + }, + })).To(Succeed()) }) var _ = AfterSuite(func() { @@ -159,14 +146,3 @@ var _ = AfterSuite(func() { stopClientCache() Expect(testEnv.Stop()).To(Succeed()) }) - -func createNamespace(name string) *corev1.Namespace { - ns := &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - }, - } - Expect( - adminClient.Create(ctx, ns)).To(Succeed()) - return ns -} diff --git a/controllers/controllers/workloads/env/env_suite_test.go b/controllers/controllers/workloads/env/env_suite_test.go index 9b48f0340..450fd5ecb 100644 --- a/controllers/controllers/workloads/env/env_suite_test.go +++ b/controllers/controllers/workloads/env/env_suite_test.go @@ -11,9 +11,9 @@ import ( k8serrors "k8s.io/apimachinery/pkg/api/errors" "code.cloudfoundry.org/korifi/controllers/controllers/shared" - "code.cloudfoundry.org/korifi/controllers/controllers/workloads/testutils" "code.cloudfoundry.org/korifi/tools/k8s" + "github.com/google/uuid" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" servicebindingv1beta1 "github.com/servicebinding/runtime/apis/v1beta1" @@ -84,20 +84,20 @@ var _ = AfterSuite(func() { var _ = BeforeEach(func() { ctx = context.Background() - rootNamespace = testutils.PrefixedGUID("root-namespace") + rootNamespace = uuid.NewString() createNamespace(rootNamespace) cfOrg = &korifiv1alpha1.CFOrg{ ObjectMeta: metav1.ObjectMeta{ - Name: testutils.PrefixedGUID("org"), + Name: uuid.NewString(), Namespace: rootNamespace, }, Spec: korifiv1alpha1.CFOrgSpec{ - DisplayName: testutils.PrefixedGUID("org"), + DisplayName: uuid.NewString(), }, } ensureCreate(cfOrg) - orgNSName := testutils.PrefixedGUID("org") + orgNSName := uuid.NewString() ensurePatch(cfOrg, func(cfOrg *korifiv1alpha1.CFOrg) { cfOrg.Status.GUID = orgNSName }) @@ -105,15 +105,15 @@ var _ = BeforeEach(func() { cfSpace = &korifiv1alpha1.CFSpace{ ObjectMeta: metav1.ObjectMeta{ - Name: testutils.PrefixedGUID("space"), + Name: uuid.NewString(), Namespace: cfOrg.Status.GUID, }, Spec: korifiv1alpha1.CFSpaceSpec{ - DisplayName: testutils.PrefixedGUID("space"), + DisplayName: uuid.NewString(), }, } ensureCreate(cfSpace) - cfNSName := testutils.PrefixedGUID("space") + cfNSName := uuid.NewString() ensurePatch(cfSpace, func(cfSpace *korifiv1alpha1.CFSpace) { cfSpace.Status.GUID = cfNSName }) diff --git a/controllers/controllers/workloads/k8sns/reconciler_test.go b/controllers/controllers/workloads/k8sns/reconciler_test.go index 02bc2a18b..37d3585c1 100644 --- a/controllers/controllers/workloads/k8sns/reconciler_test.go +++ b/controllers/controllers/workloads/k8sns/reconciler_test.go @@ -7,7 +7,6 @@ import ( korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" "code.cloudfoundry.org/korifi/controllers/controllers/workloads/k8sns" - . "code.cloudfoundry.org/korifi/controllers/controllers/workloads/testutils" "code.cloudfoundry.org/korifi/tools" "code.cloudfoundry.org/korifi/tools/k8s" "golang.org/x/exp/maps" @@ -69,7 +68,7 @@ var _ = Describe("K8S NS Reconciler Integration Tests", func() { } reconciler = k8sns.NewReconciler[korifiv1alpha1.CFOrg, *korifiv1alpha1.CFOrg](controllersClient, finalizer, metadataCompiler, []string{}) - orgGUID = PrefixedGUID("cf-org") + orgGUID = uuid.NewString() nsObj = &korifiv1alpha1.CFOrg{ ObjectMeta: metav1.ObjectMeta{ Name: orgGUID, diff --git a/controllers/controllers/workloads/cforg_controller.go b/controllers/controllers/workloads/orgs/controller.go similarity index 93% rename from controllers/controllers/workloads/cforg_controller.go rename to controllers/controllers/workloads/orgs/controller.go index 375a01c90..94f91ed1d 100644 --- a/controllers/controllers/workloads/cforg_controller.go +++ b/controllers/controllers/workloads/orgs/controller.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package workloads +package orgs import ( "context" @@ -35,12 +35,12 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" ) -type CFOrgReconciler struct { +type Reconciler struct { client client.Client namespaceReconciler *k8sns.Reconciler[korifiv1alpha1.CFOrg, *korifiv1alpha1.CFOrg] } -func NewCFOrgReconciler( +func NewReconciler( client client.Client, log logr.Logger, containerRegistrySecretNames []string, @@ -59,13 +59,13 @@ func NewCFOrgReconciler( containerRegistrySecretNames, ) - return k8s.NewPatchingReconciler[korifiv1alpha1.CFOrg, *korifiv1alpha1.CFOrg](log, client, &CFOrgReconciler{ + return k8s.NewPatchingReconciler[korifiv1alpha1.CFOrg, *korifiv1alpha1.CFOrg](log, client, &Reconciler{ client: client, namespaceReconciler: namespaceController, }) } -func (r *CFOrgReconciler) SetupWithManager(mgr ctrl.Manager) *builder.Builder { +func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) *builder.Builder { return ctrl.NewControllerManagedBy(mgr). For(&korifiv1alpha1.CFOrg{}). Watches( @@ -78,7 +78,7 @@ func (r *CFOrgReconciler) SetupWithManager(mgr ctrl.Manager) *builder.Builder { ) } -func (r *CFOrgReconciler) enqueueCFOrgRequests(ctx context.Context, object client.Object) []reconcile.Request { +func (r *Reconciler) enqueueCFOrgRequests(ctx context.Context, object client.Object) []reconcile.Request { cfOrgList := &korifiv1alpha1.CFOrgList{} err := r.client.List(ctx, cfOrgList, client.InNamespace(object.GetNamespace())) if err != nil { @@ -120,7 +120,7 @@ func (r *CFOrgReconciler) enqueueCFOrgRequests(ctx context.Context, object clien //+kubebuilder:rbac:groups="policy",resources=poddisruptionbudgets,verbs=create;deletecollection //+kubebuilder:rbac:groups="policy",resources=podsecuritypolicies,verbs=use -func (r *CFOrgReconciler) ReconcileResource(ctx context.Context, cfOrg *korifiv1alpha1.CFOrg) (ctrl.Result, error) { +func (r *Reconciler) ReconcileResource(ctx context.Context, cfOrg *korifiv1alpha1.CFOrg) (ctrl.Result, error) { var err error readyConditionBuilder := k8s.NewReadyConditionBuilder(cfOrg) defer func() { diff --git a/controllers/controllers/workloads/cforg_controller_test.go b/controllers/controllers/workloads/orgs/controller_test.go similarity index 73% rename from controllers/controllers/workloads/cforg_controller_test.go rename to controllers/controllers/workloads/orgs/controller_test.go index 105759e9b..a9a4c1b18 100644 --- a/controllers/controllers/workloads/cforg_controller_test.go +++ b/controllers/controllers/workloads/orgs/controller_test.go @@ -1,4 +1,4 @@ -package workloads_test +package orgs_test import ( korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" @@ -21,7 +21,10 @@ var _ = Describe("CFOrgReconciler Integration Tests", func() { cfOrg = &korifiv1alpha1.CFOrg{ ObjectMeta: metav1.ObjectMeta{ Name: uuid.NewString(), - Namespace: cfRootNamespace, + Namespace: testNamespace, + Finalizers: []string{ + korifiv1alpha1.CFOrgFinalizerName, + }, }, Spec: korifiv1alpha1.CFOrgSpec{ DisplayName: uuid.NewString(), @@ -44,16 +47,9 @@ var _ = Describe("CFOrgReconciler Integration Tests", func() { }).Should(Succeed()) }) - It("sets the finalizer on cfOrg", func() { - Eventually(func(g Gomega) { - g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(cfOrg), cfOrg)).To(Succeed()) - g.Expect(cfOrg.ObjectMeta.Finalizers).To(ConsistOf("cfOrg.korifi.cloudfoundry.org")) - }).Should(Succeed()) - }) - It("propagates the image-registry-credentials secrets from root-ns to org namespace", func() { Eventually(func(g Gomega) { - g.Expect(adminClient.Get(ctx, types.NamespacedName{Namespace: cfOrg.Name, Name: imageRegistrySecret.Name}, &corev1.Secret{})).To(Succeed()) + g.Expect(adminClient.Get(ctx, types.NamespacedName{Namespace: cfOrg.Name, Name: packageRegistrySecretName}, &corev1.Secret{})).To(Succeed()) }).Should(Succeed()) }) @@ -66,12 +62,4 @@ var _ = Describe("CFOrgReconciler Integration Tests", func() { g.Expect(meta.IsStatusConditionTrue(cfOrg.Status.Conditions, korifiv1alpha1.StatusConditionReady)).To(BeTrue()) }).Should(Succeed()) }) - - It("sets restricted pod security labels on the namespace", func() { - Eventually(func(g Gomega) { - var ns corev1.Namespace - g.Expect(adminClient.Get(ctx, types.NamespacedName{Name: cfOrg.Name}, &ns)).To(Succeed()) - g.Expect(ns.Labels).To(HaveKeyWithValue(api.EnforceLevelLabel, string(api.LevelRestricted))) - }).Should(Succeed()) - }) }) diff --git a/controllers/controllers/workloads/orgs/suite_test.go b/controllers/controllers/workloads/orgs/suite_test.go new file mode 100644 index 000000000..3e7ef97ac --- /dev/null +++ b/controllers/controllers/workloads/orgs/suite_test.go @@ -0,0 +1,110 @@ +package orgs_test + +import ( + "context" + "path/filepath" + "testing" + "time" + + korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" + "code.cloudfoundry.org/korifi/controllers/controllers/shared" + "code.cloudfoundry.org/korifi/controllers/controllers/workloads/labels" + "code.cloudfoundry.org/korifi/controllers/controllers/workloads/orgs" + "code.cloudfoundry.org/korifi/tests/helpers" + + "github.com/google/uuid" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "go.uber.org/zap/zapcore" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/scheme" + admission "k8s.io/pod-security-admission/api" + 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" +) + +var ( + ctx context.Context + stopManager context.CancelFunc + stopClientCache context.CancelFunc + testEnv *envtest.Environment + adminClient client.Client + testNamespace string +) + +const ( + packageRegistrySecretName = "test-package-registry-secret" +) + +func TestWorkloadsControllers(t *testing.T) { + SetDefaultEventuallyTimeout(10 * time.Second) + SetDefaultEventuallyPollingInterval(250 * time.Millisecond) + + RegisterFailHandler(Fail) + RunSpecs(t, "CFOrg Controller Integration Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true), zap.Level(zapcore.DebugLevel))) + + ctx = context.Background() + + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{ + filepath.Join("..", "..", "..", "..", "helm", "korifi", "controllers", "crds"), + }, + ErrorIfCRDPathMissing: true, + } + + _, err := testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + + Expect(korifiv1alpha1.AddToScheme(scheme.Scheme)).To(Succeed()) + Expect(corev1.AddToScheme(scheme.Scheme)).To(Succeed()) + + k8sManager := helpers.NewK8sManager(testEnv, filepath.Join("helm", "korifi", "controllers", "role.yaml")) + Expect(shared.SetupIndexWithManager(k8sManager)).To(Succeed()) + + adminClient, stopClientCache = helpers.NewCachedClient(testEnv.Config) + + labelCompiler := labels.NewCompiler().Defaults(map[string]string{ + admission.EnforceLevelLabel: string(admission.LevelRestricted), + admission.AuditLevelLabel: string(admission.LevelRestricted), + }) + + err = orgs.NewReconciler( + k8sManager.GetClient(), + ctrl.Log.WithName("controllers").WithName("CFOrg"), + []string{packageRegistrySecretName}, + labelCompiler, + ).SetupWithManager(k8sManager) + Expect(err).NotTo(HaveOccurred()) + + stopManager = helpers.StartK8sManager(k8sManager) +}) + +var _ = BeforeEach(func() { + testNamespace = uuid.NewString() + Expect(adminClient.Create(ctx, &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: testNamespace, + }, + })).To(Succeed()) + + Expect(adminClient.Create(ctx, &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: packageRegistrySecretName, + Namespace: testNamespace, + }, + })).To(Succeed()) +}) + +var _ = AfterSuite(func() { + stopManager() + stopClientCache() + Expect(testEnv.Stop()).To(Succeed()) +}) diff --git a/controllers/controllers/workloads/cfpackage_controller.go b/controllers/controllers/workloads/packages/controller.go similarity index 91% rename from controllers/controllers/workloads/cfpackage_controller.go rename to controllers/controllers/workloads/packages/controller.go index 94f7b87d5..ca349ab87 100644 --- a/controllers/controllers/workloads/cfpackage_controller.go +++ b/controllers/controllers/workloads/packages/controller.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package workloads +package packages import ( "context" @@ -50,8 +50,7 @@ type PackageCleaner interface { Clean(ctx context.Context, app types.NamespacedName) error } -// CFPackageReconciler reconciles a CFPackage object -type CFPackageReconciler struct { +type Reconciler struct { k8sClient client.Client scheme *runtime.Scheme imageDeleter ImageDeleter @@ -60,7 +59,7 @@ type CFPackageReconciler struct { log logr.Logger } -func NewCFPackageReconciler( +func NewReconciler( client client.Client, scheme *runtime.Scheme, log logr.Logger, @@ -68,7 +67,7 @@ func NewCFPackageReconciler( packageCleaner PackageCleaner, packageRepoSecretNames []string, ) *k8s.PatchingReconciler[korifiv1alpha1.CFPackage, *korifiv1alpha1.CFPackage] { - return k8s.NewPatchingReconciler[korifiv1alpha1.CFPackage, *korifiv1alpha1.CFPackage](log, client, &CFPackageReconciler{ + return k8s.NewPatchingReconciler[korifiv1alpha1.CFPackage, *korifiv1alpha1.CFPackage](log, client, &Reconciler{ k8sClient: client, scheme: scheme, log: log, @@ -78,7 +77,7 @@ func NewCFPackageReconciler( }) } -func (r *CFPackageReconciler) SetupWithManager(mgr ctrl.Manager) *builder.Builder { +func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) *builder.Builder { return ctrl.NewControllerManagedBy(mgr). For(&korifiv1alpha1.CFPackage{}) } @@ -87,7 +86,7 @@ func (r *CFPackageReconciler) SetupWithManager(mgr ctrl.Manager) *builder.Builde //+kubebuilder:rbac:groups=korifi.cloudfoundry.org,resources=cfpackages/status,verbs=get;update;patch //+kubebuilder:rbac:groups=korifi.cloudfoundry.org,resources=cfpackages/finalizers,verbs=get;update;patch -func (r *CFPackageReconciler) ReconcileResource(ctx context.Context, cfPackage *korifiv1alpha1.CFPackage) (ctrl.Result, error) { +func (r *Reconciler) ReconcileResource(ctx context.Context, cfPackage *korifiv1alpha1.CFPackage) (ctrl.Result, error) { log := logr.FromContextOrDiscard(ctx) var err error @@ -143,7 +142,7 @@ func (r *CFPackageReconciler) ReconcileResource(ctx context.Context, cfPackage * return ctrl.Result{}, nil } -func (r *CFPackageReconciler) finalize(ctx context.Context, cfPackage *korifiv1alpha1.CFPackage) (ctrl.Result, error) { +func (r *Reconciler) finalize(ctx context.Context, cfPackage *korifiv1alpha1.CFPackage) (ctrl.Result, error) { log := logr.FromContextOrDiscard(ctx).WithName("finalize") if !controllerutil.ContainsFinalizer(cfPackage, korifiv1alpha1.CFPackageFinalizerName) { diff --git a/controllers/controllers/workloads/cfpackage_controller_test.go b/controllers/controllers/workloads/packages/controller_test.go similarity index 59% rename from controllers/controllers/workloads/cfpackage_controller_test.go rename to controllers/controllers/workloads/packages/controller_test.go index 8ac82c983..136d88b36 100644 --- a/controllers/controllers/workloads/cfpackage_controller_test.go +++ b/controllers/controllers/workloads/packages/controller_test.go @@ -1,13 +1,12 @@ -package workloads_test +package packages_test import ( "context" "errors" korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" - "code.cloudfoundry.org/korifi/controllers/controllers/workloads" + "code.cloudfoundry.org/korifi/controllers/controllers/workloads/packages" "code.cloudfoundry.org/korifi/tools" - "code.cloudfoundry.org/korifi/tools/k8s" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" @@ -22,18 +21,15 @@ import ( var _ = Describe("CFPackageReconciler Integration Tests", func() { var ( - cfSpace *korifiv1alpha1.CFSpace cfApp *korifiv1alpha1.CFApp cfPackage *korifiv1alpha1.CFPackage ) BeforeEach(func() { - cfSpace = createSpace(testOrg) - cfApp = &korifiv1alpha1.CFApp{ ObjectMeta: metav1.ObjectMeta{ Name: uuid.NewString(), - Namespace: cfSpace.Status.GUID, + Namespace: testNamespace, }, Spec: korifiv1alpha1.CFAppSpec{ DisplayName: "test-app-name", @@ -44,6 +40,32 @@ var _ = Describe("CFPackageReconciler Integration Tests", func() { }, } Expect(adminClient.Create(context.Background(), cfApp)).To(Succeed()) + + cfPackage = &korifiv1alpha1.CFPackage{ + ObjectMeta: metav1.ObjectMeta{ + Name: uuid.NewString(), + Namespace: testNamespace, + Finalizers: []string{ + korifiv1alpha1.CFPackageFinalizerName, + }, + }, + Spec: korifiv1alpha1.CFPackageSpec{ + Type: "bits", + AppRef: corev1.LocalObjectReference{ + Name: cfApp.Name, + }, + Source: korifiv1alpha1.PackageSource{ + Registry: korifiv1alpha1.Registry{ + Image: "hello", + ImagePullSecrets: []corev1.LocalObjectReference{{Name: "source-registry-image-pull-secret"}}, + }, + }, + }, + } + }) + + JustBeforeEach(func() { + Expect(adminClient.Create(context.Background(), cfPackage)).To(Succeed()) }) When("a new CFPackage resource is created", func() { @@ -51,42 +73,32 @@ var _ = Describe("CFPackageReconciler Integration Tests", func() { BeforeEach(func() { cleanCallCount = packageCleaner.CleanCallCount() - - cfPackage = &korifiv1alpha1.CFPackage{ - ObjectMeta: metav1.ObjectMeta{ - Name: uuid.NewString(), - Namespace: cfSpace.Status.GUID, - }, - Spec: korifiv1alpha1.CFPackageSpec{ - Type: "bits", - AppRef: corev1.LocalObjectReference{ - Name: cfApp.Name, - }, - }, - } - Expect(adminClient.Create(context.Background(), cfPackage)).To(Succeed()) }) It("initializes it", func() { - var createdCFPackage korifiv1alpha1.CFPackage Eventually(func(g Gomega) { - g.Expect(adminClient.Get(context.Background(), client.ObjectKeyFromObject(cfPackage), &createdCFPackage)).To(Succeed()) - g.Expect(meta.IsStatusConditionTrue(createdCFPackage.Status.Conditions, workloads.InitializedConditionType)).To(BeTrue()) - }).Should(Succeed()) - - Expect(meta.FindStatusCondition(createdCFPackage.Status.Conditions, workloads.InitializedConditionType).ObservedGeneration).To(Equal(createdCFPackage.Generation)) + g.Expect(adminClient.Get(context.Background(), client.ObjectKeyFromObject(cfPackage), cfPackage)).To(Succeed()) - Expect(meta.IsStatusConditionFalse(createdCFPackage.Status.Conditions, korifiv1alpha1.StatusConditionReady)).To(BeTrue()) - Expect(meta.FindStatusCondition(createdCFPackage.Status.Conditions, korifiv1alpha1.StatusConditionReady).ObservedGeneration).To(Equal(createdCFPackage.Generation)) + initializedCondition := meta.FindStatusCondition(cfPackage.Status.Conditions, packages.InitializedConditionType) + g.Expect(initializedCondition).NotTo(BeNil()) + g.Expect(initializedCondition.ObservedGeneration).To(Equal(cfPackage.Generation)) + + g.Expect(cfPackage.GetOwnerReferences()).To(ConsistOf(metav1.OwnerReference{ + APIVersion: korifiv1alpha1.GroupVersion.Identifier(), + Kind: "CFApp", + Name: cfApp.Name, + UID: cfApp.UID, + Controller: tools.PtrTo(true), + BlockOwnerDeletion: tools.PtrTo(true), + })) + }).Should(Succeed()) + }) - Expect(createdCFPackage.GetOwnerReferences()).To(ConsistOf(metav1.OwnerReference{ - APIVersion: korifiv1alpha1.GroupVersion.Identifier(), - Kind: "CFApp", - Name: cfApp.Name, - UID: cfApp.UID, - Controller: tools.PtrTo(true), - BlockOwnerDeletion: tools.PtrTo(true), - })) + It("sets the Ready condition to true", func() { + Eventually(func(g Gomega) { + g.Expect(adminClient.Get(context.Background(), client.ObjectKeyFromObject(cfPackage), cfPackage)).To(Succeed()) + g.Expect(meta.IsStatusConditionTrue(cfPackage.Status.Conditions, korifiv1alpha1.StatusConditionReady)).To(BeTrue()) + }).Should(Succeed()) }) It("deletes the older packages for the same app", func() { @@ -95,7 +107,7 @@ var _ = Describe("CFPackageReconciler Integration Tests", func() { var cleanedApps []types.NamespacedName for currCall := cleanCallCount; currCall < packageCleaner.CleanCallCount(); currCall++ { - cleanedApps = append(cleanedApps, types.NamespacedName{Namespace: cfSpace.Status.GUID, Name: cfApp.Name}) + cleanedApps = append(cleanedApps, types.NamespacedName{Namespace: testNamespace, Name: cfApp.Name}) } g.Expect(cleanedApps).To(ContainElement(types.NamespacedName{Namespace: cfApp.Namespace, Name: cfApp.Name})) }).Should(Succeed()) @@ -108,61 +120,32 @@ var _ = Describe("CFPackageReconciler Integration Tests", func() { }).Should(Succeed()) }) - When("the package is updated with its source image", func() { + When("the package does not have source", func() { BeforeEach(func() { - Eventually(func(g Gomega) { - g.Expect(adminClient.Get(context.Background(), client.ObjectKeyFromObject(cfPackage), cfPackage)).To(Succeed()) - g.Expect(meta.IsStatusConditionTrue(cfPackage.Status.Conditions, workloads.InitializedConditionType)).To(BeTrue()) - }).Should(Succeed()) - }) - - JustBeforeEach(func() { - Expect(k8s.PatchResource(ctx, adminClient, cfPackage, func() { - cfPackage.Spec.Source.Registry.Image = "hello" - })).To(Succeed()) + cfPackage.Spec.Source = korifiv1alpha1.PackageSource{} }) - It("sets the ready condition to true", func() { + It("sets the ready condition to false", func() { Eventually(func(g Gomega) { g.Expect(adminClient.Get(context.Background(), client.ObjectKeyFromObject(cfPackage), cfPackage)).To(Succeed()) - g.Expect(meta.IsStatusConditionTrue(cfPackage.Status.Conditions, korifiv1alpha1.StatusConditionReady)).To(BeTrue()) + g.Expect(meta.IsStatusConditionTrue(cfPackage.Status.Conditions, packages.InitializedConditionType)).To(BeTrue()) + g.Expect(meta.IsStatusConditionFalse(cfPackage.Status.Conditions, korifiv1alpha1.StatusConditionReady)).To(BeTrue()) }).Should(Succeed()) }) }) }) - When("a CFPackage is deleted", func() { + Describe("finalization", func() { var deleteCount int BeforeEach(func() { - cfPackage = &korifiv1alpha1.CFPackage{ - ObjectMeta: metav1.ObjectMeta{ - Name: uuid.NewString(), - Namespace: cfSpace.Status.GUID, - }, - Spec: korifiv1alpha1.CFPackageSpec{ - Type: "bits", - AppRef: corev1.LocalObjectReference{ - Name: cfApp.Name, - }, - Source: korifiv1alpha1.PackageSource{ - Registry: korifiv1alpha1.Registry{ - Image: uuid.NewString(), - ImagePullSecrets: []corev1.LocalObjectReference{{Name: "source-registry-image-pull-secret"}}, - }, - }, - }, - } - deleteCount = imageDeleter.DeleteCallCount() }) JustBeforeEach(func() { - Expect(adminClient.Create(context.Background(), cfPackage)).To(Succeed()) - Eventually(func(g Gomega) { g.Expect(adminClient.Get(context.Background(), client.ObjectKeyFromObject(cfPackage), cfPackage)).To(Succeed()) - g.Expect(meta.IsStatusConditionTrue(cfPackage.Status.Conditions, workloads.InitializedConditionType)).To(BeTrue()) + g.Expect(meta.IsStatusConditionTrue(cfPackage.Status.Conditions, packages.InitializedConditionType)).To(BeTrue()) }).Should(Succeed()) Expect(adminClient.Delete(context.Background(), cfPackage)).To(Succeed()) @@ -173,9 +156,9 @@ var _ = Describe("CFPackageReconciler Integration Tests", func() { g.Expect(imageDeleter.DeleteCallCount()).To(BeNumerically(">", deleteCount)) _, creds, ref, tagsToDelete := imageDeleter.DeleteArgsForCall(deleteCount) - g.Expect(creds.Namespace).To(Equal(cfSpace.Status.GUID)) + g.Expect(creds.Namespace).To(Equal(testNamespace)) g.Expect(creds.SecretNames).To(ConsistOf("package-repo-secret-name")) - g.Expect(ref).To(Equal(cfPackage.Spec.Source.Registry.Image)) + g.Expect(ref).To(Equal("hello")) g.Expect(tagsToDelete).To(ConsistOf(cfPackage.Name)) }).Should(Succeed()) diff --git a/controllers/controllers/workloads/fake/image_deleter.go b/controllers/controllers/workloads/packages/fake/image_deleter.go similarity index 98% rename from controllers/controllers/workloads/fake/image_deleter.go rename to controllers/controllers/workloads/packages/fake/image_deleter.go index eb9c45b70..78ab553e7 100644 --- a/controllers/controllers/workloads/fake/image_deleter.go +++ b/controllers/controllers/workloads/packages/fake/image_deleter.go @@ -5,7 +5,7 @@ import ( "context" "sync" - "code.cloudfoundry.org/korifi/controllers/controllers/workloads" + "code.cloudfoundry.org/korifi/controllers/controllers/workloads/packages" "code.cloudfoundry.org/korifi/tools/image" ) @@ -116,4 +116,4 @@ func (fake *ImageDeleter) recordInvocation(key string, args []interface{}) { fake.invocations[key] = append(fake.invocations[key], args) } -var _ workloads.ImageDeleter = new(ImageDeleter) +var _ packages.ImageDeleter = new(ImageDeleter) diff --git a/controllers/controllers/workloads/fake/package_cleaner.go b/controllers/controllers/workloads/packages/fake/package_cleaner.go similarity index 97% rename from controllers/controllers/workloads/fake/package_cleaner.go rename to controllers/controllers/workloads/packages/fake/package_cleaner.go index c238704fe..58ff27ba5 100644 --- a/controllers/controllers/workloads/fake/package_cleaner.go +++ b/controllers/controllers/workloads/packages/fake/package_cleaner.go @@ -5,7 +5,7 @@ import ( "context" "sync" - "code.cloudfoundry.org/korifi/controllers/controllers/workloads" + "code.cloudfoundry.org/korifi/controllers/controllers/workloads/packages" "k8s.io/apimachinery/pkg/types" ) @@ -112,4 +112,4 @@ func (fake *PackageCleaner) recordInvocation(key string, args []interface{}) { fake.invocations[key] = append(fake.invocations[key], args) } -var _ workloads.PackageCleaner = new(PackageCleaner) +var _ packages.PackageCleaner = new(PackageCleaner) diff --git a/controllers/controllers/workloads/package.go b/controllers/controllers/workloads/packages/package.go similarity index 80% rename from controllers/controllers/workloads/package.go rename to controllers/controllers/workloads/packages/package.go index 4fb2d1006..c294d4ef9 100644 --- a/controllers/controllers/workloads/package.go +++ b/controllers/controllers/workloads/packages/package.go @@ -1,3 +1,3 @@ -package workloads +package packages //go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -generate diff --git a/controllers/controllers/workloads/packages/suite_test.go b/controllers/controllers/workloads/packages/suite_test.go new file mode 100644 index 000000000..2ca551be9 --- /dev/null +++ b/controllers/controllers/workloads/packages/suite_test.go @@ -0,0 +1,106 @@ +package packages_test + +import ( + "context" + "path/filepath" + "testing" + "time" + + korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" + "code.cloudfoundry.org/korifi/controllers/controllers/shared" + "code.cloudfoundry.org/korifi/controllers/controllers/workloads/packages" + "code.cloudfoundry.org/korifi/controllers/controllers/workloads/packages/fake" + "code.cloudfoundry.org/korifi/tests/helpers" + "code.cloudfoundry.org/korifi/tools/image" + + "github.com/google/uuid" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "go.uber.org/zap/zapcore" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + k8sclient "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/scheme" + 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" +) + +var ( + ctx context.Context + stopManager context.CancelFunc + stopClientCache context.CancelFunc + testEnv *envtest.Environment + adminClient client.Client + testNamespace string + imageDeleter *fake.ImageDeleter + packageCleaner *fake.PackageCleaner + imageClient image.Client +) + +func TestWorkloadsControllers(t *testing.T) { + SetDefaultEventuallyTimeout(10 * time.Second) + SetDefaultEventuallyPollingInterval(250 * time.Millisecond) + + RegisterFailHandler(Fail) + RunSpecs(t, "CFPackage Controller Integration Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true), zap.Level(zapcore.DebugLevel))) + + ctx = context.Background() + + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{ + filepath.Join("..", "..", "..", "..", "helm", "korifi", "controllers", "crds"), + }, + ErrorIfCRDPathMissing: true, + } + + _, err := testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + + Expect(korifiv1alpha1.AddToScheme(scheme.Scheme)).To(Succeed()) + Expect(corev1.AddToScheme(scheme.Scheme)).To(Succeed()) + + k8sManager := helpers.NewK8sManager(testEnv, filepath.Join("helm", "korifi", "controllers", "role.yaml")) + Expect(shared.SetupIndexWithManager(k8sManager)).To(Succeed()) + + adminClient, stopClientCache = helpers.NewCachedClient(testEnv.Config) + + k8sClient, err := k8sclient.NewForConfig(k8sManager.GetConfig()) + Expect(err).NotTo(HaveOccurred()) + imageClient = image.NewClient(k8sClient) + + imageDeleter = new(fake.ImageDeleter) + packageCleaner = new(fake.PackageCleaner) + err = packages.NewReconciler( + k8sManager.GetClient(), + k8sManager.GetScheme(), + ctrl.Log.WithName("controllers").WithName("CFPackage"), + imageDeleter, + packageCleaner, + []string{"package-repo-secret-name"}, + ).SetupWithManager(k8sManager) + Expect(err).NotTo(HaveOccurred()) + + stopManager = helpers.StartK8sManager(k8sManager) +}) + +var _ = BeforeEach(func() { + testNamespace = uuid.NewString() + Expect(adminClient.Create(ctx, &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: testNamespace, + }, + })).To(Succeed()) +}) + +var _ = AfterSuite(func() { + stopManager() + stopClientCache() + Expect(testEnv.Stop()).To(Succeed()) +}) diff --git a/controllers/controllers/workloads/cfprocess_controller.go b/controllers/controllers/workloads/processes/controller.go similarity index 90% rename from controllers/controllers/workloads/cfprocess_controller.go rename to controllers/controllers/workloads/processes/controller.go index 652a46a38..0e99bbe76 100644 --- a/controllers/controllers/workloads/cfprocess_controller.go +++ b/controllers/controllers/workloads/processes/controller.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package workloads +package processes import ( "context" @@ -48,8 +48,7 @@ type ProcessEnvBuilder interface { Build(context.Context, *korifiv1alpha1.CFApp, *korifiv1alpha1.CFProcess) ([]corev1.EnvVar, error) } -// CFProcessReconciler reconciles a CFProcess object -type CFProcessReconciler struct { +type Reconciler struct { k8sClient client.Client scheme *runtime.Scheme log logr.Logger @@ -57,18 +56,18 @@ type CFProcessReconciler struct { envBuilder ProcessEnvBuilder } -func NewCFProcessReconciler( +func NewReconciler( client client.Client, scheme *runtime.Scheme, log logr.Logger, controllerConfig *config.ControllerConfig, envBuilder ProcessEnvBuilder, ) *k8s.PatchingReconciler[korifiv1alpha1.CFProcess, *korifiv1alpha1.CFProcess] { - processReconciler := CFProcessReconciler{k8sClient: client, scheme: scheme, log: log, controllerConfig: controllerConfig, envBuilder: envBuilder} + processReconciler := Reconciler{k8sClient: client, scheme: scheme, log: log, controllerConfig: controllerConfig, envBuilder: envBuilder} return k8s.NewPatchingReconciler[korifiv1alpha1.CFProcess, *korifiv1alpha1.CFProcess](log, client, &processReconciler) } -func (r *CFProcessReconciler) SetupWithManager(mgr ctrl.Manager) *builder.Builder { +func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) *builder.Builder { return ctrl.NewControllerManagedBy(mgr). For(&korifiv1alpha1.CFProcess{}). Owns(&korifiv1alpha1.AppWorkload{}). @@ -82,11 +81,11 @@ func (r *CFProcessReconciler) SetupWithManager(mgr ctrl.Manager) *builder.Builde ) } -func (r *CFProcessReconciler) enqueueCFProcessRequestsForApp(ctx context.Context, o client.Object) []reconcile.Request { +func (r *Reconciler) enqueueCFProcessRequestsForApp(ctx context.Context, o client.Object) []reconcile.Request { return r.cfProcessRequestsForAppGUID(ctx, o.GetNamespace(), o.GetName()) } -func (r *CFProcessReconciler) cfProcessRequestsForAppGUID(ctx context.Context, cfAppNamespace, cfAppGUID string) []reconcile.Request { +func (r *Reconciler) cfProcessRequestsForAppGUID(ctx context.Context, cfAppNamespace, cfAppGUID string) []reconcile.Request { processList := &korifiv1alpha1.CFProcessList{} err := r.k8sClient.List(ctx, processList, client.InNamespace(cfAppNamespace), client.MatchingLabels{korifiv1alpha1.CFAppGUIDLabelKey: cfAppGUID}) if err != nil { @@ -102,7 +101,7 @@ func (r *CFProcessReconciler) cfProcessRequestsForAppGUID(ctx context.Context, c return requests } -func (r *CFProcessReconciler) enqueueCFProcessRequestsForRoute(ctx context.Context, o client.Object) []reconcile.Request { +func (r *Reconciler) enqueueCFProcessRequestsForRoute(ctx context.Context, o client.Object) []reconcile.Request { cfRoute, ok := o.(*korifiv1alpha1.CFRoute) if !ok { r.log.Error(errors.New("listing CFProcesses for route failed"), "expected", "CFRoute", "got", o) @@ -125,7 +124,7 @@ func (r *CFProcessReconciler) enqueueCFProcessRequestsForRoute(ctx context.Conte //+kubebuilder:rbac:groups=korifi.cloudfoundry.org,resources=appworkloads/finalizers,verbs=update //+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;patch -func (r *CFProcessReconciler) ReconcileResource(ctx context.Context, cfProcess *korifiv1alpha1.CFProcess) (ctrl.Result, error) { +func (r *Reconciler) ReconcileResource(ctx context.Context, cfProcess *korifiv1alpha1.CFProcess) (ctrl.Result, error) { log := logr.FromContextOrDiscard(ctx) var err error @@ -201,7 +200,7 @@ func needsAppWorkload(cfApp *korifiv1alpha1.CFApp, cfProcess *korifiv1alpha1.CFP return cfProcess.Spec.DesiredInstances != nil && *cfProcess.Spec.DesiredInstances > 0 } -func (r *CFProcessReconciler) createOrPatchAppWorkload(ctx context.Context, cfApp *korifiv1alpha1.CFApp, cfProcess *korifiv1alpha1.CFProcess, cfAppRev, cfLastStopAppRev string) error { +func (r *Reconciler) createOrPatchAppWorkload(ctx context.Context, cfApp *korifiv1alpha1.CFApp, cfProcess *korifiv1alpha1.CFProcess, cfAppRev, cfLastStopAppRev string) error { log := logr.FromContextOrDiscard(ctx).WithName("createOrPatchAppWorkload") cfBuild := new(korifiv1alpha1.CFBuild) @@ -255,7 +254,7 @@ func (r *CFProcessReconciler) createOrPatchAppWorkload(ctx context.Context, cfAp return nil } -func (r *CFProcessReconciler) cleanUpAppWorkloads(ctx context.Context, cfProcess *korifiv1alpha1.CFProcess, desiredState korifiv1alpha1.AppState, cfLastStopAppRev string) error { +func (r *Reconciler) cleanUpAppWorkloads(ctx context.Context, cfProcess *korifiv1alpha1.CFProcess, desiredState korifiv1alpha1.AppState, cfLastStopAppRev string) error { log := logr.FromContextOrDiscard(ctx).WithName("cleanUpAppWorkloads") appWorkloadsForProcess, err := r.fetchAppWorkloadsForProcess(ctx, cfProcess) @@ -297,7 +296,7 @@ func appWorkloadMutateFunction(actualAppWorkload, desiredAppWorkload *korifiv1al } } -func (r *CFProcessReconciler) generateAppWorkload(actualAppWorkload *korifiv1alpha1.AppWorkload, cfApp *korifiv1alpha1.CFApp, cfProcess *korifiv1alpha1.CFProcess, cfBuild *korifiv1alpha1.CFBuild, appPorts []int32, envVars []corev1.EnvVar, cfAppRev, cfLastStopAppRev string) (*korifiv1alpha1.AppWorkload, error) { +func (r *Reconciler) generateAppWorkload(actualAppWorkload *korifiv1alpha1.AppWorkload, cfApp *korifiv1alpha1.CFApp, cfProcess *korifiv1alpha1.CFProcess, cfBuild *korifiv1alpha1.CFBuild, appPorts []int32, envVars []corev1.EnvVar, cfAppRev, cfLastStopAppRev string) (*korifiv1alpha1.AppWorkload, error) { var desiredAppWorkload korifiv1alpha1.AppWorkload actualAppWorkload.DeepCopyInto(&desiredAppWorkload) @@ -366,7 +365,7 @@ func generateAppWorkloadName(cfAppRev string, processGUID string) string { return appWorkloadName } -func (r *CFProcessReconciler) fetchAppWorkloadsForProcess(ctx context.Context, cfProcess *korifiv1alpha1.CFProcess) ([]korifiv1alpha1.AppWorkload, error) { +func (r *Reconciler) fetchAppWorkloadsForProcess(ctx context.Context, cfProcess *korifiv1alpha1.CFProcess) ([]korifiv1alpha1.AppWorkload, error) { allAppWorkloads := &korifiv1alpha1.AppWorkloadList{} err := r.k8sClient.List(ctx, allAppWorkloads, client.InNamespace(cfProcess.Namespace)) if err != nil { diff --git a/controllers/controllers/workloads/cfprocess_controller_test.go b/controllers/controllers/workloads/processes/controller_test.go similarity index 54% rename from controllers/controllers/workloads/cfprocess_controller_test.go rename to controllers/controllers/workloads/processes/controller_test.go index dd4bcc2a6..422a134e1 100644 --- a/controllers/controllers/workloads/cfprocess_controller_test.go +++ b/controllers/controllers/workloads/processes/controller_test.go @@ -1,10 +1,9 @@ -package workloads_test +package processes_test import ( "context" korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" - . "code.cloudfoundry.org/korifi/controllers/controllers/workloads/testutils" "code.cloudfoundry.org/korifi/tests/matchers" "code.cloudfoundry.org/korifi/tools" "code.cloudfoundry.org/korifi/tools/k8s" @@ -20,7 +19,6 @@ import ( var _ = Describe("CFProcessReconciler Integration Tests", func() { var ( - cfSpace *korifiv1alpha1.CFSpace cfProcess *korifiv1alpha1.CFProcess cfApp *korifiv1alpha1.CFApp cfBuild *korifiv1alpha1.CFBuild @@ -28,24 +26,28 @@ var _ = Describe("CFProcessReconciler Integration Tests", func() { ) BeforeEach(func() { - cfSpace = createSpace(testOrg) - - appGUID := uuid.NewString() - buildGUID := uuid.NewString() - packageGUID := uuid.NewString() - - // Technically the app controller should be creating this process based - // on CFApp and CFBuild, but we want to drive testing with a specific - // CFProcess instead of cascading (non-object-ref) state through other - // resources. The app controller is only creating processes if they do - // not exist, so ours is going to be "adopted" - cfProcess = BuildCFProcessCRObject(GenerateGUID(), cfSpace.Status.GUID, appGUID, korifiv1alpha1.ProcessTypeWeb, "process command", "detected-command") - Expect(adminClient.Create(ctx, cfProcess)).To(Succeed()) + appEnvSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testNamespace, + Name: uuid.NewString(), + }, + StringData: map[string]string{ + "env-key": "env-val", + }, + } + Expect(adminClient.Create(ctx, appEnvSecret)).To(Succeed()) cfApp = &korifiv1alpha1.CFApp{ ObjectMeta: metav1.ObjectMeta{ - Name: appGUID, - Namespace: cfSpace.Status.GUID, + Name: uuid.NewString(), + Namespace: testNamespace, + Annotations: map[string]string{ + korifiv1alpha1.CFAppRevisionKey: "5", + korifiv1alpha1.CFAppLastStopRevisionKey: "2", + }, + Finalizers: []string{ + korifiv1alpha1.CFAppFinalizerName, + }, }, Spec: korifiv1alpha1.CFAppSpec{ DisplayName: "test-app-name", @@ -53,76 +55,109 @@ var _ = Describe("CFProcessReconciler Integration Tests", func() { Lifecycle: korifiv1alpha1.Lifecycle{ Type: "buildpack", }, - EnvSecretName: appGUID + "-env", - CurrentDropletRef: corev1.LocalObjectReference{Name: buildGUID}, + EnvSecretName: appEnvSecret.Name, }, } Expect(adminClient.Create(ctx, cfApp)).To(Succeed()) - cfPackage := &korifiv1alpha1.CFPackage{ + vcapApplicationSecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: packageGUID, - Namespace: cfSpace.Status.GUID, + Namespace: testNamespace, + Name: uuid.NewString(), }, - Spec: korifiv1alpha1.CFPackageSpec{ - Type: "bits", - AppRef: corev1.LocalObjectReference{ - Name: appGUID, - }, + StringData: map[string]string{ + "VCAP_APPLICATION": "{}", }, } - Expect(adminClient.Create(ctx, cfPackage)).To(Succeed()) + Expect(adminClient.Create(ctx, vcapApplicationSecret)).To(Succeed()) - cfBuild = BuildCFBuildObject(buildGUID, cfSpace.Status.GUID, packageGUID, cfApp.Name) - - appEnvSecret := BuildCFAppEnvVarsSecret(cfApp.Name, cfSpace.Status.GUID, map[string]string{ - "env-key": "env-val", - }) - Expect(adminClient.Create(ctx, appEnvSecret)).To(Succeed()) - - buildDropletStatus := BuildCFBuildDropletStatusObject(map[string]string{"web": "command-from-droplet"}) - cfBuild = createBuildWithDroplet(ctx, adminClient, cfBuild, buildDropletStatus) - - cfDomain := &korifiv1alpha1.CFDomain{ + vcapServicesSecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Namespace: cfSpace.Status.GUID, - Name: GenerateGUID(), + Namespace: testNamespace, + Name: uuid.NewString(), }, - Spec: korifiv1alpha1.CFDomainSpec{ - Name: "a" + uuid.NewString() + ".com", + StringData: map[string]string{ + "VCAP_SERVICES": "{}", }, } - Expect(adminClient.Create(ctx, cfDomain)).To(Succeed()) - - destination := korifiv1alpha1.Destination{ - GUID: GenerateGUID(), - AppRef: corev1.LocalObjectReference{Name: cfApp.Name}, - ProcessType: korifiv1alpha1.ProcessTypeWeb, - Port: tools.PtrTo(8080), - Protocol: tools.PtrTo("http1"), - } + Expect(adminClient.Create(ctx, vcapServicesSecret)).To(Succeed()) + + Expect(k8s.Patch(ctx, adminClient, cfApp, func() { + cfApp.Status.VCAPApplicationSecretName = vcapApplicationSecret.Name + cfApp.Status.VCAPServicesSecretName = vcapServicesSecret.Name + })).To(Succeed()) + cfRoute = &korifiv1alpha1.CFRoute{ ObjectMeta: metav1.ObjectMeta{ - Name: cfDomain.Name, - Namespace: cfSpace.Status.GUID, + Name: uuid.NewString(), + Namespace: testNamespace, }, Spec: korifiv1alpha1.CFRouteSpec{ - Host: PrefixedGUID("test-host"), - Path: "", - Protocol: "http", - DomainRef: corev1.ObjectReference{ - Name: cfDomain.Name, - Namespace: cfSpace.Status.GUID, - }, - Destinations: []korifiv1alpha1.Destination{destination}, + Destinations: []korifiv1alpha1.Destination{{ + GUID: uuid.NewString(), + AppRef: corev1.LocalObjectReference{Name: cfApp.Name}, + ProcessType: korifiv1alpha1.ProcessTypeWeb, + Port: tools.PtrTo(8080), + Protocol: tools.PtrTo("http1"), + }}, }, } Expect(adminClient.Create(ctx, cfRoute)).To(Succeed()) Expect(k8s.Patch(ctx, adminClient, cfRoute, func() { cfRoute.Status = korifiv1alpha1.CFRouteStatus{ - Destinations: []korifiv1alpha1.Destination{destination}, + Destinations: cfRoute.Spec.Destinations, + } + })).To(Succeed()) + + cfBuild = &korifiv1alpha1.CFBuild{ + ObjectMeta: metav1.ObjectMeta{ + Name: uuid.NewString(), + Namespace: testNamespace, + }, + Spec: korifiv1alpha1.CFBuildSpec{ + Lifecycle: korifiv1alpha1.Lifecycle{ + Type: "buildpack", + }, + }, + } + Expect(adminClient.Create(ctx, cfBuild)).To(Succeed()) + Expect(k8s.Patch(ctx, adminClient, cfBuild, func() { + cfBuild.Status.Droplet = &korifiv1alpha1.BuildDropletStatus{ + Registry: korifiv1alpha1.Registry{ + Image: "image/registry/url", + ImagePullSecrets: []corev1.LocalObjectReference{{Name: "some-image-pull-secret"}}, + }, } })).To(Succeed()) + Expect(k8s.Patch(ctx, adminClient, cfApp, func() { + cfApp.Spec.CurrentDropletRef.Name = cfBuild.Name + })).To(Succeed()) + + cfProcessGUID := uuid.NewString() + cfProcess = &korifiv1alpha1.CFProcess{ + ObjectMeta: metav1.ObjectMeta{ + Name: cfProcessGUID, + Namespace: testNamespace, + Labels: map[string]string{ + korifiv1alpha1.CFAppGUIDLabelKey: cfApp.Name, + korifiv1alpha1.CFProcessGUIDLabelKey: cfProcessGUID, + korifiv1alpha1.CFProcessTypeLabelKey: korifiv1alpha1.ProcessTypeWeb, + }, + }, + Spec: korifiv1alpha1.CFProcessSpec{ + AppRef: corev1.LocalObjectReference{Name: cfApp.Name}, + ProcessType: korifiv1alpha1.ProcessTypeWeb, + Command: "process command", + DetectedCommand: "detected-command", + DesiredInstances: tools.PtrTo(1), + MemoryMB: 1024, + DiskQuotaMB: 100, + }, + } + }) + + JustBeforeEach(func() { + Expect(adminClient.Create(ctx, cfProcess)).To(Succeed()) }) It("sets owner references on CFProcess", func() { @@ -154,7 +189,7 @@ var _ = Describe("CFProcessReconciler Integration Tests", func() { }) It("reconciles the CFProcess into an AppWorkload", func() { - eventuallyCreatedAppWorkloadShould(cfProcess.Name, cfSpace.Status.GUID, func(g Gomega, appWorkload korifiv1alpha1.AppWorkload) { + eventuallyCreatedAppWorkloadShould(func(g Gomega, appWorkload korifiv1alpha1.AppWorkload) { g.Expect(appWorkload.OwnerReferences).To(ConsistOf(metav1.OwnerReference{ APIVersion: korifiv1alpha1.GroupVersion.Identifier(), Kind: "CFProcess", @@ -201,12 +236,89 @@ var _ = Describe("CFProcessReconciler Integration Tests", func() { MatchFields(IgnoreExtras, Fields{"Name": Equal("env-key")}), MatchFields(IgnoreExtras, Fields{"Name": Equal("CF_INSTANCE_PORTS")}), )) + + g.Expect(appWorkload.Spec.RunnerName).To(Equal("cf-process-controller-test")) }) }) - When("the app workload instances is set", func() { + When("the CFProcess has an http health check", func() { + BeforeEach(func() { + cfProcess.Spec.HealthCheck = korifiv1alpha1.HealthCheck{ + Type: "http", + Data: korifiv1alpha1.HealthCheckData{ + HTTPEndpoint: "/healthy", + InvocationTimeoutSeconds: 3, + TimeoutSeconds: 9, + }, + } + }) + + It("sets the startup and liveness probes on the AppWorkload", func() { + eventuallyCreatedAppWorkloadShould(func(g Gomega, appWorkload korifiv1alpha1.AppWorkload) { + g.Expect(appWorkload.Spec.StartupProbe).ToNot(BeNil()) + g.Expect(appWorkload.Spec.StartupProbe.HTTPGet).ToNot(BeNil()) + g.Expect(appWorkload.Spec.StartupProbe.HTTPGet.Path).To(Equal("/healthy")) + g.Expect(appWorkload.Spec.StartupProbe.HTTPGet.Port.IntValue()).To(Equal(8080)) + g.Expect(appWorkload.Spec.StartupProbe.InitialDelaySeconds).To(BeZero()) + g.Expect(appWorkload.Spec.StartupProbe.PeriodSeconds).To(BeEquivalentTo(2)) + g.Expect(appWorkload.Spec.StartupProbe.TimeoutSeconds).To(BeEquivalentTo(3)) + g.Expect(appWorkload.Spec.StartupProbe.FailureThreshold).To(BeEquivalentTo(5)) + + g.Expect(appWorkload.Spec.LivenessProbe).ToNot(BeNil()) + g.Expect(appWorkload.Spec.LivenessProbe.InitialDelaySeconds).To(BeZero()) + g.Expect(appWorkload.Spec.LivenessProbe.PeriodSeconds).To(BeEquivalentTo(30)) + g.Expect(appWorkload.Spec.LivenessProbe.TimeoutSeconds).To(BeEquivalentTo(3)) + g.Expect(appWorkload.Spec.LivenessProbe.FailureThreshold).To(BeEquivalentTo(1)) + }) + }) + }) + + When("the CFProcess has a port health check", func() { + BeforeEach(func() { + cfProcess.Spec.HealthCheck = korifiv1alpha1.HealthCheck{ + Type: "port", + Data: korifiv1alpha1.HealthCheckData{ + InvocationTimeoutSeconds: 3, + TimeoutSeconds: 9, + }, + } + }) + + It("sets the startup and liveness probes on the AppWorkload", func() { + eventuallyCreatedAppWorkloadShould(func(g Gomega, appWorkload korifiv1alpha1.AppWorkload) { + g.Expect(appWorkload.Spec.StartupProbe).ToNot(BeNil()) + g.Expect(appWorkload.Spec.StartupProbe.TCPSocket).ToNot(BeNil()) + g.Expect(appWorkload.Spec.StartupProbe.TCPSocket.Port.IntValue()).To(Equal(8080)) + g.Expect(appWorkload.Spec.StartupProbe.InitialDelaySeconds).To(BeZero()) + g.Expect(appWorkload.Spec.StartupProbe.PeriodSeconds).To(BeEquivalentTo(2)) + g.Expect(appWorkload.Spec.StartupProbe.TimeoutSeconds).To(BeEquivalentTo(3)) + g.Expect(appWorkload.Spec.StartupProbe.FailureThreshold).To(BeEquivalentTo(5)) + + g.Expect(appWorkload.Spec.LivenessProbe).ToNot(BeNil()) + g.Expect(appWorkload.Spec.LivenessProbe.InitialDelaySeconds).To(BeZero()) + g.Expect(appWorkload.Spec.LivenessProbe.PeriodSeconds).To(BeEquivalentTo(30)) + g.Expect(appWorkload.Spec.LivenessProbe.TimeoutSeconds).To(BeEquivalentTo(3)) + g.Expect(appWorkload.Spec.LivenessProbe.FailureThreshold).To(BeEquivalentTo(1)) + }) + }) + }) + + When("the CFProcess has a process health check", func() { BeforeEach(func() { - eventuallyCreatedAppWorkloadShould(cfProcess.Name, cfSpace.Status.GUID, func(g Gomega, appWorkload korifiv1alpha1.AppWorkload) { + cfProcess.Spec.HealthCheck = korifiv1alpha1.HealthCheck{Type: "process"} + }) + + It("does not set liveness and startup probes on the AppWorkload", func() { + eventuallyCreatedAppWorkloadShould(func(g Gomega, appWorkload korifiv1alpha1.AppWorkload) { + g.Expect(appWorkload.Spec.StartupProbe).To(BeNil()) + g.Expect(appWorkload.Spec.LivenessProbe).To(BeNil()) + }) + }) + }) + + When("the app workload instances is set", func() { + JustBeforeEach(func() { + eventuallyCreatedAppWorkloadShould(func(g Gomega, appWorkload korifiv1alpha1.AppWorkload) { g.Expect(k8s.Patch(ctx, adminClient, &appWorkload, func() { appWorkload.Status.ActualInstances = 3 })).To(Succeed()) @@ -223,14 +335,12 @@ var _ = Describe("CFProcessReconciler Integration Tests", func() { When("The process command field isn't set", func() { BeforeEach(func() { - Expect(k8s.PatchResource(ctx, adminClient, cfProcess, func() { - cfProcess.Spec.Command = "" - })).To(Succeed()) + cfProcess.Spec.Command = "" }) - It("eventually creates an app workload using the command from the build", func() { - eventuallyCreatedAppWorkloadShould(cfProcess.Name, cfSpace.Status.GUID, func(g Gomega, appWorkload korifiv1alpha1.AppWorkload) { - g.Expect(appWorkload.Spec.Command).To(ConsistOf("/cnb/lifecycle/launcher", "command-from-droplet")) + It("creates an app workload using the detected command", func() { + eventuallyCreatedAppWorkloadShould(func(g Gomega, appWorkload korifiv1alpha1.AppWorkload) { + g.Expect(appWorkload.Spec.Command).To(ConsistOf("/cnb/lifecycle/launcher", "detected-command")) }) }) }) @@ -243,14 +353,14 @@ var _ = Describe("CFProcessReconciler Integration Tests", func() { }) It("does not create startup and liveness probes on the app workload", func() { - eventuallyCreatedAppWorkloadShould(cfProcess.Name, cfSpace.Status.GUID, func(g Gomega, appWorkload korifiv1alpha1.AppWorkload) { + eventuallyCreatedAppWorkloadShould(func(g Gomega, appWorkload korifiv1alpha1.AppWorkload) { g.Expect(appWorkload.Spec.StartupProbe).To(BeNil()) g.Expect(appWorkload.Spec.LivenessProbe).To(BeNil()) }) }) It("does not set port related environment variables on the app workload", func() { - eventuallyCreatedAppWorkloadShould(cfProcess.Name, cfSpace.Status.GUID, func(g Gomega, appWorkload korifiv1alpha1.AppWorkload) { + eventuallyCreatedAppWorkloadShould(func(g Gomega, appWorkload korifiv1alpha1.AppWorkload) { g.Expect(appWorkload.Spec.Env).NotTo(ContainElements( MatchFields(IgnoreExtras, Fields{"Name": Equal("VCAP_APP_PORT")}), MatchFields(IgnoreExtras, Fields{"Name": Equal("PORT")}), @@ -260,8 +370,8 @@ var _ = Describe("CFProcessReconciler Integration Tests", func() { }) When("a CFApp desired state is updated to STOPPED", func() { - BeforeEach(func() { - eventuallyCreatedAppWorkloadShould(cfProcess.Name, cfSpace.Status.GUID, func(g Gomega, appWorkload korifiv1alpha1.AppWorkload) {}) + JustBeforeEach(func() { + eventuallyCreatedAppWorkloadShould(func(g Gomega, appWorkload korifiv1alpha1.AppWorkload) {}) Expect(k8s.Patch(ctx, adminClient, cfApp, func() { cfApp.Spec.DesiredState = korifiv1alpha1.StoppedState })).To(Succeed()) @@ -270,17 +380,15 @@ var _ = Describe("CFProcessReconciler Integration Tests", func() { It("deletes the AppWorkloads", func() { Eventually(func(g Gomega) { var appWorkloads korifiv1alpha1.AppWorkloadList - g.Expect(adminClient.List(ctx, &appWorkloads, client.InNamespace(cfSpace.Status.GUID), client.MatchingLabels{ - korifiv1alpha1.CFProcessGUIDLabelKey: cfProcess.Name, - })).To(Succeed()) + g.Expect(adminClient.List(ctx, &appWorkloads, client.InNamespace(testNamespace))).To(Succeed()) g.Expect(appWorkloads.Items).To(BeEmpty()) }).Should(Succeed()) }) }) When("the app process instances are scaled down to 0", func() { - BeforeEach(func() { - eventuallyCreatedAppWorkloadShould(cfProcess.Name, cfSpace.Status.GUID, func(g Gomega, appWorkload korifiv1alpha1.AppWorkload) {}) + JustBeforeEach(func() { + eventuallyCreatedAppWorkloadShould(func(g Gomega, appWorkload korifiv1alpha1.AppWorkload) {}) Expect(k8s.Patch(ctx, adminClient, cfProcess, func() { cfProcess.Spec.DesiredInstances = tools.PtrTo(0) })).To(Succeed()) @@ -289,17 +397,15 @@ var _ = Describe("CFProcessReconciler Integration Tests", func() { It("deletes the app workload", func() { Eventually(func(g Gomega) { var appWorkloads korifiv1alpha1.AppWorkloadList - g.Expect(adminClient.List(context.Background(), &appWorkloads, client.InNamespace(cfProcess.Namespace), client.MatchingLabels{ - korifiv1alpha1.CFProcessGUIDLabelKey: cfProcess.Name, - })).To(Succeed()) + g.Expect(adminClient.List(ctx, &appWorkloads, client.InNamespace(testNamespace))).To(Succeed()) g.Expect(appWorkloads.Items).To(BeEmpty()) }).Should(Succeed()) }) }) - When("the app process instances are unset", func() { - BeforeEach(func() { - eventuallyCreatedAppWorkloadShould(cfProcess.Name, cfSpace.Status.GUID, func(g Gomega, appWorkload korifiv1alpha1.AppWorkload) {}) + When("the process desired instances are unset", func() { + JustBeforeEach(func() { + eventuallyCreatedAppWorkloadShould(func(g Gomega, appWorkload korifiv1alpha1.AppWorkload) {}) Expect(k8s.Patch(ctx, adminClient, cfProcess, func() { cfProcess.Spec.DesiredInstances = nil })).To(Succeed()) @@ -308,9 +414,7 @@ var _ = Describe("CFProcessReconciler Integration Tests", func() { It("does not delete the app workload", func() { Consistently(func(g Gomega) { var appWorkloads korifiv1alpha1.AppWorkloadList - g.Expect(adminClient.List(context.Background(), &appWorkloads, client.InNamespace(cfProcess.Namespace), client.MatchingLabels{ - korifiv1alpha1.CFProcessGUIDLabelKey: cfProcess.Name, - })).To(Succeed()) + g.Expect(adminClient.List(ctx, &appWorkloads, client.InNamespace(testNamespace))).To(Succeed()) g.Expect(appWorkloads.Items).NotTo(BeEmpty()) }).Should(Succeed()) }) @@ -319,8 +423,8 @@ var _ = Describe("CFProcessReconciler Integration Tests", func() { When("both the app-rev and the last-stop-app-rev are bumped", func() { var prevAppWorkloadName string - BeforeEach(func() { - eventuallyCreatedAppWorkloadShould(cfProcess.Name, cfSpace.Status.GUID, func(g Gomega, appWorkload korifiv1alpha1.AppWorkload) { + JustBeforeEach(func() { + eventuallyCreatedAppWorkloadShould(func(g Gomega, appWorkload korifiv1alpha1.AppWorkload) { prevAppWorkloadName = appWorkload.Name }) @@ -331,7 +435,7 @@ var _ = Describe("CFProcessReconciler Integration Tests", func() { }) It("deletes the app workload and createas a new one", func() { - eventuallyCreatedAppWorkloadShould(cfProcess.Name, cfSpace.Status.GUID, func(g Gomega, appWorkload korifiv1alpha1.AppWorkload) { + eventuallyCreatedAppWorkloadShould(func(g Gomega, appWorkload korifiv1alpha1.AppWorkload) { g.Expect(appWorkload.Name).NotTo(Equal(prevAppWorkloadName)) }) }) @@ -340,8 +444,8 @@ var _ = Describe("CFProcessReconciler Integration Tests", func() { When("the app-rev is bumped and the last-stop-app-rev is not", func() { var appWorkload *korifiv1alpha1.AppWorkload - BeforeEach(func() { - eventuallyCreatedAppWorkloadShould(cfProcess.Name, cfSpace.Status.GUID, func(g Gomega, workload korifiv1alpha1.AppWorkload) { + JustBeforeEach(func() { + eventuallyCreatedAppWorkloadShould(func(g Gomega, workload korifiv1alpha1.AppWorkload) { appWorkload = &workload }) @@ -357,110 +461,14 @@ var _ = Describe("CFProcessReconciler Integration Tests", func() { }) }) }) - - When("the CFProcess has an http health check", func() { - BeforeEach(func() { - Expect(k8s.PatchResource(ctx, adminClient, cfApp, func() { - cfApp.Spec.DesiredState = korifiv1alpha1.StartedState - })).To(Succeed()) - - Expect(k8s.Patch(ctx, adminClient, cfProcess, func() { - cfProcess.Spec.HealthCheck = korifiv1alpha1.HealthCheck{ - Type: "http", - Data: korifiv1alpha1.HealthCheckData{ - HTTPEndpoint: "/healthy", - InvocationTimeoutSeconds: 3, - TimeoutSeconds: 9, - }, - } - })).To(Succeed()) - }) - - It("sets the startup and liveness probes correctly on the AppWorkload", func() { - eventuallyCreatedAppWorkloadShould(cfProcess.Name, cfSpace.Status.GUID, func(g Gomega, appWorkload korifiv1alpha1.AppWorkload) { - g.Expect(appWorkload.Spec.StartupProbe).ToNot(BeNil()) - g.Expect(appWorkload.Spec.StartupProbe.HTTPGet).ToNot(BeNil()) - g.Expect(appWorkload.Spec.StartupProbe.HTTPGet.Path).To(Equal("/healthy")) - g.Expect(appWorkload.Spec.StartupProbe.HTTPGet.Port.IntValue()).To(Equal(8080)) - g.Expect(appWorkload.Spec.StartupProbe.InitialDelaySeconds).To(BeZero()) - g.Expect(appWorkload.Spec.StartupProbe.PeriodSeconds).To(BeEquivalentTo(2)) - g.Expect(appWorkload.Spec.StartupProbe.TimeoutSeconds).To(BeEquivalentTo(3)) - g.Expect(appWorkload.Spec.StartupProbe.FailureThreshold).To(BeEquivalentTo(5)) - - g.Expect(appWorkload.Spec.LivenessProbe).ToNot(BeNil()) - g.Expect(appWorkload.Spec.LivenessProbe.InitialDelaySeconds).To(BeZero()) - g.Expect(appWorkload.Spec.LivenessProbe.PeriodSeconds).To(BeEquivalentTo(30)) - g.Expect(appWorkload.Spec.LivenessProbe.TimeoutSeconds).To(BeEquivalentTo(3)) - g.Expect(appWorkload.Spec.LivenessProbe.FailureThreshold).To(BeEquivalentTo(1)) - }) - }) - }) - - When("the CFProcess has a port health check", func() { - BeforeEach(func() { - Expect(k8s.PatchResource(ctx, adminClient, cfApp, func() { - cfApp.Spec.DesiredState = korifiv1alpha1.StartedState - })).To(Succeed()) - - Expect(k8s.Patch(ctx, adminClient, cfProcess, func() { - cfProcess.Spec.HealthCheck = korifiv1alpha1.HealthCheck{ - Type: "port", - Data: korifiv1alpha1.HealthCheckData{ - InvocationTimeoutSeconds: 3, - TimeoutSeconds: 9, - }, - } - })).To(Succeed()) - }) - - It("sets the startup and liveness probes correctly on the AppWorkload", func() { - eventuallyCreatedAppWorkloadShould(cfProcess.Name, cfSpace.Status.GUID, func(g Gomega, appWorkload korifiv1alpha1.AppWorkload) { - g.Expect(appWorkload.Spec.StartupProbe).ToNot(BeNil()) - g.Expect(appWorkload.Spec.StartupProbe.TCPSocket).ToNot(BeNil()) - g.Expect(appWorkload.Spec.StartupProbe.TCPSocket.Port.IntValue()).To(Equal(8080)) - g.Expect(appWorkload.Spec.StartupProbe.InitialDelaySeconds).To(BeZero()) - g.Expect(appWorkload.Spec.StartupProbe.PeriodSeconds).To(BeEquivalentTo(2)) - g.Expect(appWorkload.Spec.StartupProbe.TimeoutSeconds).To(BeEquivalentTo(3)) - g.Expect(appWorkload.Spec.StartupProbe.FailureThreshold).To(BeEquivalentTo(5)) - - g.Expect(appWorkload.Spec.LivenessProbe).ToNot(BeNil()) - g.Expect(appWorkload.Spec.LivenessProbe.InitialDelaySeconds).To(BeZero()) - g.Expect(appWorkload.Spec.LivenessProbe.PeriodSeconds).To(BeEquivalentTo(30)) - g.Expect(appWorkload.Spec.LivenessProbe.TimeoutSeconds).To(BeEquivalentTo(3)) - g.Expect(appWorkload.Spec.LivenessProbe.FailureThreshold).To(BeEquivalentTo(1)) - }) - }) - }) - - When("the CFProcess has a process health check", func() { - BeforeEach(func() { - Expect(k8s.PatchResource(ctx, adminClient, cfApp, func() { - cfApp.Spec.DesiredState = korifiv1alpha1.StartedState - })).To(Succeed()) - - Expect(k8s.Patch(ctx, adminClient, cfProcess, func() { - cfProcess.Spec.HealthCheck = korifiv1alpha1.HealthCheck{Type: "process"} - })).To(Succeed()) - }) - - It("does not set liveness and startup probes on the AppWorkload", func() { - eventuallyCreatedAppWorkloadShould(cfProcess.Name, cfSpace.Status.GUID, func(g Gomega, appWorkload korifiv1alpha1.AppWorkload) { - g.Expect(appWorkload.Spec.StartupProbe).To(BeNil()) - g.Expect(appWorkload.Spec.LivenessProbe).To(BeNil()) - }) - }) - }) }) -func eventuallyCreatedAppWorkloadShould(processGUID, namespace string, shouldFn func(Gomega, korifiv1alpha1.AppWorkload)) { +func eventuallyCreatedAppWorkloadShould(shouldFn func(Gomega, korifiv1alpha1.AppWorkload)) { GinkgoHelper() Eventually(func(g Gomega) { var appWorkloads korifiv1alpha1.AppWorkloadList - err := adminClient.List(context.Background(), &appWorkloads, client.InNamespace(namespace), client.MatchingLabels{ - korifiv1alpha1.CFProcessGUIDLabelKey: processGUID, - }) - g.Expect(err).NotTo(HaveOccurred()) + g.Expect(adminClient.List(context.Background(), &appWorkloads, client.InNamespace(testNamespace))).To(Succeed()) g.Expect(appWorkloads.Items).To(HaveLen(1)) shouldFn(g, appWorkloads.Items[0]) diff --git a/controllers/controllers/workloads/processes/suite_test.go b/controllers/controllers/workloads/processes/suite_test.go new file mode 100644 index 000000000..d238ce5bf --- /dev/null +++ b/controllers/controllers/workloads/processes/suite_test.go @@ -0,0 +1,99 @@ +package processes_test + +import ( + "context" + "path/filepath" + "testing" + "time" + + korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" + "code.cloudfoundry.org/korifi/controllers/config" + "code.cloudfoundry.org/korifi/controllers/controllers/shared" + "code.cloudfoundry.org/korifi/controllers/controllers/workloads/env" + "code.cloudfoundry.org/korifi/controllers/controllers/workloads/processes" + "code.cloudfoundry.org/korifi/tests/helpers" + + "github.com/google/uuid" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "go.uber.org/zap/zapcore" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/scheme" + 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" +) + +var ( + ctx context.Context + stopManager context.CancelFunc + stopClientCache context.CancelFunc + testEnv *envtest.Environment + adminClient client.Client + testNamespace string +) + +func TestWorkloadsControllers(t *testing.T) { + SetDefaultEventuallyTimeout(10 * time.Second) + SetDefaultEventuallyPollingInterval(250 * time.Millisecond) + + RegisterFailHandler(Fail) + RunSpecs(t, "CFProcess Controllers Integration Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true), zap.Level(zapcore.DebugLevel))) + + ctx = context.Background() + + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{ + filepath.Join("..", "..", "..", "..", "helm", "korifi", "controllers", "crds"), + }, + ErrorIfCRDPathMissing: true, + } + + _, err := testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + + Expect(korifiv1alpha1.AddToScheme(scheme.Scheme)).To(Succeed()) + Expect(corev1.AddToScheme(scheme.Scheme)).To(Succeed()) + + k8sManager := helpers.NewK8sManager(testEnv, filepath.Join("helm", "korifi", "controllers", "role.yaml")) + Expect(shared.SetupIndexWithManager(k8sManager)).To(Succeed()) + + adminClient, stopClientCache = helpers.NewCachedClient(testEnv.Config) + + controllerConfig := &config.ControllerConfig{ + RunnerName: "cf-process-controller-test", + } + + err = processes.NewReconciler( + k8sManager.GetClient(), + k8sManager.GetScheme(), + ctrl.Log.WithName("controllers").WithName("CFProcess"), + controllerConfig, + env.NewProcessEnvBuilder(k8sManager.GetClient()), + ).SetupWithManager(k8sManager) + Expect(err).NotTo(HaveOccurred()) + + stopManager = helpers.StartK8sManager(k8sManager) +}) + +var _ = BeforeEach(func() { + testNamespace = uuid.NewString() + Expect(adminClient.Create(ctx, &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: testNamespace, + }, + })).To(Succeed()) +}) + +var _ = AfterSuite(func() { + stopManager() + stopClientCache() + Expect(testEnv.Stop()).To(Succeed()) +}) diff --git a/controllers/controllers/workloads/cfspace_controller.go b/controllers/controllers/workloads/spaces/controller.go similarity index 93% rename from controllers/controllers/workloads/cfspace_controller.go rename to controllers/controllers/workloads/spaces/controller.go index 5f7cb3654..3ab7580f4 100644 --- a/controllers/controllers/workloads/cfspace_controller.go +++ b/controllers/controllers/workloads/spaces/controller.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package workloads +package spaces import ( "context" @@ -40,8 +40,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" ) -// CFSpaceReconciler reconciles a CFSpace object -type CFSpaceReconciler struct { +type Reconciler struct { client client.Client namespaceReconciler *k8sns.Reconciler[korifiv1alpha1.CFSpace, *korifiv1alpha1.CFSpace] containerRegistrySecretNames []string @@ -49,7 +48,7 @@ type CFSpaceReconciler struct { appDeletionTimeout int64 } -func NewCFSpaceReconciler( +func NewReconciler( client client.Client, log logr.Logger, containerRegistrySecretNames []string, @@ -70,7 +69,7 @@ func NewCFSpaceReconciler( containerRegistrySecretNames, ) - return k8s.NewPatchingReconciler[korifiv1alpha1.CFSpace, *korifiv1alpha1.CFSpace](log, client, &CFSpaceReconciler{ + return k8s.NewPatchingReconciler[korifiv1alpha1.CFSpace, *korifiv1alpha1.CFSpace](log, client, &Reconciler{ client: client, namespaceReconciler: namespaceController, rootNamespace: rootNamespace, @@ -79,7 +78,7 @@ func NewCFSpaceReconciler( }) } -func (r *CFSpaceReconciler) SetupWithManager(mgr ctrl.Manager) *builder.Builder { +func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) *builder.Builder { return ctrl.NewControllerManagedBy(mgr). For(&korifiv1alpha1.CFSpace{}). Watches( @@ -96,7 +95,7 @@ func (r *CFSpaceReconciler) SetupWithManager(mgr ctrl.Manager) *builder.Builder ) } -func (r *CFSpaceReconciler) enqueueCFSpaceRequests(ctx context.Context, object client.Object) []reconcile.Request { +func (r *Reconciler) enqueueCFSpaceRequests(ctx context.Context, object client.Object) []reconcile.Request { cfSpaceList := &korifiv1alpha1.CFSpaceList{} err := r.client.List(ctx, cfSpaceList, client.InNamespace(object.GetNamespace())) if err != nil { @@ -111,7 +110,7 @@ func (r *CFSpaceReconciler) enqueueCFSpaceRequests(ctx context.Context, object c return requests } -func (r *CFSpaceReconciler) enqueueCFSpaceRequestsForServiceAccount(ctx context.Context, object client.Object) []reconcile.Request { +func (r *Reconciler) enqueueCFSpaceRequestsForServiceAccount(ctx context.Context, object client.Object) []reconcile.Request { if object.GetNamespace() != r.rootNamespace { return nil } @@ -139,7 +138,7 @@ func (r *CFSpaceReconciler) enqueueCFSpaceRequestsForServiceAccount(ctx context. //+kubebuilder:rbac:groups="rbac.authorization.k8s.io",resources=rolebindings,verbs=create;patch;delete;get;list;watch //+kubebuilder:rbac:groups="",resources=serviceaccounts,verbs=get;list;watch;create;patch;delete -func (r *CFSpaceReconciler) ReconcileResource(ctx context.Context, cfSpace *korifiv1alpha1.CFSpace) (ctrl.Result, error) { +func (r *Reconciler) ReconcileResource(ctx context.Context, cfSpace *korifiv1alpha1.CFSpace) (ctrl.Result, error) { var err error readyConditionBuilder := k8s.NewReadyConditionBuilder(cfSpace) defer func() { @@ -165,7 +164,7 @@ func (r *CFSpaceReconciler) ReconcileResource(ctx context.Context, cfSpace *kori return ctrl.Result{}, nil } -func (r *CFSpaceReconciler) reconcileServiceAccounts(ctx context.Context, space client.Object) error { +func (r *Reconciler) reconcileServiceAccounts(ctx context.Context, space client.Object) error { log := logr.FromContextOrDiscard(ctx).WithName("reconcileServiceAccounts"). WithValues("rootNamespace", r.rootNamespace, "targetNamespace", space.GetName()) diff --git a/controllers/controllers/workloads/cfspace_controller_test.go b/controllers/controllers/workloads/spaces/controller_test.go similarity index 92% rename from controllers/controllers/workloads/cfspace_controller_test.go rename to controllers/controllers/workloads/spaces/controller_test.go index 33131e1bc..ea83a6f97 100644 --- a/controllers/controllers/workloads/cfspace_controller_test.go +++ b/controllers/controllers/workloads/spaces/controller_test.go @@ -1,10 +1,9 @@ -package workloads_test +package spaces_test import ( "time" korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" - . "code.cloudfoundry.org/korifi/controllers/controllers/workloads/testutils" "code.cloudfoundry.org/korifi/tools/k8s" "github.com/google/uuid" @@ -28,7 +27,7 @@ var _ = Describe("CFSpaceReconciler Integration Tests", func() { cfSpace = &korifiv1alpha1.CFSpace{ ObjectMeta: metav1.ObjectMeta{ Name: uuid.NewString(), - Namespace: testOrg.Status.GUID, + Namespace: testNamespace, }, Spec: korifiv1alpha1.CFSpaceSpec{ DisplayName: uuid.NewString(), @@ -51,16 +50,9 @@ var _ = Describe("CFSpaceReconciler Integration Tests", func() { }).Should(Succeed()) }) - It("sets the finalizer on cfSpace", func() { - Eventually(func(g Gomega) { - g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(cfSpace), cfSpace)).To(Succeed()) - g.Expect(cfSpace.ObjectMeta.Finalizers).To(ConsistOf("cfSpace.korifi.cloudfoundry.org")) - }).Should(Succeed()) - }) - It("propagates the image-registry-credentials secrets to CFSpace", func() { Eventually(func(g Gomega) { - g.Expect(adminClient.Get(ctx, types.NamespacedName{Namespace: cfSpace.Name, Name: imageRegistrySecret.Name}, &corev1.Secret{})).To(Succeed()) + g.Expect(adminClient.Get(ctx, types.NamespacedName{Namespace: cfSpace.Name, Name: packageRegistrySecretName}, &corev1.Secret{})).To(Succeed()) }).Should(Succeed()) }) @@ -226,7 +218,7 @@ var _ = Describe("CFSpaceReconciler Integration Tests", func() { g.Expect(meta.IsStatusConditionTrue(cfSpace.Status.Conditions, korifiv1alpha1.StatusConditionReady)).To(BeTrue()) }, 20*time.Second).Should(Succeed()) - rootServiceAccount = createServiceAccount(ctx, PrefixedGUID("existing-service-account"), cfRootNamespace, map[string]string{"cloudfoundry.org/propagate-service-account": "true"}) + rootServiceAccount = createServiceAccount(ctx, uuid.NewString(), cfRootNamespace, map[string]string{"cloudfoundry.org/propagate-service-account": "true"}) // Ensure that the service account is propagated into the CFSpace namespace Eventually(func(g Gomega) { g.Expect(adminClient.Get(ctx, types.NamespacedName{Name: rootServiceAccount.Name, Namespace: cfSpace.Name}, &propagatedServiceAccount)).To(Succeed()) @@ -268,7 +260,7 @@ var _ = Describe("CFSpaceReconciler Integration Tests", func() { When("the package registry secret is added to the root service account", func() { var ( rootServiceAccount *corev1.ServiceAccount - propagatedServiceAccount corev1.ServiceAccount + propagatedServiceAccount *corev1.ServiceAccount tokenSecretName string dockercfgSecretName string ) @@ -281,7 +273,7 @@ var _ = Describe("CFSpaceReconciler Integration Tests", func() { rootServiceAccount = &corev1.ServiceAccount{ ObjectMeta: metav1.ObjectMeta{ - Name: PrefixedGUID("existing-service-account"), + Name: uuid.NewString(), Namespace: cfRootNamespace, Annotations: map[string]string{"cloudfoundry.org/propagate-service-account": "true"}, }, @@ -289,13 +281,14 @@ var _ = Describe("CFSpaceReconciler Integration Tests", func() { Expect(adminClient.Create(ctx, rootServiceAccount)).To(Succeed()) + propagatedServiceAccount = &corev1.ServiceAccount{} // Ensure that the service account is propagated into the CFSpace namespace Eventually(func() error { - return adminClient.Get(ctx, types.NamespacedName{Name: rootServiceAccount.Name, Namespace: cfSpace.Name}, &propagatedServiceAccount) + return adminClient.Get(ctx, types.NamespacedName{Name: rootServiceAccount.Name, Namespace: cfSpace.Name}, propagatedServiceAccount) }).Should(Succeed()) // Simulate k8s adding a token secret to the propagated service account - Expect(k8s.PatchResource(ctx, adminClient, &propagatedServiceAccount, func() { + Expect(k8s.PatchResource(ctx, adminClient, propagatedServiceAccount, func() { tokenSecretName = rootServiceAccount.Name + "-token-XYZABC" dockercfgSecretName = rootServiceAccount.Name + "-dockercfg-ABCXYZ" propagatedServiceAccount.Secrets = []corev1.ObjectReference{{Name: tokenSecretName}, {Name: dockercfgSecretName}} @@ -312,7 +305,7 @@ var _ = Describe("CFSpaceReconciler Integration Tests", func() { It("is also added to the space's copy of the service account", func() { Eventually(func(g Gomega) { g.Expect( - adminClient.Get(ctx, client.ObjectKeyFromObject(&propagatedServiceAccount), &propagatedServiceAccount), + adminClient.Get(ctx, client.ObjectKeyFromObject(propagatedServiceAccount), propagatedServiceAccount), ).To(Succeed()) g.Expect(propagatedServiceAccount.Secrets).To(ConsistOf( corev1.ObjectReference{Name: tokenSecretName}, diff --git a/controllers/controllers/workloads/spaces/suite_test.go b/controllers/controllers/workloads/spaces/suite_test.go new file mode 100644 index 000000000..2c0f515ef --- /dev/null +++ b/controllers/controllers/workloads/spaces/suite_test.go @@ -0,0 +1,141 @@ +package spaces_test + +import ( + "context" + "path/filepath" + "testing" + "time" + + korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" + "code.cloudfoundry.org/korifi/controllers/controllers/shared" + "code.cloudfoundry.org/korifi/controllers/controllers/workloads/labels" + "code.cloudfoundry.org/korifi/controllers/controllers/workloads/spaces" + "code.cloudfoundry.org/korifi/tests/helpers" + + "github.com/google/uuid" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "go.uber.org/zap/zapcore" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/scheme" + admission "k8s.io/pod-security-admission/api" + 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" +) + +var ( + ctx context.Context + stopManager context.CancelFunc + stopClientCache context.CancelFunc + testEnv *envtest.Environment + adminClient client.Client + cfRootNamespace string + testNamespace string +) + +const ( + packageRegistrySecretName = "test-package-registry-secret" +) + +func TestWorkloadsControllers(t *testing.T) { + SetDefaultEventuallyTimeout(10 * time.Second) + SetDefaultEventuallyPollingInterval(250 * time.Millisecond) + + RegisterFailHandler(Fail) + RunSpecs(t, "CFSpace Controller Integration Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true), zap.Level(zapcore.DebugLevel))) + + ctx = context.Background() + + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{ + filepath.Join("..", "..", "..", "..", "helm", "korifi", "controllers", "crds"), + }, + ErrorIfCRDPathMissing: true, + } + + _, err := testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + + Expect(korifiv1alpha1.AddToScheme(scheme.Scheme)).To(Succeed()) + Expect(corev1.AddToScheme(scheme.Scheme)).To(Succeed()) + + k8sManager := helpers.NewK8sManager(testEnv, filepath.Join("helm", "korifi", "controllers", "role.yaml")) + Expect(shared.SetupIndexWithManager(k8sManager)).To(Succeed()) + + adminClient, stopClientCache = helpers.NewCachedClient(testEnv.Config) + + labelCompiler := labels.NewCompiler().Defaults(map[string]string{ + admission.EnforceLevelLabel: string(admission.LevelRestricted), + admission.AuditLevelLabel: string(admission.LevelRestricted), + }) + + cfRootNamespace = uuid.NewString() + Expect(adminClient.Create(ctx, &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: cfRootNamespace, + }, + })).To(Succeed()) + + err = spaces.NewReconciler( + k8sManager.GetClient(), + ctrl.Log.WithName("controllers").WithName("CFSpace"), + []string{packageRegistrySecretName}, + cfRootNamespace, + int64(2), + labelCompiler, + ).SetupWithManager(k8sManager) + Expect(err).NotTo(HaveOccurred()) + + stopManager = helpers.StartK8sManager(k8sManager) +}) + +var _ = BeforeEach(func() { + testNamespace = uuid.NewString() + Expect(adminClient.Create(ctx, &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: testNamespace, + }, + })).To(Succeed()) + + Expect(adminClient.Create(ctx, &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: packageRegistrySecretName, + Namespace: testNamespace, + }, + })).To(Succeed()) +}) + +var _ = AfterSuite(func() { + stopManager() + stopClientCache() + Expect(testEnv.Stop()).To(Succeed()) +}) + +func createServiceAccount(ctx context.Context, serviceAccountName, namespace string, annotations map[string]string) *corev1.ServiceAccount { + serviceAccount := &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: serviceAccountName, + Namespace: namespace, + Annotations: annotations, + }, + Secrets: []corev1.ObjectReference{ + {Name: serviceAccountName + "-token-someguid"}, + {Name: serviceAccountName + "-dockercfg-someguid"}, + {Name: packageRegistrySecretName}, + }, + ImagePullSecrets: []corev1.LocalObjectReference{ + {Name: serviceAccountName + "-dockercfg-someguid"}, + {Name: packageRegistrySecretName}, + }, + } + Expect(adminClient.Create(ctx, serviceAccount)).To(Succeed()) + return serviceAccount +} diff --git a/controllers/controllers/workloads/suite_test.go b/controllers/controllers/workloads/suite_test.go deleted file mode 100644 index 7051d73d3..000000000 --- a/controllers/controllers/workloads/suite_test.go +++ /dev/null @@ -1,369 +0,0 @@ -package workloads_test - -import ( - "context" - "path/filepath" - "testing" - "time" - - korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" - "code.cloudfoundry.org/korifi/controllers/config" - "code.cloudfoundry.org/korifi/controllers/controllers/shared" - . "code.cloudfoundry.org/korifi/controllers/controllers/workloads" - "code.cloudfoundry.org/korifi/controllers/controllers/workloads/apps" - buildfake "code.cloudfoundry.org/korifi/controllers/controllers/workloads/build/fake" - "code.cloudfoundry.org/korifi/controllers/controllers/workloads/env" - "code.cloudfoundry.org/korifi/controllers/controllers/workloads/fake" - "code.cloudfoundry.org/korifi/controllers/controllers/workloads/labels" - "code.cloudfoundry.org/korifi/controllers/controllers/workloads/testutils" - "code.cloudfoundry.org/korifi/controllers/coordination" - controllerfake "code.cloudfoundry.org/korifi/controllers/fake" - "code.cloudfoundry.org/korifi/controllers/webhooks" - "code.cloudfoundry.org/korifi/controllers/webhooks/finalizer" - "code.cloudfoundry.org/korifi/controllers/webhooks/networking" - "code.cloudfoundry.org/korifi/controllers/webhooks/services" - "code.cloudfoundry.org/korifi/controllers/webhooks/version" - "code.cloudfoundry.org/korifi/controllers/webhooks/workloads" - "code.cloudfoundry.org/korifi/tests/helpers" - "code.cloudfoundry.org/korifi/tests/helpers/oci" - "code.cloudfoundry.org/korifi/tools" - "code.cloudfoundry.org/korifi/tools/image" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - servicebindingv1beta1 "github.com/servicebinding/runtime/apis/v1beta1" - "go.uber.org/zap/zapcore" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - k8sclient "k8s.io/client-go/kubernetes" - "k8s.io/client-go/kubernetes/scheme" - admission "k8s.io/pod-security-admission/api" - 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" -) - -var ( - ctx context.Context - stopManager context.CancelFunc - stopClientCache context.CancelFunc - testEnv *envtest.Environment - adminClient client.Client - cfRootNamespace string - testOrg *korifiv1alpha1.CFOrg - imageRegistrySecret *corev1.Secret - imageDeleter *fake.ImageDeleter - packageCleaner *fake.PackageCleaner - eventRecorder *controllerfake.EventRecorder - imageClient image.Client - containerRegistry *oci.Registry -) - -const ( - defaultMemoryMB = 128 - defaultDiskQuotaMB = 256 - defaultTimeout = 60 - - packageRegistrySecretName = "test-package-registry-secret" -) - -func TestWorkloadsControllers(t *testing.T) { - SetDefaultEventuallyTimeout(10 * time.Second) - SetDefaultEventuallyPollingInterval(250 * time.Millisecond) - - RegisterFailHandler(Fail) - RunSpecs(t, "Workloads Controllers Integration Suite") -} - -var _ = BeforeSuite(func() { - logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true), zap.Level(zapcore.DebugLevel))) - - ctx = context.Background() - - containerRegistry = oci.NewContainerRegistry("user", "password") - - testEnv = &envtest.Environment{ - CRDDirectoryPaths: []string{ - filepath.Join("..", "..", "..", "helm", "korifi", "controllers", "crds"), - }, - WebhookInstallOptions: envtest.WebhookInstallOptions{ - Paths: []string{filepath.Join("..", "..", "..", "helm", "korifi", "controllers", "manifests.yaml")}, - }, - ErrorIfCRDPathMissing: true, - } - - _, err := testEnv.Start() - Expect(err).NotTo(HaveOccurred()) - - Expect(korifiv1alpha1.AddToScheme(scheme.Scheme)).To(Succeed()) - Expect(servicebindingv1beta1.AddToScheme(scheme.Scheme)).To(Succeed()) - Expect(corev1.AddToScheme(scheme.Scheme)).To(Succeed()) - - k8sManager := helpers.NewK8sManager(testEnv, filepath.Join("helm", "korifi", "controllers", "role.yaml")) - Expect(shared.SetupIndexWithManager(k8sManager)).To(Succeed()) - - adminClient, stopClientCache = helpers.NewCachedClient(testEnv.Config) - - cfRootNamespace = testutils.PrefixedGUID("root-namespace") - - controllerConfig := &config.ControllerConfig{ - CFProcessDefaults: config.CFProcessDefaults{ - MemoryMB: 500, - DiskQuotaMB: 512, - }, - CFRootNamespace: cfRootNamespace, - ContainerRegistrySecretNames: []string{packageRegistrySecretName}, - SpaceFinalizerAppDeletionTimeout: tools.PtrTo(int64(2)), - } - - k8sClient, err := k8sclient.NewForConfig(k8sManager.GetConfig()) - Expect(err).NotTo(HaveOccurred()) - imageClient = image.NewClient(k8sClient) - - eventRecorder = new(controllerfake.EventRecorder) - - err = apps.NewReconciler( - k8sManager.GetClient(), - k8sManager.GetScheme(), - ctrl.Log.WithName("controllers").WithName("CFApp"), - env.NewVCAPServicesEnvValueBuilder(k8sManager.GetClient()), - env.NewVCAPApplicationEnvValueBuilder(k8sManager.GetClient(), nil), - ).SetupWithManager(k8sManager) - Expect(err).NotTo(HaveOccurred()) - - cfBuildpackBuildReconciler := NewCFBuildpackBuildReconciler( - k8sManager.GetClient(), - new(buildfake.BuildCleaner), - k8sManager.GetScheme(), - ctrl.Log.WithName("controllers").WithName("CFBuildpackBuild"), - controllerConfig, - env.NewAppEnvBuilder(k8sManager.GetClient()), - ) - err = (cfBuildpackBuildReconciler).SetupWithManager(k8sManager) - Expect(err).NotTo(HaveOccurred()) - - cfDockerBuildReconciler := NewCFDockerBuildReconciler( - k8sManager.GetClient(), - new(buildfake.BuildCleaner), - imageClient, - k8sManager.GetScheme(), - ctrl.Log.WithName("controllers").WithName("CFDockerBuild"), - ) - err = (cfDockerBuildReconciler).SetupWithManager(k8sManager) - Expect(err).NotTo(HaveOccurred()) - - err = (NewCFProcessReconciler( - k8sManager.GetClient(), - k8sManager.GetScheme(), - ctrl.Log.WithName("controllers").WithName("CFProcess"), - controllerConfig, - env.NewProcessEnvBuilder(k8sManager.GetClient()), - )).SetupWithManager(k8sManager) - Expect(err).NotTo(HaveOccurred()) - - imageDeleter = new(fake.ImageDeleter) - packageCleaner = new(fake.PackageCleaner) - err = (NewCFPackageReconciler( - k8sManager.GetClient(), - k8sManager.GetScheme(), - ctrl.Log.WithName("controllers").WithName("CFPackage"), - imageDeleter, - packageCleaner, - []string{"package-repo-secret-name"}, - )).SetupWithManager(k8sManager) - Expect(err).NotTo(HaveOccurred()) - - labelCompiler := labels.NewCompiler().Defaults(map[string]string{ - admission.EnforceLevelLabel: string(admission.LevelRestricted), - admission.AuditLevelLabel: string(admission.LevelRestricted), - }) - - err = NewCFOrgReconciler( - k8sManager.GetClient(), - ctrl.Log.WithName("controllers").WithName("CFOrg"), - controllerConfig.ContainerRegistrySecretNames, - labelCompiler, - ).SetupWithManager(k8sManager) - Expect(err).NotTo(HaveOccurred()) - - err = NewCFSpaceReconciler( - k8sManager.GetClient(), - ctrl.Log.WithName("controllers").WithName("CFSpace"), - controllerConfig.ContainerRegistrySecretNames, - controllerConfig.CFRootNamespace, - *controllerConfig.SpaceFinalizerAppDeletionTimeout, - labelCompiler, - ).SetupWithManager(k8sManager) - Expect(err).NotTo(HaveOccurred()) - - err = NewCFTaskReconciler( - k8sManager.GetClient(), - k8sManager.GetScheme(), - eventRecorder, - ctrl.Log.WithName("controllers").WithName("CFTask"), - env.NewAppEnvBuilder(k8sManager.GetClient()), - 2*time.Second, - ).SetupWithManager(k8sManager) - Expect(err).NotTo(HaveOccurred()) - - uncachedClient := helpers.NewUncachedClient(k8sManager.GetConfig()) - finalizer.NewControllersFinalizerWebhook().SetupWebhookWithManager(k8sManager) - version.NewVersionWebhook("some-version").SetupWebhookWithManager(k8sManager) - Expect((&korifiv1alpha1.CFApp{}).SetupWebhookWithManager(k8sManager)).To(Succeed()) - Expect(workloads.NewCFAppValidator( - webhooks.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, workloads.AppEntityType)), - ).SetupWebhookWithManager(k8sManager)).To(Succeed()) - (&workloads.AppRevWebhook{}).SetupWebhookWithManager(k8sManager) - - orgNameDuplicateValidator := webhooks.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, workloads.CFOrgEntityType)) - orgPlacementValidator := webhooks.NewPlacementValidator(uncachedClient, cfRootNamespace) - Expect(workloads.NewCFOrgValidator(orgNameDuplicateValidator, orgPlacementValidator).SetupWebhookWithManager(k8sManager)).To(Succeed()) - - spaceNameDuplicateValidator := webhooks.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, workloads.CFSpaceEntityType)) - spacePlacementValidator := webhooks.NewPlacementValidator(uncachedClient, cfRootNamespace) - Expect(workloads.NewCFSpaceValidator(spaceNameDuplicateValidator, spacePlacementValidator).SetupWebhookWithManager(k8sManager)).To(Succeed()) - - Expect(networking.NewCFDomainValidator(uncachedClient).SetupWebhookWithManager(k8sManager)).To(Succeed()) - Expect(services.NewCFServiceInstanceValidator( - webhooks.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, services.ServiceInstanceEntityType)), - ).SetupWebhookWithManager(k8sManager)).To(Succeed()) - - Expect((&korifiv1alpha1.CFPackage{}).SetupWebhookWithManager(k8sManager)).To(Succeed()) - - Expect(workloads.NewCFTaskValidator().SetupWebhookWithManager(k8sManager)).To(Succeed()) - Expect(workloads.NewCFTaskDefaulter(config.CFProcessDefaults{ - MemoryMB: 128, - DiskQuotaMB: 256, - }).SetupWebhookWithManager(k8sManager)).To(Succeed()) - - Expect(korifiv1alpha1.NewCFProcessDefaulter(defaultMemoryMB, defaultDiskQuotaMB, defaultTimeout). - SetupWebhookWithManager(k8sManager)).To(Succeed()) - Expect((&korifiv1alpha1.CFBuild{}).SetupWebhookWithManager(k8sManager)).To(Succeed()) - Expect((&korifiv1alpha1.CFRoute{}).SetupWebhookWithManager(k8sManager)).To(Succeed()) - Expect(networking.NewCFRouteValidator( - webhooks.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, networking.RouteEntityType)), - cfRootNamespace, - uncachedClient, - ).SetupWebhookWithManager(k8sManager)).To(Succeed()) - Expect(services.NewCFServiceBindingValidator( - webhooks.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, services.ServiceBindingEntityType)), - ).SetupWebhookWithManager(k8sManager)).To(Succeed()) - Expect(workloads.NewCFPackageValidator().SetupWebhookWithManager(k8sManager)).To(Succeed()) - - stopManager = helpers.StartK8sManager(k8sManager) - - createNamespace(cfRootNamespace) - imageRegistrySecret = createImageRegistrySecret(ctx, packageRegistrySecretName, cfRootNamespace) - - testOrg = createOrg(cfRootNamespace) -}) - -var _ = AfterSuite(func() { - stopManager() - stopClientCache() - Expect(testEnv.Stop()).To(Succeed()) -}) - -func createBuildWithDroplet(ctx context.Context, k8sClient client.Client, cfBuild *korifiv1alpha1.CFBuild, droplet *korifiv1alpha1.BuildDropletStatus) *korifiv1alpha1.CFBuild { - Expect( - k8sClient.Create(ctx, cfBuild), - ).To(Succeed()) - - patchedCFBuild := cfBuild.DeepCopy() - patchedCFBuild.Status.Droplet = droplet - Expect( - k8sClient.Status().Patch(ctx, patchedCFBuild, client.MergeFrom(cfBuild)), - ).To(Succeed()) - return patchedCFBuild -} - -func createNamespace(name string) *corev1.Namespace { - ns := &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - }, - } - Expect( - adminClient.Create(ctx, ns)).To(Succeed()) - return ns -} - -func createImageRegistrySecret(ctx context.Context, name string, namespace string) *corev1.Secret { - secret := &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Annotations: map[string]string{ - "kapp.k14s.io/foo": "bar", - "meta.helm.sh/baz": "foo", - "bar": "baz", - }, - }, - StringData: map[string]string{ - "foo": "bar", - }, - Type: "Docker", - } - Expect(adminClient.Create(ctx, secret)).To(Succeed()) - return secret -} - -func createServiceAccount(ctx context.Context, serviceAccountName, namespace string, annotations map[string]string) *corev1.ServiceAccount { - serviceAccount := &corev1.ServiceAccount{ - ObjectMeta: metav1.ObjectMeta{ - Name: serviceAccountName, - Namespace: namespace, - Annotations: annotations, - }, - Secrets: []corev1.ObjectReference{ - {Name: serviceAccountName + "-token-someguid"}, - {Name: serviceAccountName + "-dockercfg-someguid"}, - {Name: packageRegistrySecretName}, - }, - ImagePullSecrets: []corev1.LocalObjectReference{ - {Name: serviceAccountName + "-dockercfg-someguid"}, - {Name: packageRegistrySecretName}, - }, - } - Expect(adminClient.Create(ctx, serviceAccount)).To(Succeed()) - return serviceAccount -} - -func createOrg(rootNamespace string) *korifiv1alpha1.CFOrg { - org := &korifiv1alpha1.CFOrg{ - ObjectMeta: metav1.ObjectMeta{ - Name: testutils.PrefixedGUID("org"), - Namespace: rootNamespace, - }, - Spec: korifiv1alpha1.CFOrgSpec{ - DisplayName: testutils.PrefixedGUID("org"), - }, - } - Expect(adminClient.Create(ctx, org)).To(Succeed()) - Eventually(func(g Gomega) { - g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(org), org)).To(Succeed()) - g.Expect(meta.IsStatusConditionTrue(org.Status.Conditions, korifiv1alpha1.StatusConditionReady)).To(BeTrue()) - }).Should(Succeed()) - return org -} - -func createSpace(org *korifiv1alpha1.CFOrg) *korifiv1alpha1.CFSpace { - cfSpace := &korifiv1alpha1.CFSpace{ - ObjectMeta: metav1.ObjectMeta{ - Name: testutils.PrefixedGUID("space"), - Namespace: org.Status.GUID, - }, - Spec: korifiv1alpha1.CFSpaceSpec{ - DisplayName: testutils.PrefixedGUID("space"), - }, - } - Expect(adminClient.Create(ctx, cfSpace)).To(Succeed()) - Eventually(func(g Gomega) { - g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(cfSpace), cfSpace)).To(Succeed()) - g.Expect(meta.IsStatusConditionTrue(cfSpace.Status.Conditions, korifiv1alpha1.StatusConditionReady)).To(BeTrue()) - }).Should(Succeed()) - return cfSpace -} diff --git a/controllers/controllers/workloads/cftask_controller.go b/controllers/controllers/workloads/tasks/controller.go similarity index 88% rename from controllers/controllers/workloads/cftask_controller.go rename to controllers/controllers/workloads/tasks/controller.go index 64b5f7e38..0de63cf0e 100644 --- a/controllers/controllers/workloads/cftask_controller.go +++ b/controllers/controllers/workloads/tasks/controller.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package workloads +package tasks import ( "context" @@ -49,8 +49,7 @@ type TaskEnvBuilder interface { Build(context.Context, *korifiv1alpha1.CFApp) ([]corev1.EnvVar, error) } -// CFTaskReconciler reconciles a CFTask object -type CFTaskReconciler struct { +type Reconciler struct { k8sClient client.Client scheme *runtime.Scheme recorder record.EventRecorder @@ -59,7 +58,7 @@ type CFTaskReconciler struct { taskTTLDuration time.Duration } -func NewCFTaskReconciler( +func NewReconciler( client client.Client, scheme *runtime.Scheme, recorder record.EventRecorder, @@ -67,7 +66,7 @@ func NewCFTaskReconciler( envBuilder TaskEnvBuilder, taskTTLDuration time.Duration, ) *k8s.PatchingReconciler[korifiv1alpha1.CFTask, *korifiv1alpha1.CFTask] { - taskReconciler := CFTaskReconciler{ + taskReconciler := Reconciler{ k8sClient: client, scheme: scheme, recorder: recorder, @@ -78,7 +77,7 @@ func NewCFTaskReconciler( return k8s.NewPatchingReconciler[korifiv1alpha1.CFTask, *korifiv1alpha1.CFTask](log, client, &taskReconciler) } -func (r *CFTaskReconciler) SetupWithManager(mgr ctrl.Manager) *builder.Builder { +func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) *builder.Builder { return ctrl.NewControllerManagedBy(mgr). For(&korifiv1alpha1.CFTask{}). Owns(&korifiv1alpha1.TaskWorkload{}) @@ -90,7 +89,7 @@ func (r *CFTaskReconciler) SetupWithManager(mgr ctrl.Manager) *builder.Builder { //+kubebuilder:rbac:groups=korifi.cloudfoundry.org,resources=taskworkloads,verbs=get;list;watch;create;patch;delete //+kubebuilder:rbac:groups="",resources=events,verbs=create;patch -func (r *CFTaskReconciler) ReconcileResource(ctx context.Context, cfTask *korifiv1alpha1.CFTask) (ctrl.Result, error) { +func (r *Reconciler) ReconcileResource(ctx context.Context, cfTask *korifiv1alpha1.CFTask) (ctrl.Result, error) { log := logr.FromContextOrDiscard(ctx) cfTask.Status.ObservedGeneration = cfTask.Generation @@ -150,7 +149,7 @@ func (r *CFTaskReconciler) ReconcileResource(ctx context.Context, cfTask *korifi return r.reconcileResult(cfTask, nil) } -func (r *CFTaskReconciler) setTaskStatus(cfTask *korifiv1alpha1.CFTask, taskWorkloadConditions []metav1.Condition) { +func (r *Reconciler) setTaskStatus(cfTask *korifiv1alpha1.CFTask, taskWorkloadConditions []metav1.Condition) { for _, conditionType := range []string{ korifiv1alpha1.TaskStartedConditionType, korifiv1alpha1.TaskSucceededConditionType, @@ -165,7 +164,7 @@ func (r *CFTaskReconciler) setTaskStatus(cfTask *korifiv1alpha1.CFTask, taskWork } } -func (r *CFTaskReconciler) getApp(ctx context.Context, cfTask *korifiv1alpha1.CFTask) (*korifiv1alpha1.CFApp, error) { +func (r *Reconciler) getApp(ctx context.Context, cfTask *korifiv1alpha1.CFTask) (*korifiv1alpha1.CFApp, error) { log := logr.FromContextOrDiscard(ctx).WithName("getApp").WithValues("appName", cfTask.Spec.AppRef.Name) cfApp := new(korifiv1alpha1.CFApp) @@ -201,7 +200,7 @@ func (r *CFTaskReconciler) getApp(ctx context.Context, cfTask *korifiv1alpha1.CF return cfApp, nil } -func (r *CFTaskReconciler) getDroplet(ctx context.Context, cfTask *korifiv1alpha1.CFTask, cfApp *korifiv1alpha1.CFApp) (*korifiv1alpha1.CFBuild, error) { +func (r *Reconciler) getDroplet(ctx context.Context, cfTask *korifiv1alpha1.CFTask, cfApp *korifiv1alpha1.CFApp) (*korifiv1alpha1.CFBuild, error) { log := logr.FromContextOrDiscard(ctx).WithName("getDroplet").WithValues("dropletName", cfApp.Spec.CurrentDropletRef.Name) cfDroplet := new(korifiv1alpha1.CFBuild) @@ -228,7 +227,7 @@ func (r *CFTaskReconciler) getDroplet(ctx context.Context, cfTask *korifiv1alpha return cfDroplet, nil } -func (r *CFTaskReconciler) getWebProcess(ctx context.Context, cfApp *korifiv1alpha1.CFApp) (korifiv1alpha1.CFProcess, error) { +func (r *Reconciler) getWebProcess(ctx context.Context, cfApp *korifiv1alpha1.CFApp) (korifiv1alpha1.CFProcess, error) { var processList korifiv1alpha1.CFProcessList err := r.k8sClient.List(ctx, &processList, client.InNamespace(cfApp.Namespace), client.MatchingLabels{ korifiv1alpha1.CFAppGUIDLabelKey: cfApp.Name, @@ -245,7 +244,7 @@ func (r *CFTaskReconciler) getWebProcess(ctx context.Context, cfApp *korifiv1alp return processList.Items[0], nil } -func (r *CFTaskReconciler) createOrPatchTaskWorkload(ctx context.Context, cfTask *korifiv1alpha1.CFTask, cfDroplet *korifiv1alpha1.CFBuild, webProcess korifiv1alpha1.CFProcess, env []corev1.EnvVar) (*korifiv1alpha1.TaskWorkload, error) { +func (r *Reconciler) createOrPatchTaskWorkload(ctx context.Context, cfTask *korifiv1alpha1.CFTask, cfDroplet *korifiv1alpha1.CFBuild, webProcess korifiv1alpha1.CFProcess, env []corev1.EnvVar) (*korifiv1alpha1.TaskWorkload, error) { log := logr.FromContextOrDiscard(ctx).WithName("createOrPatchTaskWorkload") taskWorkload := &korifiv1alpha1.TaskWorkload{ @@ -314,7 +313,7 @@ func calculateDefaultCPURequestMillicores(memoryMiB int64) int64 { return cpuMillicores } -func (r *CFTaskReconciler) initializeStatus(ctx context.Context, cfTask *korifiv1alpha1.CFTask, cfDroplet *korifiv1alpha1.CFBuild) { +func (r *Reconciler) initializeStatus(ctx context.Context, cfTask *korifiv1alpha1.CFTask, cfDroplet *korifiv1alpha1.CFBuild) { cfTask.Status.DropletRef.Name = cfDroplet.Name meta.SetStatusCondition(&cfTask.Status.Conditions, metav1.Condition{ Type: korifiv1alpha1.TaskInitializedConditionType, @@ -324,7 +323,7 @@ func (r *CFTaskReconciler) initializeStatus(ctx context.Context, cfTask *korifiv }) } -func (r *CFTaskReconciler) handleCancelation(ctx context.Context, cfTask *korifiv1alpha1.CFTask) error { +func (r *Reconciler) handleCancelation(ctx context.Context, cfTask *korifiv1alpha1.CFTask) error { log := logr.FromContextOrDiscard(ctx).WithName("handleCancelation") taskWorkload := &korifiv1alpha1.TaskWorkload{ @@ -358,7 +357,7 @@ func (r *CFTaskReconciler) handleCancelation(ctx context.Context, cfTask *korifi return nil } -func (r *CFTaskReconciler) reconcileResult(cfTask *korifiv1alpha1.CFTask, reconcileErr error) (ctrl.Result, error) { +func (r *Reconciler) reconcileResult(cfTask *korifiv1alpha1.CFTask, reconcileErr error) (ctrl.Result, error) { if reconcileErr != nil { return ctrl.Result{}, reconcileErr } @@ -366,7 +365,7 @@ func (r *CFTaskReconciler) reconcileResult(cfTask *korifiv1alpha1.CFTask, reconc return ctrl.Result{RequeueAfter: r.computeRequeueAfter(cfTask)}, nil } -func (r *CFTaskReconciler) computeRequeueAfter(cfTask *korifiv1alpha1.CFTask) time.Duration { +func (r *Reconciler) computeRequeueAfter(cfTask *korifiv1alpha1.CFTask) time.Duration { completeTime, isCompleted := getCompletionTime(cfTask) if !isCompleted { return 0 @@ -375,7 +374,7 @@ func (r *CFTaskReconciler) computeRequeueAfter(cfTask *korifiv1alpha1.CFTask) ti return time.Until(completeTime.Add(r.taskTTLDuration)) } -func (r *CFTaskReconciler) alreadyExpired(cfTask *korifiv1alpha1.CFTask) bool { +func (r *Reconciler) alreadyExpired(cfTask *korifiv1alpha1.CFTask) bool { completeTime, isCompleted := getCompletionTime(cfTask) if !isCompleted { diff --git a/controllers/controllers/workloads/cftask_controller_test.go b/controllers/controllers/workloads/tasks/controller_test.go similarity index 74% rename from controllers/controllers/workloads/cftask_controller_test.go rename to controllers/controllers/workloads/tasks/controller_test.go index f7623b903..3fd235083 100644 --- a/controllers/controllers/workloads/cftask_controller_test.go +++ b/controllers/controllers/workloads/tasks/controller_test.go @@ -1,40 +1,35 @@ -package workloads_test +package tasks_test import ( - "fmt" - korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" - "code.cloudfoundry.org/korifi/controllers/controllers/workloads/testutils" "code.cloudfoundry.org/korifi/tools/k8s" + "github.com/google/uuid" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" . "github.com/onsi/gomega/gstruct" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" ) var _ = Describe("CFTaskReconciler Integration Tests", func() { var ( - cfApp *korifiv1alpha1.CFApp - cfDroplet *korifiv1alpha1.CFBuild - cfSpace *korifiv1alpha1.CFSpace - cfTask *korifiv1alpha1.CFTask - envSecret *corev1.Secret + cfApp *korifiv1alpha1.CFApp + cfDroplet *korifiv1alpha1.CFBuild + cfTask *korifiv1alpha1.CFTask + envSecret *corev1.Secret + eventCallCount int ) BeforeEach(func() { - cfSpace = createSpace(testOrg) - - cfAppName := testutils.PrefixedGUID("app") + cfAppName := uuid.NewString() cfPackage := &korifiv1alpha1.CFPackage{ ObjectMeta: metav1.ObjectMeta{ - Namespace: cfSpace.Status.GUID, - Name: testutils.PrefixedGUID("package"), + Namespace: testNamespace, + Name: uuid.NewString(), }, Spec: korifiv1alpha1.CFPackageSpec{ Type: "bits", @@ -47,8 +42,8 @@ var _ = Describe("CFTaskReconciler Integration Tests", func() { cfDroplet = &korifiv1alpha1.CFBuild{ ObjectMeta: metav1.ObjectMeta{ - Namespace: cfSpace.Status.GUID, - Name: testutils.PrefixedGUID("droplet"), + Namespace: testNamespace, + Name: uuid.NewString(), }, Spec: korifiv1alpha1.CFBuildSpec{ PackageRef: corev1.LocalObjectReference{ @@ -62,30 +57,25 @@ var _ = Describe("CFTaskReconciler Integration Tests", func() { } Expect(adminClient.Create(ctx, cfDroplet)).To(Succeed()) - cfDropletCopy := cfDroplet.DeepCopy() - cfDropletCopy.Status.Droplet = &korifiv1alpha1.BuildDropletStatus{ - Registry: korifiv1alpha1.Registry{ - Image: "registry.io/my/image", - ImagePullSecrets: []corev1.LocalObjectReference{{ - Name: "registry-secret", + Expect(k8s.Patch(ctx, adminClient, cfDroplet, func() { + cfDroplet.Status.Droplet = &korifiv1alpha1.BuildDropletStatus{ + Registry: korifiv1alpha1.Registry{ + Image: "registry.io/my/image", + ImagePullSecrets: []corev1.LocalObjectReference{{ + Name: "registry-secret", + }}, + }, + ProcessTypes: []korifiv1alpha1.ProcessType{{ + Type: "web", + Command: "cmd", }}, - }, - ProcessTypes: []korifiv1alpha1.ProcessType{{ - Type: "web", - Command: "cmd", - }}, - } - meta.SetStatusCondition(&cfDropletCopy.Status.Conditions, metav1.Condition{ - Type: "type", - Status: "Unknown", - Reason: "reason", - }) - Expect(adminClient.Status().Patch(ctx, cfDropletCopy, client.MergeFrom(cfDroplet))).To(Succeed()) + } + })).To(Succeed()) envSecret = &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: testutils.PrefixedGUID("env-secret"), - Namespace: cfSpace.Status.GUID, + Name: uuid.NewString(), + Namespace: testNamespace, }, StringData: map[string]string{ "BOB": "flemming", @@ -97,8 +87,8 @@ var _ = Describe("CFTaskReconciler Integration Tests", func() { cfProcess := &korifiv1alpha1.CFProcess{ ObjectMeta: metav1.ObjectMeta{ - Namespace: cfSpace.Status.GUID, - Name: testutils.PrefixedGUID("web-process"), + Namespace: testNamespace, + Name: uuid.NewString(), Labels: map[string]string{ korifiv1alpha1.CFProcessTypeLabelKey: "web", korifiv1alpha1.CFAppGUIDLabelKey: cfAppName, @@ -120,7 +110,7 @@ var _ = Describe("CFTaskReconciler Integration Tests", func() { cfApp = &korifiv1alpha1.CFApp{ ObjectMeta: metav1.ObjectMeta{ - Namespace: cfSpace.Status.GUID, + Namespace: testNamespace, Name: cfAppName, }, Spec: korifiv1alpha1.CFAppSpec{ @@ -135,10 +125,38 @@ var _ = Describe("CFTaskReconciler Integration Tests", func() { } Expect(adminClient.Create(ctx, cfApp)).To(Succeed()) + vcapApplicationSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testNamespace, + Name: uuid.NewString(), + }, + StringData: map[string]string{ + "VCAP_APPLICATION": "{}", + }, + } + Expect(adminClient.Create(ctx, vcapApplicationSecret)).To(Succeed()) + + vcapServicesSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testNamespace, + Name: uuid.NewString(), + }, + StringData: map[string]string{ + "VCAP_SERVICES": "{}", + }, + } + Expect(adminClient.Create(ctx, vcapServicesSecret)).To(Succeed()) + + Expect(k8s.Patch(ctx, adminClient, cfApp, func() { + cfApp.Status.VCAPApplicationSecretName = vcapApplicationSecret.Name + cfApp.Status.VCAPServicesSecretName = vcapServicesSecret.Name + meta.SetStatusCondition(&cfApp.Status.Conditions, k8s.NewReadyConditionBuilder(cfApp).Ready().Build()) + })).To(Succeed()) + cfTask = &korifiv1alpha1.CFTask{ ObjectMeta: metav1.ObjectMeta{ - Namespace: cfSpace.Status.GUID, - Name: testutils.PrefixedGUID("cftask"), + Namespace: testNamespace, + Name: uuid.NewString(), }, Spec: korifiv1alpha1.CFTaskSpec{ Command: "echo hello", @@ -147,47 +165,37 @@ var _ = Describe("CFTaskReconciler Integration Tests", func() { }, }, } - }) - Describe("CFTask creation", func() { - var ( - eventCallCount int - task *korifiv1alpha1.CFTask - ) - - BeforeEach(func() { - task = &korifiv1alpha1.CFTask{} - Eventually(func(g Gomega) { - g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(cfApp), cfApp)).To(Succeed()) - g.Expect(meta.IsStatusConditionTrue(cfApp.Status.Conditions, korifiv1alpha1.StatusConditionReady)).To(BeTrue()) - }).Should(Succeed()) - - eventCallCount = eventRecorder.EventfCallCount() - }) + eventCallCount = eventRecorder.EventfCallCount() + Expect(adminClient.Create(ctx, cfTask)).To(Succeed()) + Expect(k8s.Patch(ctx, adminClient, cfTask, func() { + cfTask.Status.MemoryMB = 128 + cfTask.Status.DiskQuotaMB = 256 + })).To(Succeed()) + }) + Describe("CFTask initialization", func() { JustBeforeEach(func() { - Expect(adminClient.Create(ctx, cfTask)).To(Succeed()) - Eventually(func(g Gomega) { - g.Expect(adminClient.Get(ctx, types.NamespacedName{Namespace: cfSpace.Status.GUID, Name: cfTask.Name}, task)).To(Succeed()) - initializedStatusCondition := meta.FindStatusCondition(task.Status.Conditions, korifiv1alpha1.TaskInitializedConditionType) + g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(cfTask), cfTask)).To(Succeed()) + initializedStatusCondition := meta.FindStatusCondition(cfTask.Status.Conditions, korifiv1alpha1.TaskInitializedConditionType) g.Expect(initializedStatusCondition).NotTo(BeNil()) g.Expect(initializedStatusCondition.Status).To(Equal(metav1.ConditionTrue), "task did not become initialized") g.Expect(initializedStatusCondition.Reason).To(Equal("TaskInitialized")) - g.Expect(initializedStatusCondition.ObservedGeneration).To(Equal(task.Generation)) + g.Expect(initializedStatusCondition.ObservedGeneration).To(Equal(cfTask.Generation)) }).Should(Succeed()) }) It("sets the app to be the task owner", func() { - Expect(task.GetOwnerReferences()).To(ConsistOf(HaveField("Name", cfApp.Name))) + Expect(cfTask.GetOwnerReferences()).To(ConsistOf(HaveField("Name", cfApp.Name))) }) It("populates the droplet name in the status", func() { - Expect(task.Status.DropletRef.Name).To(Equal(cfDroplet.Name)) + Expect(cfTask.Status.DropletRef.Name).To(Equal(cfDroplet.Name)) }) It("sets the ObservedGeneration status field", func() { - Expect(task.Status.ObservedGeneration).To(Equal(task.Generation)) + Expect(cfTask.Status.ObservedGeneration).To(Equal(cfTask.Generation)) }) It("creates an TaskWorkload", func() { @@ -197,7 +205,7 @@ var _ = Describe("CFTaskReconciler Integration Tests", func() { var taskWorkloads korifiv1alpha1.TaskWorkloadList g.Expect(adminClient.List(ctx, &taskWorkloads, - client.InNamespace(cfSpace.Status.GUID), + client.InNamespace(testNamespace), client.MatchingLabels{korifiv1alpha1.CFTaskGUIDLabelKey: cfTask.Name}, )).To(Succeed()) g.Expect(taskWorkloads.Items).To(HaveLen(1)) @@ -207,10 +215,10 @@ var _ = Describe("CFTaskReconciler Integration Tests", func() { g.Expect(taskWorkload.Spec.Command).To(Equal([]string{"/cnb/lifecycle/launcher", "echo hello"})) g.Expect(taskWorkload.Spec.Image).To(Equal("registry.io/my/image")) g.Expect(taskWorkload.Spec.ImagePullSecrets).To(Equal([]corev1.LocalObjectReference{{Name: "registry-secret"}})) - g.Expect(taskWorkload.Spec.Resources.Requests.Memory().String()).To(Equal(fmt.Sprintf("%dM", defaultMemoryMB))) - g.Expect(taskWorkload.Spec.Resources.Limits.Memory().String()).To(Equal(fmt.Sprintf("%dM", defaultMemoryMB))) - g.Expect(taskWorkload.Spec.Resources.Requests.StorageEphemeral().String()).To(Equal(fmt.Sprintf("%dM", defaultDiskQuotaMB))) - g.Expect(taskWorkload.Spec.Resources.Limits.StorageEphemeral().String()).To(Equal(fmt.Sprintf("%dM", defaultDiskQuotaMB))) + g.Expect(taskWorkload.Spec.Resources.Requests.Memory().String()).To(Equal("128M")) + g.Expect(taskWorkload.Spec.Resources.Limits.Memory().String()).To(Equal("128M")) + g.Expect(taskWorkload.Spec.Resources.Requests.StorageEphemeral().String()).To(Equal("256M")) + g.Expect(taskWorkload.Spec.Resources.Limits.StorageEphemeral().String()).To(Equal("256M")) g.Expect(taskWorkload.Spec.Resources.Requests.Cpu().String()).To(Equal("75m")) g.Expect(taskWorkload.GetOwnerReferences()).To(ConsistOf(SatisfyAll( HaveField("Name", cfTask.Name), @@ -273,11 +281,11 @@ var _ = Describe("CFTaskReconciler Integration Tests", func() { Expect(eventRecorder.EventfCallCount()).To(Equal(eventCallCount+1), "eventRecorder.Eventf call count mismatch") eventTaskObj, eventType, eventReason, eventMessage, eventMessageArgs := eventRecorder.EventfArgsForCall(eventCallCount) eventTask := eventTaskObj.(*korifiv1alpha1.CFTask) - Expect(client.ObjectKeyFromObject(eventTask)).To(Equal(client.ObjectKeyFromObject(task))) + Expect(client.ObjectKeyFromObject(eventTask)).To(Equal(client.ObjectKeyFromObject(cfTask))) Expect(eventType).To(Equal("Normal"), "Unexpected event type in event record") Expect(eventReason).To(Equal("TaskWorkloadCreated"), "Unexpected event reason in event record") Expect(eventMessage).To(Equal("Created task workload %s"), "Unexpected event message in event record") - Expect(eventMessageArgs).To(Equal([]interface{}{task.Name}), "Unexpected event message args in event record") + Expect(eventMessageArgs).To(Equal([]interface{}{cfTask.Name}), "Unexpected event message args in event record") }) When("the task workload status condition changes", func() { @@ -286,7 +294,7 @@ var _ = Describe("CFTaskReconciler Integration Tests", func() { var taskWorkloads korifiv1alpha1.TaskWorkloadList g.Expect(adminClient.List(ctx, &taskWorkloads, - client.InNamespace(cfSpace.Status.GUID), + client.InNamespace(testNamespace), client.MatchingLabels{korifiv1alpha1.CFTaskGUIDLabelKey: cfTask.Name}, )).To(Succeed()) g.Expect(taskWorkloads.Items).To(HaveLen(1)) @@ -305,18 +313,14 @@ var _ = Describe("CFTaskReconciler Integration Tests", func() { It("reflects the status in the korifi task", func() { Eventually(func(g Gomega) { - g.Expect(adminClient.Get(ctx, types.NamespacedName{Namespace: cfSpace.Status.GUID, Name: cfTask.Name}, task)).To(Succeed()) - g.Expect(meta.IsStatusConditionTrue(task.Status.Conditions, korifiv1alpha1.TaskStartedConditionType)).To(BeTrue()) + g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(cfTask), cfTask)).To(Succeed()) + g.Expect(meta.IsStatusConditionTrue(cfTask.Status.Conditions, korifiv1alpha1.TaskStartedConditionType)).To(BeTrue()) }).Should(Succeed()) }) }) }) Describe("CFTask Cancellation", func() { - BeforeEach(func() { - Expect(adminClient.Create(ctx, cfTask)).To(Succeed()) - }) - When("spec.canceled is set to true", func() { BeforeEach(func() { Expect(k8s.PatchResource(ctx, adminClient, cfTask, func() { @@ -339,13 +343,11 @@ var _ = Describe("CFTaskReconciler Integration Tests", func() { Describe("CFTask TTL", func() { BeforeEach(func() { - Expect(adminClient.Create(ctx, cfTask)).To(Succeed()) - Eventually(func(g Gomega) { var taskWorkloads korifiv1alpha1.TaskWorkloadList g.Expect(adminClient.List(ctx, &taskWorkloads, - client.InNamespace(cfSpace.Status.GUID), + client.InNamespace(testNamespace), client.MatchingLabels{korifiv1alpha1.CFTaskGUIDLabelKey: cfTask.Name}, )).To(Succeed()) g.Expect(taskWorkloads.Items).To(HaveLen(1)) diff --git a/controllers/controllers/workloads/tasks/suite_test.go b/controllers/controllers/workloads/tasks/suite_test.go new file mode 100644 index 000000000..5988ebe40 --- /dev/null +++ b/controllers/controllers/workloads/tasks/suite_test.go @@ -0,0 +1,99 @@ +package tasks_test + +import ( + "context" + "path/filepath" + "testing" + "time" + + korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" + "code.cloudfoundry.org/korifi/controllers/controllers/shared" + "code.cloudfoundry.org/korifi/controllers/controllers/workloads/env" + "code.cloudfoundry.org/korifi/controllers/controllers/workloads/tasks" + controllerfake "code.cloudfoundry.org/korifi/controllers/fake" + "code.cloudfoundry.org/korifi/tests/helpers" + + "github.com/google/uuid" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "go.uber.org/zap/zapcore" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/scheme" + 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" +) + +var ( + ctx context.Context + stopManager context.CancelFunc + stopClientCache context.CancelFunc + testEnv *envtest.Environment + adminClient client.Client + testNamespace string + eventRecorder *controllerfake.EventRecorder +) + +func TestWorkloadsControllers(t *testing.T) { + SetDefaultEventuallyTimeout(10 * time.Second) + SetDefaultEventuallyPollingInterval(250 * time.Millisecond) + + RegisterFailHandler(Fail) + RunSpecs(t, "CFTask Controller Integration Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true), zap.Level(zapcore.DebugLevel))) + + ctx = context.Background() + + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{ + filepath.Join("..", "..", "..", "..", "helm", "korifi", "controllers", "crds"), + }, + ErrorIfCRDPathMissing: true, + } + + _, err := testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + + Expect(korifiv1alpha1.AddToScheme(scheme.Scheme)).To(Succeed()) + Expect(corev1.AddToScheme(scheme.Scheme)).To(Succeed()) + + k8sManager := helpers.NewK8sManager(testEnv, filepath.Join("helm", "korifi", "controllers", "role.yaml")) + Expect(shared.SetupIndexWithManager(k8sManager)).To(Succeed()) + + adminClient, stopClientCache = helpers.NewCachedClient(testEnv.Config) + + eventRecorder = new(controllerfake.EventRecorder) + + err = tasks.NewReconciler( + k8sManager.GetClient(), + k8sManager.GetScheme(), + eventRecorder, + ctrl.Log.WithName("controllers").WithName("CFTask"), + env.NewAppEnvBuilder(k8sManager.GetClient()), + 2*time.Second, + ).SetupWithManager(k8sManager) + Expect(err).NotTo(HaveOccurred()) + + stopManager = helpers.StartK8sManager(k8sManager) +}) + +var _ = BeforeEach(func() { + testNamespace = uuid.NewString() + Expect(adminClient.Create(ctx, &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: testNamespace, + }, + })).To(Succeed()) +}) + +var _ = AfterSuite(func() { + stopManager() + stopClientCache() + Expect(testEnv.Stop()).To(Succeed()) +}) diff --git a/controllers/controllers/workloads/testutils/shared_test_utils.go b/controllers/controllers/workloads/testutils/shared_test_utils.go index cd1878b6f..442e6b813 100644 --- a/controllers/controllers/workloads/testutils/shared_test_utils.go +++ b/controllers/controllers/workloads/testutils/shared_test_utils.go @@ -1,138 +1,9 @@ package testutils import ( - "encoding/base64" - - korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" - "code.cloudfoundry.org/korifi/tools" - "github.com/google/uuid" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func GenerateGUID() string { - return uuid.NewString() -} - func PrefixedGUID(prefix string) string { return prefix + "-" + uuid.NewString()[:8] } - -func BuildCFAppEnvVarsSecret(appGUID, spaceGUID string, envVars map[string]string) *corev1.Secret { - return &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: spaceGUID, - Name: appGUID + "-env", - }, - StringData: envVars, - } -} - -func BuildCFBuildObject(cfBuildGUID string, namespace string, cfPackageGUID string, cfAppGUID string) *korifiv1alpha1.CFBuild { - return &korifiv1alpha1.CFBuild{ - ObjectMeta: metav1.ObjectMeta{ - Name: cfBuildGUID, - Namespace: namespace, - }, - TypeMeta: metav1.TypeMeta{ - Kind: "CFBuild", - APIVersion: "korifi.cloudfoundry.org/v1alpha1", - }, - Spec: korifiv1alpha1.CFBuildSpec{ - PackageRef: corev1.LocalObjectReference{ - Name: cfPackageGUID, - }, - AppRef: corev1.LocalObjectReference{ - Name: cfAppGUID, - }, - StagingMemoryMB: 1024, - StagingDiskMB: 1024, - Lifecycle: korifiv1alpha1.Lifecycle{ - Type: "buildpack", - Data: korifiv1alpha1.LifecycleData{ - Buildpacks: nil, - Stack: "", - }, - }, - }, - } -} - -func BuildCFBuildDropletStatusObject(dropletProcessTypeMap map[string]string) *korifiv1alpha1.BuildDropletStatus { - dropletProcessTypes := make([]korifiv1alpha1.ProcessType, 0, len(dropletProcessTypeMap)) - for k, v := range dropletProcessTypeMap { - dropletProcessTypes = append(dropletProcessTypes, korifiv1alpha1.ProcessType{ - Type: k, - Command: v, - }) - } - return &korifiv1alpha1.BuildDropletStatus{ - Registry: korifiv1alpha1.Registry{ - Image: "image/registry/url", - ImagePullSecrets: []corev1.LocalObjectReference{{Name: "some-image-pull-secret"}}, - }, - Stack: "cflinuxfs3", - ProcessTypes: dropletProcessTypes, - } -} - -func BuildDockerRegistrySecret(name, namespace string) *corev1.Secret { - dockerRegistryUsername := "user" - dockerRegistryPassword := "password" - dockerAuth := base64.StdEncoding.EncodeToString([]byte(dockerRegistryUsername + ":" + dockerRegistryPassword)) - dockerConfigJSON := `{"auths":{"https://index.docker.io/v1/":{"username":"` + dockerRegistryUsername + `","password":"` + dockerRegistryPassword + `","auth":"` + dockerAuth + `"}}}` - return &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - }, - Immutable: nil, - Data: nil, - StringData: map[string]string{ - ".dockerconfigjson": dockerConfigJSON, - }, - Type: "kubernetes.io/dockerconfigjson", - } -} - -func BuildServiceAccount(name, namespace, imagePullSecretName string) *corev1.ServiceAccount { - return &corev1.ServiceAccount{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - }, - Secrets: []corev1.ObjectReference{{Name: imagePullSecretName}}, - ImagePullSecrets: []corev1.LocalObjectReference{{Name: imagePullSecretName}}, - } -} - -func BuildCFProcessCRObject(cfProcessGUID, namespace, cfAppGUID, processType, processCommand, processDetectedCommand string) *korifiv1alpha1.CFProcess { - return &korifiv1alpha1.CFProcess{ - ObjectMeta: metav1.ObjectMeta{ - Name: cfProcessGUID, - Namespace: namespace, - Labels: map[string]string{ - korifiv1alpha1.CFAppGUIDLabelKey: cfAppGUID, - korifiv1alpha1.CFProcessGUIDLabelKey: cfProcessGUID, - korifiv1alpha1.CFProcessTypeLabelKey: processType, - }, - }, - Spec: korifiv1alpha1.CFProcessSpec{ - AppRef: corev1.LocalObjectReference{Name: cfAppGUID}, - ProcessType: processType, - Command: processCommand, - DetectedCommand: processDetectedCommand, - HealthCheck: korifiv1alpha1.HealthCheck{ - Type: "process", - Data: korifiv1alpha1.HealthCheckData{ - InvocationTimeoutSeconds: 0, - TimeoutSeconds: 0, - }, - }, - DesiredInstances: tools.PtrTo(1), - MemoryMB: 1024, - DiskQuotaMB: 100, - }, - } -} diff --git a/controllers/main.go b/controllers/main.go index cc7944a5f..99ed3f47d 100644 --- a/controllers/main.go +++ b/controllers/main.go @@ -32,17 +32,29 @@ import ( "code.cloudfoundry.org/korifi/controllers/controllers/services/bindings" "code.cloudfoundry.org/korifi/controllers/controllers/services/instances" "code.cloudfoundry.org/korifi/controllers/controllers/shared" - workloadscontrollers "code.cloudfoundry.org/korifi/controllers/controllers/workloads" "code.cloudfoundry.org/korifi/controllers/controllers/workloads/apps" + "code.cloudfoundry.org/korifi/controllers/controllers/workloads/build/buildpack" + "code.cloudfoundry.org/korifi/controllers/controllers/workloads/build/docker" "code.cloudfoundry.org/korifi/controllers/controllers/workloads/env" "code.cloudfoundry.org/korifi/controllers/controllers/workloads/labels" + "code.cloudfoundry.org/korifi/controllers/controllers/workloads/orgs" + "code.cloudfoundry.org/korifi/controllers/controllers/workloads/packages" + "code.cloudfoundry.org/korifi/controllers/controllers/workloads/processes" + "code.cloudfoundry.org/korifi/controllers/controllers/workloads/spaces" + "code.cloudfoundry.org/korifi/controllers/controllers/workloads/tasks" "code.cloudfoundry.org/korifi/controllers/coordination" - "code.cloudfoundry.org/korifi/controllers/webhooks" controllersfinalizer "code.cloudfoundry.org/korifi/controllers/webhooks/finalizer" - "code.cloudfoundry.org/korifi/controllers/webhooks/networking" - "code.cloudfoundry.org/korifi/controllers/webhooks/services" + domainswebhook "code.cloudfoundry.org/korifi/controllers/webhooks/networking/domains" + routeswebhook "code.cloudfoundry.org/korifi/controllers/webhooks/networking/routes" + bindingswebhook "code.cloudfoundry.org/korifi/controllers/webhooks/services/bindings" + instanceswebhook "code.cloudfoundry.org/korifi/controllers/webhooks/services/instances" + "code.cloudfoundry.org/korifi/controllers/webhooks/validation" versionwebhook "code.cloudfoundry.org/korifi/controllers/webhooks/version" - "code.cloudfoundry.org/korifi/controllers/webhooks/workloads" + appswebhook "code.cloudfoundry.org/korifi/controllers/webhooks/workloads/apps" + orgswebhook "code.cloudfoundry.org/korifi/controllers/webhooks/workloads/orgs" + packageswebhook "code.cloudfoundry.org/korifi/controllers/webhooks/workloads/packages" + spaceswebhook "code.cloudfoundry.org/korifi/controllers/webhooks/workloads/spaces" + taskswebhook "code.cloudfoundry.org/korifi/controllers/webhooks/workloads/tasks" jobtaskrunnercontrollers "code.cloudfoundry.org/korifi/job-task-runner/controllers" "code.cloudfoundry.org/korifi/kpack-image-builder/controllers" kpackimagebuilderfinalizer "code.cloudfoundry.org/korifi/kpack-image-builder/controllers/webhooks/finalizer" @@ -166,48 +178,48 @@ func main() { } buildCleaner := cleanup.NewBuildCleaner(mgr.GetClient(), controllerConfig.MaxRetainedBuildsPerApp) - if err = (workloadscontrollers.NewCFBuildpackBuildReconciler( + if err = buildpack.NewReconciler( mgr.GetClient(), buildCleaner, mgr.GetScheme(), ctrl.Log.WithName("controllers").WithName("CFBuildpackBuild"), controllerConfig, env.NewAppEnvBuilder(mgr.GetClient()), - )).SetupWithManager(mgr); err != nil { + ).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "CFBuildpackBuild") os.Exit(1) } - if err = (workloadscontrollers.NewCFDockerBuildReconciler( + if err = docker.NewReconciler( mgr.GetClient(), buildCleaner, imageClient, mgr.GetScheme(), ctrl.Log.WithName("controllers").WithName("CFDockerBuild"), - )).SetupWithManager(mgr); err != nil { + ).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "CFDockerBuild") os.Exit(1) } - if err = (workloadscontrollers.NewCFPackageReconciler( + if err = packages.NewReconciler( mgr.GetClient(), mgr.GetScheme(), ctrl.Log.WithName("controllers").WithName("CFPackage"), imageClient, cleanup.NewPackageCleaner(mgr.GetClient(), controllerConfig.MaxRetainedPackagesPerApp), controllerConfig.ContainerRegistrySecretNames, - )).SetupWithManager(mgr); err != nil { + ).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "CFPackage") os.Exit(1) } - if err = (workloadscontrollers.NewCFProcessReconciler( + if err = processes.NewReconciler( mgr.GetClient(), mgr.GetScheme(), ctrl.Log.WithName("controllers").WithName("CFProcess"), controllerConfig, env.NewProcessEnvBuilder(mgr.GetClient()), - )).SetupWithManager(mgr); err != nil { + ).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "CFProcess") os.Exit(1) } @@ -237,7 +249,7 @@ func main() { }). Defaults(controllerConfig.NamespaceLabels) - if err = workloadscontrollers.NewCFOrgReconciler( + if err = orgs.NewReconciler( mgr.GetClient(), ctrl.Log.WithName("controllers").WithName("CFOrg"), controllerConfig.ContainerRegistrySecretNames, @@ -247,7 +259,7 @@ func main() { os.Exit(1) } - if err = workloadscontrollers.NewCFSpaceReconciler( + if err = spaces.NewReconciler( mgr.GetClient(), ctrl.Log.WithName("controllers").WithName("CFSpace"), controllerConfig.ContainerRegistrySecretNames, @@ -266,7 +278,7 @@ func main() { os.Exit(1) } - if err = workloadscontrollers.NewCFTaskReconciler( + if err = tasks.NewReconciler( mgr.GetClient(), mgr.GetScheme(), mgr.GetEventRecorderFor("cftask-controller"), @@ -407,7 +419,7 @@ func main() { os.Exit(1) } - (&workloads.AppRevWebhook{}).SetupWebhookWithManager(mgr) + (&appswebhook.AppRevWebhook{}).SetupWebhookWithManager(mgr) if err = (&korifiv1alpha1.CFPackage{}).SetupWebhookWithManager(mgr); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "CFPackage") @@ -435,15 +447,15 @@ func main() { os.Exit(1) } - if err = workloads.NewCFAppValidator( - webhooks.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, workloads.AppEntityType)), + if err = appswebhook.NewValidator( + validation.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, appswebhook.AppEntityType)), ).SetupWebhookWithManager(mgr); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "CFApp") os.Exit(1) } - if err = networking.NewCFRouteValidator( - webhooks.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, networking.RouteEntityType)), + if err = routeswebhook.NewValidator( + validation.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, routeswebhook.RouteEntityType)), controllerConfig.CFRootNamespace, uncachedClient, ).SetupWebhookWithManager(mgr); err != nil { @@ -451,38 +463,38 @@ func main() { os.Exit(1) } - if err = services.NewCFServiceInstanceValidator( - webhooks.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, services.ServiceInstanceEntityType)), + if err = instanceswebhook.NewValidator( + validation.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, instanceswebhook.ServiceInstanceEntityType)), ).SetupWebhookWithManager(mgr); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "CFServiceInstance") os.Exit(1) } - if err = services.NewCFServiceBindingValidator( - webhooks.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, services.ServiceBindingEntityType)), + if err = bindingswebhook.NewCFServiceBindingValidator( + validation.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, bindingswebhook.ServiceBindingEntityType)), ).SetupWebhookWithManager(mgr); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "CFServiceBinding") os.Exit(1) } - if err = networking.NewCFDomainValidator( + if err = domainswebhook.NewValidator( uncachedClient, ).SetupWebhookWithManager(mgr); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "CFDomain") os.Exit(1) } - if err = workloads.NewCFOrgValidator( - webhooks.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, workloads.CFOrgEntityType)), - webhooks.NewPlacementValidator(uncachedClient, controllerConfig.CFRootNamespace), + if err = orgswebhook.NewValidator( + validation.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, orgswebhook.CFOrgEntityType)), + validation.NewPlacementValidator(uncachedClient, controllerConfig.CFRootNamespace), ).SetupWebhookWithManager(mgr); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "CFOrg") os.Exit(1) } - if err = workloads.NewCFSpaceValidator( - webhooks.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, workloads.CFSpaceEntityType)), - webhooks.NewPlacementValidator(uncachedClient, controllerConfig.CFRootNamespace), + if err = spaceswebhook.NewValidator( + validation.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, spaceswebhook.CFSpaceEntityType)), + validation.NewPlacementValidator(uncachedClient, controllerConfig.CFRootNamespace), ).SetupWebhookWithManager(mgr); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "CFSpace") os.Exit(1) @@ -493,12 +505,12 @@ func main() { os.Exit(1) } - if err = workloads.NewCFTaskDefaulter(controllerConfig.CFProcessDefaults).SetupWebhookWithManager(mgr); err != nil { + if err = taskswebhook.NewDefaulter(controllerConfig.CFProcessDefaults).SetupWebhookWithManager(mgr); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "CFTask") os.Exit(1) } - if err = workloads.NewCFTaskValidator().SetupWebhookWithManager(mgr); err != nil { + if err = taskswebhook.NewValidator().SetupWebhookWithManager(mgr); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "CFTask") os.Exit(1) } @@ -506,7 +518,7 @@ func main() { versionwebhook.NewVersionWebhook(version.Version).SetupWebhookWithManager(mgr) controllersfinalizer.NewControllersFinalizerWebhook().SetupWebhookWithManager(mgr) - if err = workloads.NewCFPackageValidator().SetupWebhookWithManager(mgr); err != nil { + if err = packageswebhook.NewValidator().SetupWebhookWithManager(mgr); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "CFPackage") os.Exit(1) } diff --git a/controllers/webhooks/finalizer/suite_integration_test.go b/controllers/webhooks/finalizer/suite_test.go similarity index 68% rename from controllers/webhooks/finalizer/suite_integration_test.go rename to controllers/webhooks/finalizer/suite_test.go index c67366efe..ea4b041d4 100644 --- a/controllers/webhooks/finalizer/suite_integration_test.go +++ b/controllers/webhooks/finalizer/suite_test.go @@ -9,11 +9,15 @@ import ( korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" "code.cloudfoundry.org/korifi/controllers/controllers/shared" "code.cloudfoundry.org/korifi/controllers/coordination" - "code.cloudfoundry.org/korifi/controllers/webhooks" "code.cloudfoundry.org/korifi/controllers/webhooks/finalizer" - "code.cloudfoundry.org/korifi/controllers/webhooks/networking" + "code.cloudfoundry.org/korifi/controllers/webhooks/networking/domains" + "code.cloudfoundry.org/korifi/controllers/webhooks/networking/routes" + "code.cloudfoundry.org/korifi/controllers/webhooks/validation" "code.cloudfoundry.org/korifi/controllers/webhooks/version" - "code.cloudfoundry.org/korifi/controllers/webhooks/workloads" + "code.cloudfoundry.org/korifi/controllers/webhooks/workloads/apps" + "code.cloudfoundry.org/korifi/controllers/webhooks/workloads/orgs" + "code.cloudfoundry.org/korifi/controllers/webhooks/workloads/packages" + "code.cloudfoundry.org/korifi/controllers/webhooks/workloads/spaces" "code.cloudfoundry.org/korifi/tests/helpers" . "github.com/onsi/ginkgo/v2" @@ -47,7 +51,7 @@ func TestWorkloadsWebhooks(t *testing.T) { SetDefaultEventuallyPollingInterval(250 * time.Millisecond) RegisterFailHandler(Fail) - RunSpecs(t, "Workloads Validating Webhooks Integration Test Suite") + RunSpecs(t, "Finalizer Webhook Integration Test Suite") } var _ = BeforeSuite(func() { @@ -82,29 +86,29 @@ var _ = BeforeSuite(func() { Expect((&korifiv1alpha1.CFApp{}).SetupWebhookWithManager(k8sManager)).To(Succeed()) uncachedClient := helpers.NewUncachedClient(k8sManager.GetConfig()) - Expect(workloads.NewCFAppValidator( - webhooks.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, workloads.AppEntityType)), + Expect(apps.NewValidator( + validation.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, apps.AppEntityType)), ).SetupWebhookWithManager(k8sManager)).To(Succeed()) - orgNameDuplicateValidator := webhooks.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, workloads.CFOrgEntityType)) - orgPlacementValidator := webhooks.NewPlacementValidator(uncachedClient, rootNamespace) - Expect(workloads.NewCFOrgValidator(orgNameDuplicateValidator, orgPlacementValidator).SetupWebhookWithManager(k8sManager)).To(Succeed()) + orgNameDuplicateValidator := validation.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, orgs.CFOrgEntityType)) + orgPlacementValidator := validation.NewPlacementValidator(uncachedClient, rootNamespace) + Expect(orgs.NewValidator(orgNameDuplicateValidator, orgPlacementValidator).SetupWebhookWithManager(k8sManager)).To(Succeed()) - spaceNameDuplicateValidator := webhooks.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, workloads.CFSpaceEntityType)) - spacePlacementValidator := webhooks.NewPlacementValidator(uncachedClient, rootNamespace) - Expect(workloads.NewCFSpaceValidator(spaceNameDuplicateValidator, spacePlacementValidator).SetupWebhookWithManager(k8sManager)).To(Succeed()) + spaceNameDuplicateValidator := validation.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, spaces.CFSpaceEntityType)) + spacePlacementValidator := validation.NewPlacementValidator(uncachedClient, rootNamespace) + Expect(spaces.NewValidator(spaceNameDuplicateValidator, spacePlacementValidator).SetupWebhookWithManager(k8sManager)).To(Succeed()) - Expect(networking.NewCFDomainValidator(uncachedClient).SetupWebhookWithManager(k8sManager)).To(Succeed()) + Expect(domains.NewValidator(uncachedClient).SetupWebhookWithManager(k8sManager)).To(Succeed()) Expect((&korifiv1alpha1.CFPackage{}).SetupWebhookWithManager(k8sManager)).To(Succeed()) Expect((&korifiv1alpha1.CFRoute{}).SetupWebhookWithManager(k8sManager)).To(Succeed()) - Expect(networking.NewCFRouteValidator( - webhooks.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, networking.RouteEntityType)), + Expect(routes.NewValidator( + validation.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, routes.RouteEntityType)), rootNamespace, uncachedClient, ).SetupWebhookWithManager(k8sManager)).To(Succeed()) - Expect(workloads.NewCFPackageValidator().SetupWebhookWithManager(k8sManager)).To(Succeed()) + Expect(packages.NewValidator().SetupWebhookWithManager(k8sManager)).To(Succeed()) stopManager = helpers.StartK8sManager(k8sManager) diff --git a/controllers/webhooks/finalizer/finalizer_webhook.go b/controllers/webhooks/finalizer/webhook.go similarity index 100% rename from controllers/webhooks/finalizer/finalizer_webhook.go rename to controllers/webhooks/finalizer/webhook.go diff --git a/controllers/webhooks/finalizer/finalizer_webhook_test.go b/controllers/webhooks/finalizer/webhook_test.go similarity index 100% rename from controllers/webhooks/finalizer/finalizer_webhook_test.go rename to controllers/webhooks/finalizer/webhook_test.go diff --git a/controllers/webhooks/networking/networking_suite_test.go b/controllers/webhooks/networking/domains/suite_test.go similarity index 75% rename from controllers/webhooks/networking/networking_suite_test.go rename to controllers/webhooks/networking/domains/suite_test.go index 61ad37433..5e94f8d4d 100644 --- a/controllers/webhooks/networking/networking_suite_test.go +++ b/controllers/webhooks/networking/domains/suite_test.go @@ -1,4 +1,4 @@ -package networking_test +package domains_test import ( "testing" @@ -13,5 +13,5 @@ func TestNetworking(t *testing.T) { SetDefaultEventuallyPollingInterval(250 * time.Millisecond) RegisterFailHandler(Fail) - RunSpecs(t, "Networking Validating Webhooks Unit Test Suite") + RunSpecs(t, "CFDomain Webhook Unit Test Suite") } diff --git a/controllers/webhooks/networking/cfdomain_validator.go b/controllers/webhooks/networking/domains/validator.go similarity index 77% rename from controllers/webhooks/networking/cfdomain_validator.go rename to controllers/webhooks/networking/domains/validator.go index c6128420a..759517356 100644 --- a/controllers/webhooks/networking/cfdomain_validator.go +++ b/controllers/webhooks/networking/domains/validator.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package networking +package domains import ( "context" @@ -22,7 +22,7 @@ import ( "strings" korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" - "code.cloudfoundry.org/korifi/controllers/webhooks" + validationwebhook "code.cloudfoundry.org/korifi/controllers/webhooks/validation" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" @@ -46,26 +46,26 @@ var log = logf.Log.WithName("domain-validation") //+kubebuilder:webhook:path=/validate-korifi-cloudfoundry-org-v1alpha1-cfdomain,mutating=false,failurePolicy=fail,sideEffects=None,groups=korifi.cloudfoundry.org,resources=cfdomains,verbs=create;update,versions=v1alpha1,name=vcfdomain.korifi.cloudfoundry.org,admissionReviewVersions=v1 -type CFDomainValidator struct { +type Validator struct { client client.Client } -var _ webhook.CustomValidator = &CFDomainValidator{} +var _ webhook.CustomValidator = &Validator{} -func NewCFDomainValidator(client client.Client) *CFDomainValidator { - return &CFDomainValidator{ +func NewValidator(client client.Client) *Validator { + return &Validator{ client: client, } } -func (v *CFDomainValidator) SetupWebhookWithManager(mgr ctrl.Manager) error { +func (v *Validator) SetupWebhookWithManager(mgr ctrl.Manager) error { return ctrl.NewWebhookManagedBy(mgr). For(&korifiv1alpha1.CFDomain{}). WithValidator(v). Complete() } -func (v *CFDomainValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { +func (v *Validator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { domain, ok := obj.(*korifiv1alpha1.CFDomain) if !ok { return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a CFDomain but got a %T", obj)) @@ -73,7 +73,7 @@ func (v *CFDomainValidator) ValidateCreate(ctx context.Context, obj runtime.Obje err := validateDomainName(domain.Spec.Name) if err != nil { - return nil, webhooks.ValidationError{ + return nil, validationwebhook.ValidationError{ Type: InvalidDomainErrorType, Message: fmt.Sprintf("%q is not a valid domain: %s", domain.Spec.Name, err.Error()), }.ExportJSONError() @@ -82,14 +82,14 @@ func (v *CFDomainValidator) ValidateCreate(ctx context.Context, obj runtime.Obje isOverlapping, err := v.domainIsOverlapping(ctx, domain.Spec.Name) if err != nil { log.Info("error checking for overlapping domain", "reason", err) - return nil, webhooks.ValidationError{ - Type: webhooks.UnknownErrorType, - Message: webhooks.UnknownErrorMessage, + return nil, validationwebhook.ValidationError{ + Type: validationwebhook.UnknownErrorType, + Message: validationwebhook.UnknownErrorMessage, }.ExportJSONError() } if isOverlapping { - return nil, webhooks.ValidationError{ + return nil, validationwebhook.ValidationError{ Type: DuplicateDomainErrorType, Message: "Overlapping domain exists", }.ExportJSONError() @@ -102,7 +102,7 @@ func validateDomainName(domainName string) error { return validation.IsFullyQualifiedDomainName(field.NewPath("CFDomain", "Spec", "Name"), domainName).ToAggregate() } -func (v *CFDomainValidator) ValidateUpdate(ctx context.Context, oldObj runtime.Object, obj runtime.Object) (admission.Warnings, error) { +func (v *Validator) ValidateUpdate(ctx context.Context, oldObj runtime.Object, obj runtime.Object) (admission.Warnings, error) { domain, ok := obj.(*korifiv1alpha1.CFDomain) if !ok { return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a CFDomain but got a %T", obj)) @@ -118,20 +118,20 @@ func (v *CFDomainValidator) ValidateUpdate(ctx context.Context, oldObj runtime.O } if oldDomain.Spec.Name != domain.Spec.Name { - return nil, webhooks.ValidationError{ - Type: webhooks.ImmutableFieldErrorType, - Message: fmt.Sprintf(webhooks.ImmutableFieldErrorMessageTemplate, "CFDomain.Spec.Name"), + return nil, validationwebhook.ValidationError{ + Type: validationwebhook.ImmutableFieldErrorType, + Message: fmt.Sprintf(validationwebhook.ImmutableFieldErrorMessageTemplate, "CFDomain.Spec.Name"), }.ExportJSONError() } return nil, nil } -func (v *CFDomainValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { +func (v *Validator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { return nil, nil } -func (v *CFDomainValidator) domainIsOverlapping(ctx context.Context, domainName string) (bool, error) { +func (v *Validator) domainIsOverlapping(ctx context.Context, domainName string) (bool, error) { var existingDomainList korifiv1alpha1.CFDomainList err := v.client.List(ctx, &existingDomainList) if err != nil { diff --git a/controllers/webhooks/networking/cfdomain_validator_test.go b/controllers/webhooks/networking/domains/validator_test.go similarity index 90% rename from controllers/webhooks/networking/cfdomain_validator_test.go rename to controllers/webhooks/networking/domains/validator_test.go index 3c6278042..caa8ce9ab 100644 --- a/controllers/webhooks/networking/cfdomain_validator_test.go +++ b/controllers/webhooks/networking/domains/validator_test.go @@ -1,4 +1,4 @@ -package networking_test +package domains_test import ( "context" @@ -7,8 +7,8 @@ import ( korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" "code.cloudfoundry.org/korifi/controllers/fake" - "code.cloudfoundry.org/korifi/controllers/webhooks" - "code.cloudfoundry.org/korifi/controllers/webhooks/networking" + "code.cloudfoundry.org/korifi/controllers/webhooks/networking/domains" + "code.cloudfoundry.org/korifi/controllers/webhooks/validation" "code.cloudfoundry.org/korifi/tests/matchers" "github.com/google/uuid" @@ -34,7 +34,7 @@ var _ = Describe("CFDomainValidator", func() { listDomainsErr error retErr error - validatingWebhook *networking.CFDomainValidator + validatingWebhook *domains.Validator ) BeforeEach(func() { @@ -63,7 +63,7 @@ var _ = Describe("CFDomainValidator", func() { requestDomainName = "foo.example.com" - validatingWebhook = networking.NewCFDomainValidator(fakeClient) + validatingWebhook = domains.NewValidator(fakeClient) }) Describe("ValidateCreate", func() { @@ -106,7 +106,7 @@ var _ = Describe("CFDomainValidator", func() { It("returns an error", func() { Expect(retErr).To(matchers.BeValidationError( - networking.DuplicateDomainErrorType, + domains.DuplicateDomainErrorType, Equal("Overlapping domain exists"), )) }) @@ -119,7 +119,7 @@ var _ = Describe("CFDomainValidator", func() { It("returns an error", func() { Expect(retErr).To(matchers.BeValidationError( - networking.DuplicateDomainErrorType, + domains.DuplicateDomainErrorType, Equal("Overlapping domain exists"), )) }) @@ -132,8 +132,8 @@ var _ = Describe("CFDomainValidator", func() { It("denies the request", func() { Expect(retErr).To(matchers.BeValidationError( - webhooks.UnknownErrorType, - Equal(webhooks.UnknownErrorMessage), + validation.UnknownErrorType, + Equal(validation.UnknownErrorMessage), )) }) }) @@ -145,7 +145,7 @@ var _ = Describe("CFDomainValidator", func() { It("denies the request", func() { Expect(retErr).To(matchers.BeValidationError( - networking.InvalidDomainErrorType, + domains.InvalidDomainErrorType, ContainSubstring("is not a valid domain"), )) }) @@ -169,7 +169,7 @@ var _ = Describe("CFDomainValidator", func() { It("returns an error", func() { Expect(retErr).To(matchers.BeValidationError( - webhooks.ImmutableFieldErrorType, + validation.ImmutableFieldErrorType, Equal("'CFDomain.Spec.Name' field is immutable"), )) }) diff --git a/controllers/webhooks/networking/package.go b/controllers/webhooks/networking/package.go deleted file mode 100644 index 2802b43e2..000000000 --- a/controllers/webhooks/networking/package.go +++ /dev/null @@ -1,3 +0,0 @@ -package networking - -//go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -generate diff --git a/controllers/webhooks/networking/routes/suite_test.go b/controllers/webhooks/networking/routes/suite_test.go new file mode 100644 index 000000000..6ee27d9e2 --- /dev/null +++ b/controllers/webhooks/networking/routes/suite_test.go @@ -0,0 +1,17 @@ +package routes_test + +import ( + "testing" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestNetworking(t *testing.T) { + SetDefaultEventuallyTimeout(10 * time.Second) + SetDefaultEventuallyPollingInterval(250 * time.Millisecond) + + RegisterFailHandler(Fail) + RunSpecs(t, "CFRoute Webhook Unit Test Suite") +} diff --git a/controllers/webhooks/networking/cfroute_validator.go b/controllers/webhooks/networking/routes/validator.go similarity index 77% rename from controllers/webhooks/networking/cfroute_validator.go rename to controllers/webhooks/networking/routes/validator.go index 56b7e5c8f..a387a3eb2 100644 --- a/controllers/webhooks/networking/cfroute_validator.go +++ b/controllers/webhooks/networking/routes/validator.go @@ -1,4 +1,4 @@ -package networking +package routes import ( "context" @@ -9,6 +9,7 @@ import ( korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" "code.cloudfoundry.org/korifi/controllers/webhooks" + validationwebhook "code.cloudfoundry.org/korifi/controllers/webhooks/validation" "github.com/hashicorp/go-multierror" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -46,34 +47,34 @@ var logger = logf.Log.WithName("route-validation") //+kubebuilder:webhook:path=/validate-korifi-cloudfoundry-org-v1alpha1-cfroute,mutating=false,failurePolicy=fail,sideEffects=NoneOnDryRun,groups=korifi.cloudfoundry.org,resources=cfroutes,verbs=create;update;delete,versions=v1alpha1,name=vcfroute.korifi.cloudfoundry.org,admissionReviewVersions={v1,v1beta1} -type CFRouteValidator struct { +type Validator struct { duplicateValidator webhooks.NameValidator rootNamespace string client client.Client } -var _ webhook.CustomValidator = &CFRouteValidator{} +var _ webhook.CustomValidator = &Validator{} -func NewCFRouteValidator( +func NewValidator( nameValidator webhooks.NameValidator, rootNamespace string, client client.Client, -) *CFRouteValidator { - return &CFRouteValidator{ +) *Validator { + return &Validator{ duplicateValidator: nameValidator, rootNamespace: rootNamespace, client: client, } } -func (v *CFRouteValidator) SetupWebhookWithManager(mgr ctrl.Manager) error { +func (v *Validator) SetupWebhookWithManager(mgr ctrl.Manager) error { return ctrl.NewWebhookManagedBy(mgr). For(&korifiv1alpha1.CFRoute{}). WithValidator(v). Complete() } -func (v *CFRouteValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { +func (v *Validator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { route, ok := obj.(*korifiv1alpha1.CFRoute) if !ok { return nil, apierrors.NewBadRequest(fmt.Sprintf("expected nil, a CFRoute but got a %T", obj)) @@ -89,7 +90,7 @@ func (v *CFRouteValidator) ValidateCreate(ctx context.Context, obj runtime.Objec return nil, v.duplicateValidator.ValidateCreate(ctx, logger, v.rootNamespace, route) } -func (v *CFRouteValidator) ValidateUpdate(ctx context.Context, oldObj, obj runtime.Object) (admission.Warnings, error) { +func (v *Validator) ValidateUpdate(ctx context.Context, oldObj, obj runtime.Object) (admission.Warnings, error) { route, ok := obj.(*korifiv1alpha1.CFRoute) if !ok { return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a CFRoute but got a %T", obj)) @@ -104,27 +105,27 @@ func (v *CFRouteValidator) ValidateUpdate(ctx context.Context, oldObj, obj runti return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a CFRoute but got a %T", obj)) } - immutableError := webhooks.ValidationError{ - Type: webhooks.ImmutableFieldErrorType, + immutableError := validationwebhook.ValidationError{ + Type: validationwebhook.ImmutableFieldErrorType, } if route.Spec.Host != oldRoute.Spec.Host { - immutableError.Message = fmt.Sprintf(webhooks.ImmutableFieldErrorMessageTemplate, "CFRoute.Spec.Host") + immutableError.Message = fmt.Sprintf(validationwebhook.ImmutableFieldErrorMessageTemplate, "CFRoute.Spec.Host") return nil, immutableError.ExportJSONError() } if route.Spec.Path != oldRoute.Spec.Path { - immutableError.Message = fmt.Sprintf(webhooks.ImmutableFieldErrorMessageTemplate, "CFRoute.Spec.Path") + immutableError.Message = fmt.Sprintf(validationwebhook.ImmutableFieldErrorMessageTemplate, "CFRoute.Spec.Path") return nil, immutableError.ExportJSONError() } if route.Spec.Protocol != oldRoute.Spec.Protocol { - immutableError.Message = fmt.Sprintf(webhooks.ImmutableFieldErrorMessageTemplate, "CFRoute.Spec.Protocol") + immutableError.Message = fmt.Sprintf(validationwebhook.ImmutableFieldErrorMessageTemplate, "CFRoute.Spec.Protocol") return nil, immutableError.ExportJSONError() } if route.Spec.DomainRef.Name != oldRoute.Spec.DomainRef.Name { - immutableError.Message = fmt.Sprintf(webhooks.ImmutableFieldErrorMessageTemplate, "CFRoute.Spec.DomainRef.Name") + immutableError.Message = fmt.Sprintf(validationwebhook.ImmutableFieldErrorMessageTemplate, "CFRoute.Spec.DomainRef.Name") return nil, immutableError.ExportJSONError() } @@ -136,7 +137,7 @@ func (v *CFRouteValidator) ValidateUpdate(ctx context.Context, oldObj, obj runti return nil, v.duplicateValidator.ValidateUpdate(ctx, logger, v.rootNamespace, oldRoute, route) } -func (v *CFRouteValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { +func (v *Validator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { route, ok := obj.(*korifiv1alpha1.CFRoute) if !ok { return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a CFRoute but got a %T", obj)) @@ -145,7 +146,7 @@ func (v *CFRouteValidator) ValidateDelete(ctx context.Context, obj runtime.Objec return nil, v.duplicateValidator.ValidateDelete(ctx, logger, v.rootNamespace, route) } -func (v *CFRouteValidator) validateRoute(ctx context.Context, route *korifiv1alpha1.CFRoute) (*korifiv1alpha1.CFDomain, error) { +func (v *Validator) validateRoute(ctx context.Context, route *korifiv1alpha1.CFRoute) (*korifiv1alpha1.CFDomain, error) { domain, err := v.fetchDomain(ctx, route) if err != nil { return domain, err @@ -167,31 +168,31 @@ func (v *CFRouteValidator) validateRoute(ctx context.Context, route *korifiv1alp return domain, nil } -func (v *CFRouteValidator) fetchDomain(ctx context.Context, route *korifiv1alpha1.CFRoute) (*korifiv1alpha1.CFDomain, error) { +func (v *Validator) fetchDomain(ctx context.Context, route *korifiv1alpha1.CFRoute) (*korifiv1alpha1.CFDomain, error) { domain := &korifiv1alpha1.CFDomain{} err := v.client.Get(ctx, types.NamespacedName{Name: route.Spec.DomainRef.Name, Namespace: route.Spec.DomainRef.Namespace}, domain) if err != nil { errMessage := "Error while retrieving CFDomain object" logger.Info(errMessage, "reason", err) - return nil, webhooks.ValidationError{ - Type: webhooks.UnknownErrorType, + return nil, validationwebhook.ValidationError{ + Type: validationwebhook.UnknownErrorType, Message: errMessage, }.ExportJSONError() } return domain, err } -func (v *CFRouteValidator) validateDestinations(ctx context.Context, route *korifiv1alpha1.CFRoute) error { +func (v *Validator) validateDestinations(ctx context.Context, route *korifiv1alpha1.CFRoute) error { err := v.checkDestinationsExistInNamespace(ctx, *route) if err != nil { - validationErr := webhooks.ValidationError{} + validationErr := validationwebhook.ValidationError{} if apierrors.IsNotFound(err) { validationErr.Type = RouteDestinationNotInSpaceErrorType validationErr.Message = RouteDestinationNotInSpaceErrorMessage } else { - validationErr.Type = webhooks.UnknownErrorType - validationErr.Message = webhooks.UnknownErrorMessage + validationErr.Type = validationwebhook.UnknownErrorType + validationErr.Message = validationwebhook.UnknownErrorMessage } logger.Info(validationErr.Message, "reason", err) @@ -206,7 +207,7 @@ func validateFQDN(host, domain string) error { // is either "*" or a valid dns label. The domain webhook already // guarantees that the domain is well formed if len(host+"."+domain) > validation.DNS1123SubdomainMaxLength { - return webhooks.ValidationError{ + return validationwebhook.ValidationError{ Type: RouteSubdomainValidationErrorType, Message: fmt.Sprintf("A valid DNS-1123 subdomain must not exceed %d characters.", validation.DNS1123SubdomainMaxLength), }.ExportJSONError() @@ -215,7 +216,7 @@ func validateFQDN(host, domain string) error { host = strings.ToLower(host) err := validateHost(host) if err != nil { - return webhooks.ValidationError{ + return validationwebhook.ValidationError{ Type: RouteHostNameValidationErrorType, Message: fmt.Sprintf("Host %q is not valid: %s", host, err.Error()), }.ExportJSONError() @@ -270,7 +271,7 @@ func validatePath(path string) error { } if len(errStrings) > 0 { - return webhooks.ValidationError{ + return validationwebhook.ValidationError{ Type: RoutePathValidationErrorType, Message: strings.Join(errStrings, ", "), }.ExportJSONError() @@ -279,7 +280,7 @@ func validatePath(path string) error { return nil } -func (v *CFRouteValidator) checkDestinationsExistInNamespace(ctx context.Context, route korifiv1alpha1.CFRoute) error { +func (v *Validator) checkDestinationsExistInNamespace(ctx context.Context, route korifiv1alpha1.CFRoute) error { for _, destination := range route.Spec.Destinations { err := v.client.Get(ctx, client.ObjectKey{Namespace: route.Namespace, Name: destination.AppRef.Name}, &korifiv1alpha1.CFApp{}) if err != nil { diff --git a/controllers/webhooks/networking/cfroute_validator_test.go b/controllers/webhooks/networking/routes/validator_test.go similarity index 88% rename from controllers/webhooks/networking/cfroute_validator_test.go rename to controllers/webhooks/networking/routes/validator_test.go index 1a322b76a..6bb5bd1ee 100644 --- a/controllers/webhooks/networking/cfroute_validator_test.go +++ b/controllers/webhooks/networking/routes/validator_test.go @@ -1,4 +1,4 @@ -package networking_test +package routes_test import ( "context" @@ -9,9 +9,9 @@ import ( korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" controllerfake "code.cloudfoundry.org/korifi/controllers/fake" - "code.cloudfoundry.org/korifi/controllers/webhooks" "code.cloudfoundry.org/korifi/controllers/webhooks/fake" - "code.cloudfoundry.org/korifi/controllers/webhooks/networking" + "code.cloudfoundry.org/korifi/controllers/webhooks/networking/routes" + validationwebhook "code.cloudfoundry.org/korifi/controllers/webhooks/validation" "code.cloudfoundry.org/korifi/tests/matchers" . "github.com/onsi/ginkgo/v2" @@ -33,7 +33,7 @@ var _ = Describe("CFRouteValidator", func() { cfRoute *korifiv1alpha1.CFRoute cfDomain *korifiv1alpha1.CFDomain cfApp *korifiv1alpha1.CFApp - validatingWebhook *networking.CFRouteValidator + validatingWebhook *routes.Validator testRouteGUID string testRouteNamespace string @@ -102,7 +102,7 @@ var _ = Describe("CFRouteValidator", func() { } } - validatingWebhook = networking.NewCFRouteValidator(duplicateValidator, rootNamespace, fakeClient) + validatingWebhook = routes.NewValidator(duplicateValidator, rootNamespace, fakeClient) }) Describe("ValidateCreate", func() { @@ -144,7 +144,7 @@ var _ = Describe("CFRouteValidator", func() { It("denies the request", func() { Expect(retErr).To(matchers.BeValidationError( - webhooks.UnknownErrorType, + validationwebhook.UnknownErrorType, ContainSubstring("Error while retrieving CFDomain object"), )) }) @@ -157,7 +157,7 @@ var _ = Describe("CFRouteValidator", func() { It("denies the request", func() { Expect(retErr).To(matchers.BeValidationError( - networking.RouteHostNameValidationErrorType, + routes.RouteHostNameValidationErrorType, ContainSubstring("Host \"inval!dname?\" is not valid"), )) }) @@ -183,7 +183,7 @@ var _ = Describe("CFRouteValidator", func() { It("denies the request", func() { Expect(retErr).To(matchers.BeValidationError( - networking.RouteSubdomainValidationErrorType, + routes.RouteSubdomainValidationErrorType, ContainSubstring("subdomain must not exceed 253 characters"), )) }) @@ -196,8 +196,8 @@ var _ = Describe("CFRouteValidator", func() { It("denies the request", func() { Expect(retErr).To(matchers.BeValidationError( - networking.RoutePathValidationErrorType, - Equal(networking.InvalidURIError), + routes.RoutePathValidationErrorType, + Equal(routes.InvalidURIError), )) }) }) @@ -209,8 +209,8 @@ var _ = Describe("CFRouteValidator", func() { It("denies the request", func() { Expect(retErr).To(matchers.BeValidationError( - networking.RoutePathValidationErrorType, - Equal(networking.PathIsSlashError), + routes.RoutePathValidationErrorType, + Equal(routes.PathIsSlashError), )) }) }) @@ -222,8 +222,8 @@ var _ = Describe("CFRouteValidator", func() { It("denies the request", func() { Expect(retErr).To(matchers.BeValidationError( - networking.RoutePathValidationErrorType, - Equal(networking.InvalidURIError), + routes.RoutePathValidationErrorType, + Equal(routes.InvalidURIError), )) }) }) @@ -235,8 +235,8 @@ var _ = Describe("CFRouteValidator", func() { It("denies the request", func() { Expect(retErr).To(matchers.BeValidationError( - networking.RoutePathValidationErrorType, - Equal(networking.PathHasQuestionMarkError), + routes.RoutePathValidationErrorType, + Equal(routes.PathHasQuestionMarkError), )) }) }) @@ -248,8 +248,8 @@ var _ = Describe("CFRouteValidator", func() { It("denies the request", func() { Expect(retErr).To(matchers.BeValidationError( - networking.RoutePathValidationErrorType, - Equal(networking.PathLengthExceededError), + routes.RoutePathValidationErrorType, + Equal(routes.PathLengthExceededError), )) }) }) @@ -276,8 +276,8 @@ var _ = Describe("CFRouteValidator", func() { It("denies the request", func() { Expect(retErr).To(matchers.BeValidationError( - networking.RouteDestinationNotInSpaceErrorType, - Equal(networking.RouteDestinationNotInSpaceErrorMessage), + routes.RouteDestinationNotInSpaceErrorType, + Equal(routes.RouteDestinationNotInSpaceErrorMessage), )) }) }) @@ -289,8 +289,8 @@ var _ = Describe("CFRouteValidator", func() { It("denies the request", func() { Expect(retErr).To(matchers.BeValidationError( - webhooks.UnknownErrorType, - Equal(webhooks.UnknownErrorMessage), + validationwebhook.UnknownErrorType, + Equal(validationwebhook.UnknownErrorMessage), )) }) }) @@ -349,7 +349,7 @@ var _ = Describe("CFRouteValidator", func() { It("denies the request", func() { Expect(retErr).To(matchers.BeValidationError( - webhooks.ImmutableFieldErrorType, + validationwebhook.ImmutableFieldErrorType, Equal("'CFRoute.Spec.Host' field is immutable"), )) }) @@ -372,7 +372,7 @@ var _ = Describe("CFRouteValidator", func() { It("denies the request", func() { Expect(retErr).To(matchers.BeValidationError( - webhooks.ImmutableFieldErrorType, + validationwebhook.ImmutableFieldErrorType, Equal("'CFRoute.Spec.Path' field is immutable"), )) }) @@ -385,7 +385,7 @@ var _ = Describe("CFRouteValidator", func() { It("denies the request", func() { Expect(retErr).To(matchers.BeValidationError( - webhooks.ImmutableFieldErrorType, + validationwebhook.ImmutableFieldErrorType, Equal("'CFRoute.Spec.Protocol' field is immutable"), )) }) @@ -398,7 +398,7 @@ var _ = Describe("CFRouteValidator", func() { It("denies the request", func() { Expect(retErr).To(matchers.BeValidationError( - webhooks.ImmutableFieldErrorType, + validationwebhook.ImmutableFieldErrorType, Equal("'CFRoute.Spec.DomainRef.Name' field is immutable"), )) }) @@ -411,8 +411,8 @@ var _ = Describe("CFRouteValidator", func() { It("denies the request", func() { Expect(retErr).To(matchers.BeValidationError( - networking.RouteDestinationNotInSpaceErrorType, - Equal(networking.RouteDestinationNotInSpaceErrorMessage), + routes.RouteDestinationNotInSpaceErrorType, + Equal(routes.RouteDestinationNotInSpaceErrorMessage), )) }) }) @@ -424,8 +424,8 @@ var _ = Describe("CFRouteValidator", func() { It("denies the request", func() { Expect(retErr).To(matchers.BeValidationError( - webhooks.UnknownErrorType, - Equal(webhooks.UnknownErrorMessage), + validationwebhook.UnknownErrorType, + Equal(validationwebhook.UnknownErrorMessage), )) }) }) diff --git a/controllers/webhooks/services/suite_test.go b/controllers/webhooks/services/bindings/suite_test.go similarity index 67% rename from controllers/webhooks/services/suite_test.go rename to controllers/webhooks/services/bindings/suite_test.go index 194e31410..bdfcb56f7 100644 --- a/controllers/webhooks/services/suite_test.go +++ b/controllers/webhooks/services/bindings/suite_test.go @@ -1,12 +1,9 @@ -package services_test +package bindings_test import ( - "fmt" "testing" "time" - "github.com/google/uuid" - . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" logf "sigs.k8s.io/controller-runtime/pkg/log" @@ -18,15 +15,9 @@ func TestServicesValidatingWebhooks(t *testing.T) { SetDefaultEventuallyPollingInterval(250 * time.Millisecond) RegisterFailHandler(Fail) - RunSpecs(t, "Services Validating Webhooks Unit Test Suite") + RunSpecs(t, "CFServiceBinding Webhook Unit Test Suite") } var _ = BeforeSuite(func() { logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) }) - -func generateGUID(prefix string) string { - guid := uuid.NewString() - - return fmt.Sprintf("%s-%s", prefix, guid[:13]) -} diff --git a/controllers/webhooks/services/cfservicebinding_validator.go b/controllers/webhooks/services/bindings/validator.go similarity index 88% rename from controllers/webhooks/services/cfservicebinding_validator.go rename to controllers/webhooks/services/bindings/validator.go index fce5591a0..29991a180 100644 --- a/controllers/webhooks/services/cfservicebinding_validator.go +++ b/controllers/webhooks/services/bindings/validator.go @@ -1,4 +1,4 @@ -package services +package bindings import ( "context" @@ -6,6 +6,7 @@ import ( korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" "code.cloudfoundry.org/korifi/controllers/webhooks" + validation "code.cloudfoundry.org/korifi/controllers/webhooks/validation" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" @@ -69,15 +70,15 @@ func (v *CFServiceBindingValidator) ValidateUpdate(ctx context.Context, oldObj, } if oldServiceBinding.Spec.AppRef.Name != serviceBinding.Spec.AppRef.Name { - return nil, webhooks.ValidationError{Type: ServiceBindingErrorType, Message: "AppRef.Name is immutable"} + return nil, validation.ValidationError{Type: ServiceBindingErrorType, Message: "AppRef.Name is immutable"} } if oldServiceBinding.Spec.Service.Name != serviceBinding.Spec.Service.Name { - return nil, webhooks.ValidationError{Type: ServiceBindingErrorType, Message: "Service.Name is immutable"} + return nil, validation.ValidationError{Type: ServiceBindingErrorType, Message: "Service.Name is immutable"} } if oldServiceBinding.Spec.Service.Namespace != serviceBinding.Spec.Service.Namespace { - return nil, webhooks.ValidationError{Type: ServiceBindingErrorType, Message: "Service.Namespace is immutable"} + return nil, validation.ValidationError{Type: ServiceBindingErrorType, Message: "Service.Namespace is immutable"} } return nil, nil diff --git a/controllers/webhooks/services/cfservicebinding_validator_test.go b/controllers/webhooks/services/bindings/validator_test.go similarity index 92% rename from controllers/webhooks/services/cfservicebinding_validator_test.go rename to controllers/webhooks/services/bindings/validator_test.go index fb1f5b3d6..45b909f5e 100644 --- a/controllers/webhooks/services/cfservicebinding_validator_test.go +++ b/controllers/webhooks/services/bindings/validator_test.go @@ -1,4 +1,4 @@ -package services_test +package bindings_test import ( "context" @@ -7,8 +7,9 @@ import ( korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" "code.cloudfoundry.org/korifi/controllers/webhooks/fake" - "code.cloudfoundry.org/korifi/controllers/webhooks/services" + "code.cloudfoundry.org/korifi/controllers/webhooks/services/bindings" + "github.com/google/uuid" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" v1 "k8s.io/api/core/v1" @@ -24,11 +25,10 @@ var _ = Describe("CFServiceBindingValidatingWebhook", func() { var ( appGUID string serviceInstanceGUID string - serviceBindingGUID string ctx context.Context duplicateValidator *fake.NameValidator serviceBinding *korifiv1alpha1.CFServiceBinding - validatingWebhook *services.CFServiceBindingValidator + validatingWebhook *bindings.CFServiceBindingValidator retErr error ) @@ -39,12 +39,11 @@ var _ = Describe("CFServiceBindingValidatingWebhook", func() { err := korifiv1alpha1.AddToScheme(scheme) Expect(err).NotTo(HaveOccurred()) - appGUID = generateGUID("app") - serviceInstanceGUID = generateGUID("service-instance") - serviceBindingGUID = generateGUID("service-binding") + appGUID = uuid.NewString() + serviceInstanceGUID = uuid.NewString() serviceBinding = &korifiv1alpha1.CFServiceBinding{ ObjectMeta: metav1.ObjectMeta{ - Name: serviceBindingGUID, + Name: uuid.NewString(), Namespace: defaultNamespace, }, Spec: korifiv1alpha1.CFServiceBindingSpec{ @@ -59,7 +58,7 @@ var _ = Describe("CFServiceBindingValidatingWebhook", func() { } duplicateValidator = new(fake.NameValidator) - validatingWebhook = services.NewCFServiceBindingValidator(duplicateValidator) + validatingWebhook = bindings.NewCFServiceBindingValidator(duplicateValidator) }) Describe("ValidateCreate", func() { diff --git a/controllers/webhooks/services/instances/suite_test.go b/controllers/webhooks/services/instances/suite_test.go new file mode 100644 index 000000000..c622cccdd --- /dev/null +++ b/controllers/webhooks/services/instances/suite_test.go @@ -0,0 +1,23 @@ +package instances_test + +import ( + "testing" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" +) + +func TestServicesValidatingWebhooks(t *testing.T) { + SetDefaultEventuallyTimeout(10 * time.Second) + SetDefaultEventuallyPollingInterval(250 * time.Millisecond) + + RegisterFailHandler(Fail) + RunSpecs(t, "CFServiceInstance Webhooks Unit Test Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) +}) diff --git a/controllers/webhooks/services/cfserviceinstance_validator.go b/controllers/webhooks/services/instances/validator.go similarity index 75% rename from controllers/webhooks/services/cfserviceinstance_validator.go rename to controllers/webhooks/services/instances/validator.go index 74b5555be..bb23daae6 100644 --- a/controllers/webhooks/services/cfserviceinstance_validator.go +++ b/controllers/webhooks/services/instances/validator.go @@ -1,4 +1,4 @@ -package services +package instances import ( "context" @@ -23,26 +23,26 @@ var cfserviceinstancelog = logf.Log.WithName("cfserviceinstance-validate") //+kubebuilder:webhook:path=/validate-korifi-cloudfoundry-org-v1alpha1-cfserviceinstance,mutating=false,failurePolicy=fail,sideEffects=NoneOnDryRun,groups=korifi.cloudfoundry.org,resources=cfserviceinstances,verbs=create;update;delete,versions=v1alpha1,name=vcfserviceinstance.korifi.cloudfoundry.org,admissionReviewVersions={v1,v1beta1} -type CFServiceInstanceValidator struct { +type Validator struct { duplicateValidator webhooks.NameValidator } -var _ webhook.CustomValidator = &CFServiceInstanceValidator{} +var _ webhook.CustomValidator = &Validator{} -func NewCFServiceInstanceValidator(duplicateValidator webhooks.NameValidator) *CFServiceInstanceValidator { - return &CFServiceInstanceValidator{ +func NewValidator(duplicateValidator webhooks.NameValidator) *Validator { + return &Validator{ duplicateValidator: duplicateValidator, } } -func (v *CFServiceInstanceValidator) SetupWebhookWithManager(mgr ctrl.Manager) error { +func (v *Validator) SetupWebhookWithManager(mgr ctrl.Manager) error { return ctrl.NewWebhookManagedBy(mgr). For(&korifiv1alpha1.CFServiceInstance{}). WithValidator(v). Complete() } -func (v *CFServiceInstanceValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { +func (v *Validator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { serviceInstance, ok := obj.(*korifiv1alpha1.CFServiceInstance) if !ok { return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a CFServiceInstance but got a %T", obj)) @@ -51,7 +51,7 @@ func (v *CFServiceInstanceValidator) ValidateCreate(ctx context.Context, obj run return nil, v.duplicateValidator.ValidateCreate(ctx, cfserviceinstancelog, serviceInstance.Namespace, serviceInstance) } -func (v *CFServiceInstanceValidator) ValidateUpdate(ctx context.Context, oldObj, obj runtime.Object) (admission.Warnings, error) { +func (v *Validator) ValidateUpdate(ctx context.Context, oldObj, obj runtime.Object) (admission.Warnings, error) { serviceInstance, ok := obj.(*korifiv1alpha1.CFServiceInstance) if !ok { return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a CFServiceInstance but got a %T", obj)) @@ -69,7 +69,7 @@ func (v *CFServiceInstanceValidator) ValidateUpdate(ctx context.Context, oldObj, return nil, v.duplicateValidator.ValidateUpdate(ctx, cfserviceinstancelog, serviceInstance.Namespace, oldServiceInstance, serviceInstance) } -func (v *CFServiceInstanceValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { +func (v *Validator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { serviceInstance, ok := obj.(*korifiv1alpha1.CFServiceInstance) if !ok { return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a CFServiceInstance but got a %T", obj)) diff --git a/controllers/webhooks/services/cfserviceinstance_validator_test.go b/controllers/webhooks/services/instances/validator_test.go similarity index 86% rename from controllers/webhooks/services/cfserviceinstance_validator_test.go rename to controllers/webhooks/services/instances/validator_test.go index e463d775f..3c432f398 100644 --- a/controllers/webhooks/services/cfserviceinstance_validator_test.go +++ b/controllers/webhooks/services/instances/validator_test.go @@ -1,4 +1,4 @@ -package services_test +package instances_test import ( "context" @@ -7,8 +7,9 @@ import ( korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" "code.cloudfoundry.org/korifi/controllers/webhooks/fake" - "code.cloudfoundry.org/korifi/controllers/webhooks/services" + "code.cloudfoundry.org/korifi/controllers/webhooks/services/instances" + "github.com/google/uuid" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -21,13 +22,11 @@ var _ = Describe("CFServiceInstanceValidatingWebhook", func() { ) var ( - serviceInstanceGUID string - serviceInstanceName string - ctx context.Context - duplicateValidator *fake.NameValidator - serviceInstance *korifiv1alpha1.CFServiceInstance - validatingWebhook *services.CFServiceInstanceValidator - retErr error + ctx context.Context + duplicateValidator *fake.NameValidator + serviceInstance *korifiv1alpha1.CFServiceInstance + validatingWebhook *instances.Validator + retErr error ) BeforeEach(func() { @@ -37,21 +36,19 @@ var _ = Describe("CFServiceInstanceValidatingWebhook", func() { err := korifiv1alpha1.AddToScheme(scheme) Expect(err).NotTo(HaveOccurred()) - serviceInstanceName = generateGUID("service-instance") - serviceInstanceGUID = generateGUID("service-instance") serviceInstance = &korifiv1alpha1.CFServiceInstance{ ObjectMeta: metav1.ObjectMeta{ - Name: serviceInstanceGUID, + Name: uuid.NewString(), Namespace: defaultNamespace, }, Spec: korifiv1alpha1.CFServiceInstanceSpec{ - DisplayName: serviceInstanceName, + DisplayName: uuid.NewString(), Type: korifiv1alpha1.UserProvidedType, }, } duplicateValidator = new(fake.NameValidator) - validatingWebhook = services.NewCFServiceInstanceValidator(duplicateValidator) + validatingWebhook = instances.NewValidator(duplicateValidator) }) Describe("ValidateCreate", func() { diff --git a/controllers/webhooks/shared.go b/controllers/webhooks/shared.go index dae1f4a0f..c2f7468f0 100644 --- a/controllers/webhooks/shared.go +++ b/controllers/webhooks/shared.go @@ -5,6 +5,15 @@ import ( korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" "github.com/go-logr/logr" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + MaxLabelLength = 63 + + ImmutableFieldModificationErrorType = "ImmutableFieldModificationError" + MissingRequredFieldErrorType = "MissingRequiredFieldError" + InvalidFieldValueErrorType = "InvalidFieldValueError" ) //go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -generate @@ -22,3 +31,22 @@ type NamespaceValidator interface { ValidateOrgCreate(org korifiv1alpha1.CFOrg) error ValidateSpaceCreate(space korifiv1alpha1.CFSpace) error } + +//go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -generate +//counterfeiter:generate -o fake -fake-name NameRegistry . NameRegistry + +type NameRegistry interface { + RegisterName(ctx context.Context, namespace, name, ownerNamespace, ownerName string) error + DeregisterName(ctx context.Context, namespace, name string) error + TryLockName(ctx context.Context, namespace, name string) error + UnlockName(ctx context.Context, namespace, name string) error + CheckNameOwnership(ctx context.Context, namespace, name, ownerNamespace, ownerName string) (bool, error) +} + +//counterfeiter:generate -o fake -fake-name UniqueClientObject . UniqueClientObject + +type UniqueClientObject interface { + client.Object + UniqueName() string + UniqueValidationErrorMessage() string +} diff --git a/controllers/webhooks/cf_validation_errors.go b/controllers/webhooks/validation/cf_validation_errors.go similarity index 98% rename from controllers/webhooks/cf_validation_errors.go rename to controllers/webhooks/validation/cf_validation_errors.go index 0d5e1d42c..9b2294a27 100644 --- a/controllers/webhooks/cf_validation_errors.go +++ b/controllers/webhooks/validation/cf_validation_errors.go @@ -1,4 +1,4 @@ -package webhooks +package validation import ( "encoding/json" diff --git a/controllers/webhooks/cf_validation_errors_test.go b/controllers/webhooks/validation/cf_validation_errors_test.go similarity index 83% rename from controllers/webhooks/cf_validation_errors_test.go rename to controllers/webhooks/validation/cf_validation_errors_test.go index d6c541d9b..05257a78b 100644 --- a/controllers/webhooks/cf_validation_errors_test.go +++ b/controllers/webhooks/validation/cf_validation_errors_test.go @@ -1,9 +1,9 @@ -package webhooks_test +package validation_test import ( "errors" - . "code.cloudfoundry.org/korifi/controllers/webhooks" + "code.cloudfoundry.org/korifi/controllers/webhooks/validation" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -14,12 +14,12 @@ import ( var _ = Describe("CFWebhookValidationError", func() { var ( validationErrorType, validationErrorMessage string - validationErr ValidationError + validationErr validation.ValidationError ) BeforeEach(func() { validationErrorType = "some-validation-error-type" validationErrorMessage = "some validation error message" - validationErr = ValidationError{ + validationErr = validation.ValidationError{ Type: validationErrorType, Message: validationErrorMessage, } @@ -51,7 +51,7 @@ var _ = Describe("WebhookErrorToValidationError", func() { var ( validationErrorType, validationErrorMessage string inputErr error - validationErr ValidationError + validationErr validation.ValidationError isValidationError bool ) @@ -67,12 +67,12 @@ var _ = Describe("WebhookErrorToValidationError", func() { }) JustBeforeEach(func() { - validationErr, isValidationError = WebhookErrorToValidationError(inputErr) + validationErr, isValidationError = validation.WebhookErrorToValidationError(inputErr) }) It("unmarshals a K8s-wrapped validation error into a ValidationError, and returns true", func() { Expect(isValidationError).To(BeTrue()) - Expect(validationErr).To(Equal(ValidationError{ + Expect(validationErr).To(Equal(validation.ValidationError{ Type: validationErrorType, Message: validationErrorMessage, })) @@ -85,7 +85,7 @@ var _ = Describe("WebhookErrorToValidationError", func() { It("returns an empty ValidationError and false", func() { Expect(isValidationError).To(BeFalse()) - Expect(validationErr).To(Equal(ValidationError{})) + Expect(validationErr).To(Equal(validation.ValidationError{})) }) }) }) diff --git a/controllers/webhooks/duplicate_validator.go b/controllers/webhooks/validation/duplicate_validator.go similarity index 75% rename from controllers/webhooks/duplicate_validator.go rename to controllers/webhooks/validation/duplicate_validator.go index 22cc638c6..bbd905199 100644 --- a/controllers/webhooks/duplicate_validator.go +++ b/controllers/webhooks/validation/duplicate_validator.go @@ -1,8 +1,9 @@ -package webhooks +package validation import ( "context" + "code.cloudfoundry.org/korifi/controllers/webhooks" "github.com/go-logr/logr" k8serrors "k8s.io/apimachinery/pkg/api/errors" "sigs.k8s.io/controller-runtime/pkg/client" @@ -10,36 +11,17 @@ import ( const DuplicateNameErrorType = "DuplicateNameError" -//go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -generate -//counterfeiter:generate -o fake -fake-name NameRegistry . NameRegistry - -type NameRegistry interface { - RegisterName(ctx context.Context, namespace, name, ownerNamespace, ownerName string) error - DeregisterName(ctx context.Context, namespace, name string) error - TryLockName(ctx context.Context, namespace, name string) error - UnlockName(ctx context.Context, namespace, name string) error - CheckNameOwnership(ctx context.Context, namespace, name, ownerNamespace, ownerName string) (bool, error) -} - -//counterfeiter:generate -o fake -fake-name UniqueClientObject . UniqueClientObject - -type UniqueClientObject interface { - client.Object - UniqueName() string - UniqueValidationErrorMessage() string -} - type DuplicateValidator struct { - nameRegistry NameRegistry + nameRegistry webhooks.NameRegistry } -func NewDuplicateValidator(nameRegistry NameRegistry) *DuplicateValidator { +func NewDuplicateValidator(nameRegistry webhooks.NameRegistry) *DuplicateValidator { return &DuplicateValidator{ nameRegistry: nameRegistry, } } -func (v DuplicateValidator) ValidateCreate(ctx context.Context, logger logr.Logger, namespace string, obj UniqueClientObject) error { +func (v DuplicateValidator) ValidateCreate(ctx context.Context, logger logr.Logger, namespace string, obj webhooks.UniqueClientObject) error { logger = logger.WithName("duplicateValidator.ValidateCreate") err := v.nameRegistry.RegisterName(ctx, namespace, obj.UniqueName(), obj.GetNamespace(), obj.GetName()) if err != nil { @@ -59,7 +41,7 @@ func (v DuplicateValidator) ValidateCreate(ctx context.Context, logger logr.Logg return nil } -func (v DuplicateValidator) ValidateUpdate(ctx context.Context, logger logr.Logger, namespace string, oldObj, obj UniqueClientObject) error { +func (v DuplicateValidator) ValidateUpdate(ctx context.Context, logger logr.Logger, namespace string, oldObj, obj webhooks.UniqueClientObject) error { if oldObj.UniqueName() == obj.UniqueName() { return nil } @@ -129,7 +111,7 @@ func (v DuplicateValidator) ValidateUpdate(ctx context.Context, logger logr.Logg return nil } -func (v DuplicateValidator) ValidateDelete(ctx context.Context, logger logr.Logger, namespace string, obj UniqueClientObject) error { +func (v DuplicateValidator) ValidateDelete(ctx context.Context, logger logr.Logger, namespace string, obj webhooks.UniqueClientObject) error { logger = logger.WithName("duplicateValidator.ValidateDelete") err := v.nameRegistry.DeregisterName(ctx, namespace, obj.UniqueName()) if err != nil { @@ -149,7 +131,7 @@ func (v DuplicateValidator) ValidateDelete(ctx context.Context, logger logr.Logg return nil } -func duplicateError(obj UniqueClientObject) error { +func duplicateError(obj webhooks.UniqueClientObject) error { return ValidationError{ Type: DuplicateNameErrorType, Message: obj.UniqueValidationErrorMessage(), @@ -164,4 +146,4 @@ func unknownError() error { } // check interface is implemented correctly -var _ NameValidator = DuplicateValidator{} +var _ webhooks.NameValidator = DuplicateValidator{} diff --git a/controllers/webhooks/duplicate_validator_test.go b/controllers/webhooks/validation/duplicate_validator_test.go similarity index 91% rename from controllers/webhooks/duplicate_validator_test.go rename to controllers/webhooks/validation/duplicate_validator_test.go index cedd60980..b077f1b1f 100644 --- a/controllers/webhooks/duplicate_validator_test.go +++ b/controllers/webhooks/validation/duplicate_validator_test.go @@ -1,4 +1,4 @@ -package webhooks_test +package validation_test import ( "context" @@ -6,8 +6,8 @@ import ( "fmt" korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" - "code.cloudfoundry.org/korifi/controllers/webhooks" "code.cloudfoundry.org/korifi/controllers/webhooks/fake" + "code.cloudfoundry.org/korifi/controllers/webhooks/validation" "code.cloudfoundry.org/korifi/tests/matchers" "github.com/go-logr/logr" @@ -24,7 +24,7 @@ var _ = Describe("DuplicateValidator", func() { ctx context.Context nameRegistry *fake.NameRegistry logger logr.Logger - duplicateValidator *webhooks.DuplicateValidator + duplicateValidator *validation.DuplicateValidator validationErr error uniqueClientObj *fake.UniqueClientObject ) @@ -36,7 +36,7 @@ var _ = Describe("DuplicateValidator", func() { Expect(korifiv1alpha1.AddToScheme(scheme)).To(Succeed()) nameRegistry = new(fake.NameRegistry) - duplicateValidator = webhooks.NewDuplicateValidator(nameRegistry) + duplicateValidator = validation.NewDuplicateValidator(nameRegistry) logger = logf.Log uniqueClientObj = new(fake.UniqueClientObject) @@ -71,7 +71,7 @@ var _ = Describe("DuplicateValidator", func() { It("fails", func() { Expect(validationErr).To(matchers.BeValidationError( - webhooks.DuplicateNameErrorType, + validation.DuplicateNameErrorType, Equal("uniqueness-error"), )) }) @@ -84,8 +84,8 @@ var _ = Describe("DuplicateValidator", func() { It("returns the error", func() { Expect(validationErr).To(matchers.BeValidationError( - webhooks.UnknownErrorType, - Equal(webhooks.UnknownErrorMessage), + validation.UnknownErrorType, + Equal(validation.UnknownErrorMessage), )) }) }) @@ -148,8 +148,8 @@ var _ = Describe("DuplicateValidator", func() { It("fails", func() { Expect(validationErr).To(matchers.BeValidationError( - webhooks.UnknownErrorType, - Equal(webhooks.UnknownErrorMessage), + validation.UnknownErrorType, + Equal(validation.UnknownErrorMessage), )) Expect(nameRegistry.RegisterNameCallCount()).To(Equal(0)) }) @@ -184,8 +184,8 @@ var _ = Describe("DuplicateValidator", func() { It("rejects the request", func() { Expect(validationErr).To(matchers.BeValidationError( - webhooks.UnknownErrorType, - Equal(webhooks.UnknownErrorMessage), + validation.UnknownErrorType, + Equal(validation.UnknownErrorMessage), )) }) }) @@ -197,8 +197,8 @@ var _ = Describe("DuplicateValidator", func() { It("rejects the request", func() { Expect(validationErr).To(matchers.BeValidationError( - webhooks.UnknownErrorType, - Equal(webhooks.UnknownErrorMessage), + validation.UnknownErrorType, + Equal(validation.UnknownErrorMessage), )) }) }) @@ -212,7 +212,7 @@ var _ = Describe("DuplicateValidator", func() { It("fails", func() { Expect(validationErr).To(matchers.BeValidationError( - webhooks.DuplicateNameErrorType, + validation.DuplicateNameErrorType, Equal("new-uniqueness-error"), )) }) @@ -225,8 +225,8 @@ var _ = Describe("DuplicateValidator", func() { It("denies the request", func() { Expect(validationErr).To(matchers.BeValidationError( - webhooks.UnknownErrorType, - Equal(webhooks.UnknownErrorMessage), + validation.UnknownErrorType, + Equal(validation.UnknownErrorMessage), )) }) @@ -244,8 +244,8 @@ var _ = Describe("DuplicateValidator", func() { It("fails with the register error", func() { Expect(validationErr).To(matchers.BeValidationError( - webhooks.UnknownErrorType, - Equal(webhooks.UnknownErrorMessage), + validation.UnknownErrorType, + Equal(validation.UnknownErrorMessage), )) }) }) @@ -295,8 +295,8 @@ var _ = Describe("DuplicateValidator", func() { It("fails", func() { Expect(validationErr).To(matchers.BeValidationError( - webhooks.UnknownErrorType, - Equal(webhooks.UnknownErrorMessage), + validation.UnknownErrorType, + Equal(validation.UnknownErrorMessage), )) }) }) diff --git a/controllers/webhooks/placement_validator.go b/controllers/webhooks/validation/placement_validator.go similarity index 98% rename from controllers/webhooks/placement_validator.go rename to controllers/webhooks/validation/placement_validator.go index 6a7f46462..e3458c817 100644 --- a/controllers/webhooks/placement_validator.go +++ b/controllers/webhooks/validation/placement_validator.go @@ -1,4 +1,4 @@ -package webhooks +package validation import ( "context" diff --git a/controllers/webhooks/placement_validator_test.go b/controllers/webhooks/validation/placement_validator_test.go similarity index 81% rename from controllers/webhooks/placement_validator_test.go rename to controllers/webhooks/validation/placement_validator_test.go index 69006014f..54b75883b 100644 --- a/controllers/webhooks/placement_validator_test.go +++ b/controllers/webhooks/validation/placement_validator_test.go @@ -1,4 +1,4 @@ -package webhooks_test +package validation_test import ( "errors" @@ -6,7 +6,7 @@ import ( korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" "code.cloudfoundry.org/korifi/controllers/fake" - "code.cloudfoundry.org/korifi/controllers/webhooks" + "code.cloudfoundry.org/korifi/controllers/webhooks/validation" "code.cloudfoundry.org/korifi/tests/matchers" . "github.com/onsi/ginkgo/v2" @@ -19,7 +19,7 @@ import ( var _ = Describe("CFPlacementValidation", func() { var ( fakeClient *fake.Client - placementValidator *webhooks.PlacementValidator + placementValidator *validation.PlacementValidator validationErr error rootNamespace string @@ -34,7 +34,7 @@ var _ = Describe("CFPlacementValidation", func() { scheme := runtime.NewScheme() Expect(korifiv1alpha1.AddToScheme(scheme)).To(Succeed()) - placementValidator = webhooks.NewPlacementValidator(fakeClient, rootNamespace) + placementValidator = validation.NewPlacementValidator(fakeClient, rootNamespace) }) Describe("ValidateOrgCreate", func() { @@ -67,8 +67,8 @@ var _ = Describe("CFPlacementValidation", func() { It("fails", func() { Expect(validationErr).To(matchers.BeValidationError( - webhooks.OrgPlacementErrorType, - Equal(fmt.Sprintf(webhooks.OrgPlacementErrorMessage, org.Spec.DisplayName)), + validation.OrgPlacementErrorType, + Equal(fmt.Sprintf(validation.OrgPlacementErrorMessage, org.Spec.DisplayName)), )) }) }) @@ -102,8 +102,8 @@ var _ = Describe("CFPlacementValidation", func() { It("fails", func() { Expect(validationErr).To(matchers.BeValidationError( - webhooks.SpacePlacementErrorType, - Equal(fmt.Sprintf(webhooks.SpacePlacementErrorMessage, "org-ns", space.Spec.DisplayName)), + validation.SpacePlacementErrorType, + Equal(fmt.Sprintf(validation.SpacePlacementErrorMessage, "org-ns", space.Spec.DisplayName)), )) }) }) diff --git a/controllers/webhooks/webhooks_suite_test.go b/controllers/webhooks/validation/suite_test.go similarity index 80% rename from controllers/webhooks/webhooks_suite_test.go rename to controllers/webhooks/validation/suite_test.go index ee53f335b..e2bed4118 100644 --- a/controllers/webhooks/webhooks_suite_test.go +++ b/controllers/webhooks/validation/suite_test.go @@ -1,4 +1,4 @@ -package webhooks_test +package validation_test import ( "testing" @@ -13,5 +13,5 @@ func TestWebhooks(t *testing.T) { SetDefaultEventuallyPollingInterval(250 * time.Millisecond) RegisterFailHandler(Fail) - RunSpecs(t, "Webhooks Suite") + RunSpecs(t, "Webhook Validation Suite") } diff --git a/controllers/webhooks/version/suite_integration_test.go b/controllers/webhooks/version/suite_test.go similarity index 60% rename from controllers/webhooks/version/suite_integration_test.go rename to controllers/webhooks/version/suite_test.go index 383467702..39532898d 100644 --- a/controllers/webhooks/version/suite_integration_test.go +++ b/controllers/webhooks/version/suite_test.go @@ -9,14 +9,20 @@ import ( korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" "code.cloudfoundry.org/korifi/controllers/controllers/shared" "code.cloudfoundry.org/korifi/controllers/coordination" - "code.cloudfoundry.org/korifi/controllers/webhooks" "code.cloudfoundry.org/korifi/tests/helpers" "code.cloudfoundry.org/korifi/controllers/webhooks/finalizer" - "code.cloudfoundry.org/korifi/controllers/webhooks/networking" - "code.cloudfoundry.org/korifi/controllers/webhooks/services" + "code.cloudfoundry.org/korifi/controllers/webhooks/networking/domains" + "code.cloudfoundry.org/korifi/controllers/webhooks/networking/routes" + "code.cloudfoundry.org/korifi/controllers/webhooks/services/bindings" + "code.cloudfoundry.org/korifi/controllers/webhooks/services/instances" + "code.cloudfoundry.org/korifi/controllers/webhooks/validation" "code.cloudfoundry.org/korifi/controllers/webhooks/version" - "code.cloudfoundry.org/korifi/controllers/webhooks/workloads" + "code.cloudfoundry.org/korifi/controllers/webhooks/workloads/apps" + "code.cloudfoundry.org/korifi/controllers/webhooks/workloads/orgs" + "code.cloudfoundry.org/korifi/controllers/webhooks/workloads/packages" + "code.cloudfoundry.org/korifi/controllers/webhooks/workloads/spaces" + "code.cloudfoundry.org/korifi/controllers/webhooks/workloads/tasks" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" admissionv1beta1 "k8s.io/api/admission/v1beta1" @@ -45,7 +51,7 @@ func TestWorkloadsWebhooks(t *testing.T) { SetDefaultEventuallyPollingInterval(250 * time.Millisecond) RegisterFailHandler(Fail) - RunSpecs(t, "Workloads Validating Webhooks Integration Test Suite") + RunSpecs(t, "Version Webhook Integration Test Suite") } var _ = BeforeSuite(func() { @@ -80,40 +86,40 @@ var _ = BeforeSuite(func() { Expect((&korifiv1alpha1.CFApp{}).SetupWebhookWithManager(k8sManager)).To(Succeed()) uncachedClient := helpers.NewUncachedClient(k8sManager.GetConfig()) - orgNameDuplicateValidator := webhooks.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, workloads.CFOrgEntityType)) - orgPlacementValidator := webhooks.NewPlacementValidator(uncachedClient, rootNamespace) - Expect(workloads.NewCFOrgValidator(orgNameDuplicateValidator, orgPlacementValidator).SetupWebhookWithManager(k8sManager)).To(Succeed()) + orgNameDuplicateValidator := validation.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, orgs.CFOrgEntityType)) + orgPlacementValidator := validation.NewPlacementValidator(uncachedClient, rootNamespace) + Expect(orgs.NewValidator(orgNameDuplicateValidator, orgPlacementValidator).SetupWebhookWithManager(k8sManager)).To(Succeed()) - spaceNameDuplicateValidator := webhooks.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, workloads.CFSpaceEntityType)) - spacePlacementValidator := webhooks.NewPlacementValidator(uncachedClient, rootNamespace) - Expect(workloads.NewCFSpaceValidator(spaceNameDuplicateValidator, spacePlacementValidator).SetupWebhookWithManager(k8sManager)).To(Succeed()) + spaceNameDuplicateValidator := validation.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, spaces.CFSpaceEntityType)) + spacePlacementValidator := validation.NewPlacementValidator(uncachedClient, rootNamespace) + Expect(spaces.NewValidator(spaceNameDuplicateValidator, spacePlacementValidator).SetupWebhookWithManager(k8sManager)).To(Succeed()) - Expect(networking.NewCFDomainValidator(uncachedClient).SetupWebhookWithManager(k8sManager)).To(Succeed()) - Expect(services.NewCFServiceInstanceValidator( - webhooks.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, services.ServiceInstanceEntityType)), + Expect(domains.NewValidator(uncachedClient).SetupWebhookWithManager(k8sManager)).To(Succeed()) + Expect(instances.NewValidator( + validation.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, instances.ServiceInstanceEntityType)), ).SetupWebhookWithManager(k8sManager)).To(Succeed()) - Expect(workloads.NewCFAppValidator( - webhooks.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, workloads.AppEntityType)), + Expect(apps.NewValidator( + validation.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, apps.AppEntityType)), ).SetupWebhookWithManager(k8sManager)).To(Succeed()) Expect((&korifiv1alpha1.CFPackage{}).SetupWebhookWithManager(k8sManager)).To(Succeed()) - Expect(workloads.NewCFTaskValidator().SetupWebhookWithManager(k8sManager)).To(Succeed()) + Expect(tasks.NewValidator().SetupWebhookWithManager(k8sManager)).To(Succeed()) - Expect(korifiv1alpha1.NewCFProcessDefaulter(defaultMemoryMB, defaultDiskQuotaMB, defaultTimeout). + Expect(korifiv1alpha1.NewCFProcessDefaulter(128, 256, 60). SetupWebhookWithManager(k8sManager)).To(Succeed()) Expect((&korifiv1alpha1.CFBuild{}).SetupWebhookWithManager(k8sManager)).To(Succeed()) Expect((&korifiv1alpha1.CFRoute{}).SetupWebhookWithManager(k8sManager)).To(Succeed()) - Expect(networking.NewCFRouteValidator( - webhooks.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, networking.RouteEntityType)), + Expect(routes.NewValidator( + validation.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, routes.RouteEntityType)), rootNamespace, uncachedClient, ).SetupWebhookWithManager(k8sManager)).To(Succeed()) - Expect(services.NewCFServiceBindingValidator( - webhooks.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, services.ServiceBindingEntityType)), + Expect(bindings.NewCFServiceBindingValidator( + validation.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, bindings.ServiceBindingEntityType)), ).SetupWebhookWithManager(k8sManager)).To(Succeed()) finalizer.NewControllersFinalizerWebhook().SetupWebhookWithManager(k8sManager) - Expect(workloads.NewCFPackageValidator().SetupWebhookWithManager(k8sManager)).To(Succeed()) + Expect(packages.NewValidator().SetupWebhookWithManager(k8sManager)).To(Succeed()) stopManager = helpers.StartK8sManager(k8sManager) diff --git a/controllers/webhooks/version/version_webhook.go b/controllers/webhooks/version/version.go similarity index 86% rename from controllers/webhooks/version/version_webhook.go rename to controllers/webhooks/version/version.go index f731da270..5f3e56186 100644 --- a/controllers/webhooks/version/version_webhook.go +++ b/controllers/webhooks/version/version.go @@ -46,16 +46,16 @@ func (r *VersionWebhook) Handle(ctx context.Context, req admission.Request) admi switch req.Operation { case corev1.Create: logger.V(1).Info("adding-version-on-create") - return r.setVersion(ctx, obj, r.version) + return r.setVersion(obj, r.version) case corev1.Update: - return r.resetVersion(ctx, logger, obj, req) + return r.resetVersion(logger, obj, req) default: logger.Info("received-unexpected-operation-type", "operation", req.Operation) return admission.Denied("we only accept create/update") } } -func (r *VersionWebhook) setVersion(ctx context.Context, obj metav1.PartialObjectMetadata, ver string) admission.Response { +func (r *VersionWebhook) setVersion(obj metav1.PartialObjectMetadata, ver string) admission.Response { origMarshalled, err := json.Marshal(obj) if err != nil { return admission.Errored(http.StatusInternalServerError, err) @@ -65,7 +65,7 @@ func (r *VersionWebhook) setVersion(ctx context.Context, obj metav1.PartialObjec if anns == nil { anns = map[string]string{} } - anns[version.KorifiCreationVersionKey] = r.version + anns[version.KorifiCreationVersionKey] = ver obj.SetAnnotations(anns) marshalled, err := json.Marshal(obj) @@ -76,7 +76,7 @@ func (r *VersionWebhook) setVersion(ctx context.Context, obj metav1.PartialObjec return admission.PatchResponseFromRaw(origMarshalled, marshalled) } -func (r *VersionWebhook) resetVersion(ctx context.Context, logger logr.Logger, obj metav1.PartialObjectMetadata, req admission.Request) admission.Response { +func (r *VersionWebhook) resetVersion(logger logr.Logger, obj metav1.PartialObjectMetadata, req admission.Request) admission.Response { if _, ok := obj.Annotations[version.KorifiCreationVersionKey]; ok { return admission.Allowed("already set") } @@ -89,7 +89,7 @@ func (r *VersionWebhook) resetVersion(ctx context.Context, logger logr.Logger, o if oldVersion, ok := oldObj.Annotations[version.KorifiCreationVersionKey]; ok { logger.V(1).Info("restoring-removed-version", "version", oldVersion) - return r.setVersion(ctx, obj, oldVersion) + return r.setVersion(obj, oldVersion) } return admission.Allowed("no old version") diff --git a/controllers/webhooks/version/version_webhook_test.go b/controllers/webhooks/version/version_test.go similarity index 91% rename from controllers/webhooks/version/version_webhook_test.go rename to controllers/webhooks/version/version_test.go index f5b4ffda9..6ac787efd 100644 --- a/controllers/webhooks/version/version_webhook_test.go +++ b/controllers/webhooks/version/version_test.go @@ -15,12 +15,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) -const ( - defaultMemoryMB = 128 - defaultDiskQuotaMB = 256 - defaultTimeout = 60 -) - var _ = Describe("Setting the version annotation", func() { Describe("create", func() { var testObjects []client.Object @@ -218,6 +212,21 @@ var _ = Describe("Setting the version annotation", func() { g.Expect(taskWorkload.Spec.Command).To(ConsistOf("bar")) }).Should(Succeed()) }) + + When("the object version differs from the webhook version", func() { + BeforeEach(func() { + Expect(k8s.Patch(context.Background(), adminClient, taskWorkload, func() { + taskWorkload.Annotations[version.KorifiCreationVersionKey] = "another-version" + })).To(Succeed()) + }) + + It("restores the original version", func() { + Eventually(func(g Gomega) { + g.Expect(adminClient.Get(context.Background(), client.ObjectKeyFromObject(taskWorkload), taskWorkload)).To(Succeed()) + g.Expect(taskWorkload.Annotations).To(HaveKeyWithValue(version.KorifiCreationVersionKey, "another-version")) + }).Should(Succeed()) + }) + }) }) When("updating the version", func() { diff --git a/controllers/webhooks/workloads/apprev_webhook_test.go b/controllers/webhooks/workloads/apprev_webhook_test.go deleted file mode 100644 index bea2a95b8..000000000 --- a/controllers/webhooks/workloads/apprev_webhook_test.go +++ /dev/null @@ -1,89 +0,0 @@ -package workloads_test - -import ( - "context" - - korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" - "github.com/google/uuid" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -var _ = Describe("ApprevWebhook", func() { - var ( - ctx context.Context - namespace string - app *korifiv1alpha1.CFApp - originalApp *korifiv1alpha1.CFApp - ) - - BeforeEach(func() { - ctx = context.Background() - - namespace = "ns-" + uuid.NewString() - - Expect(adminClient.Create(ctx, &v1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: namespace, - }, - })).To(Succeed()) - - app = makeCFApp(uuid.NewString(), namespace, uuid.NewString()) - app.Annotations = map[string]string{ - korifiv1alpha1.CFAppRevisionKey: "5", - } - app.Spec.DesiredState = korifiv1alpha1.StartedState - Expect(adminClient.Create(ctx, app)).To(Succeed()) - originalApp = app.DeepCopy() - }) - - JustBeforeEach(func() { - app.Spec.DisplayName = "changed-display-name" - Expect(adminClient.Patch(ctx, app, client.MergeFrom(originalApp))).To(Succeed()) - }) - - It("does not change the app rev", func() { - Expect(app.Annotations[korifiv1alpha1.CFAppRevisionKey]).To(Equal("5")) - }) - - When("desiredState changes from started to stopped", func() { - BeforeEach(func() { - app.Spec.DesiredState = korifiv1alpha1.StoppedState - }) - - It("increments the app rev", func() { - Expect(app.Annotations[korifiv1alpha1.CFAppRevisionKey]).To(Equal("6")) - }) - - It("updates status.lastStopAppRev", func() { - Expect(app.Annotations[korifiv1alpha1.CFAppLastStopRevisionKey]).To(Equal("6")) - }) - - When("the app rev is not a number", func() { - BeforeEach(func() { - app.Annotations[korifiv1alpha1.CFAppRevisionKey] = "a" - Expect(adminClient.Patch(ctx, app, client.MergeFrom(originalApp))).To(Succeed()) - originalApp = app.DeepCopy() - }) - - It("defaults the app rev to 0", func() { - Expect(app.Annotations[korifiv1alpha1.CFAppRevisionKey]).To(Equal("0")) - }) - }) - - When("the app rev is negative", func() { - BeforeEach(func() { - app.Annotations[korifiv1alpha1.CFAppRevisionKey] = "-10" - Expect(adminClient.Patch(ctx, app, client.MergeFrom(originalApp))).To(Succeed()) - originalApp = app.DeepCopy() - }) - - It("sets the app rev to 0", func() { - Expect(app.Annotations[korifiv1alpha1.CFAppRevisionKey]).To(Equal("0")) - }) - }) - }) -}) diff --git a/controllers/webhooks/workloads/apprev_webhook.go b/controllers/webhooks/workloads/apps/apprev.go similarity index 99% rename from controllers/webhooks/workloads/apprev_webhook.go rename to controllers/webhooks/workloads/apps/apprev.go index 1d7854f24..5d5931fc5 100644 --- a/controllers/webhooks/workloads/apprev_webhook.go +++ b/controllers/webhooks/workloads/apps/apprev.go @@ -1,4 +1,4 @@ -package workloads +package apps //+kubebuilder:webhook:path=/mutate-korifi-cloudfoundry-org-v1alpha1-cfapp-apprev,mutating=true,failurePolicy=fail,sideEffects=None,groups=korifi.cloudfoundry.org,resources=cfapps,verbs=update,versions=v1alpha1,name=mcfapprev.korifi.cloudfoundry.org,admissionReviewVersions={v1,v1beta1} diff --git a/controllers/webhooks/workloads/apps/apprev_test.go b/controllers/webhooks/workloads/apps/apprev_test.go new file mode 100644 index 000000000..aae6d7f17 --- /dev/null +++ b/controllers/webhooks/workloads/apps/apprev_test.go @@ -0,0 +1,90 @@ +package apps_test + +import ( + korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" + "code.cloudfoundry.org/korifi/tools/k8s" + "github.com/google/uuid" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ = Describe("ApprevWebhook", func() { + var app *korifiv1alpha1.CFApp + + BeforeEach(func() { + app = &korifiv1alpha1.CFApp{ + ObjectMeta: metav1.ObjectMeta{ + Name: uuid.NewString(), + Namespace: testNamespace, + Annotations: map[string]string{ + korifiv1alpha1.CFAppRevisionKey: "5", + }, + }, + Spec: korifiv1alpha1.CFAppSpec{ + DisplayName: uuid.NewString(), + DesiredState: korifiv1alpha1.StoppedState, + Lifecycle: korifiv1alpha1.Lifecycle{ + Type: "buildpack", + }, + }, + } + Expect(adminClient.Create(ctx, app)).To(Succeed()) + + Expect(k8s.Patch(ctx, adminClient, app, func() { + app.Spec.DesiredState = korifiv1alpha1.StartedState + })).To(Succeed()) + }) + + When("the app change does not affect the app rev", func() { + JustBeforeEach(func() { + Expect(k8s.Patch(ctx, adminClient, app, func() { + app.Spec.DisplayName = "changed-display-name" + })).To(Succeed()) + }) + + It("does not change the app rev", func() { + Expect(app.Annotations[korifiv1alpha1.CFAppRevisionKey]).To(Equal("5")) + }) + }) + + When("desiredState changes from started to stopped", func() { + JustBeforeEach(func() { + Expect(k8s.Patch(ctx, adminClient, app, func() { + app.Spec.DesiredState = korifiv1alpha1.StoppedState + })).To(Succeed()) + }) + + It("increments the app rev", func() { + Expect(app.Annotations[korifiv1alpha1.CFAppRevisionKey]).To(Equal("6")) + }) + + It("updates status.lastStopAppRev", func() { + Expect(app.Annotations[korifiv1alpha1.CFAppLastStopRevisionKey]).To(Equal("6")) + }) + + When("the app rev is not a number", func() { + BeforeEach(func() { + Expect(k8s.Patch(ctx, adminClient, app, func() { + app.Annotations[korifiv1alpha1.CFAppRevisionKey] = "a" + })).To(Succeed()) + }) + + It("defaults the app rev to 0", func() { + Expect(app.Annotations[korifiv1alpha1.CFAppRevisionKey]).To(Equal("0")) + }) + }) + + When("the app rev is negative", func() { + BeforeEach(func() { + Expect(k8s.Patch(ctx, adminClient, app, func() { + app.Annotations[korifiv1alpha1.CFAppRevisionKey] = "-10" + })).To(Succeed()) + }) + + It("sets the app rev to 0", func() { + Expect(app.Annotations[korifiv1alpha1.CFAppRevisionKey]).To(Equal("0")) + }) + }) + }) +}) diff --git a/controllers/webhooks/workloads/apps/suite_test.go b/controllers/webhooks/workloads/apps/suite_test.go new file mode 100644 index 000000000..f97f5832e --- /dev/null +++ b/controllers/webhooks/workloads/apps/suite_test.go @@ -0,0 +1,108 @@ +package apps_test + +import ( + "context" + "path/filepath" + "testing" + "time" + + korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" + "code.cloudfoundry.org/korifi/controllers/controllers/shared" + "code.cloudfoundry.org/korifi/controllers/coordination" + "code.cloudfoundry.org/korifi/tests/helpers" + + "code.cloudfoundry.org/korifi/controllers/webhooks/finalizer" + "code.cloudfoundry.org/korifi/controllers/webhooks/validation" + "code.cloudfoundry.org/korifi/controllers/webhooks/version" + "code.cloudfoundry.org/korifi/controllers/webhooks/workloads/apps" + "github.com/google/uuid" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/scheme" + "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" + //+kubebuilder:scaffold:imports +) + +var ( + stopManager context.CancelFunc + stopClientCache context.CancelFunc + testEnv *envtest.Environment + adminClient client.Client + adminNonSyncClient client.Client + + ctx context.Context + testNamespace string +) + +func TestWorkloadsWebhooks(t *testing.T) { + SetDefaultEventuallyTimeout(10 * time.Second) + SetDefaultEventuallyPollingInterval(250 * time.Millisecond) + + RegisterFailHandler(Fail) + RunSpecs(t, "CFApp Webhooks Integration Test Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{ + filepath.Join("..", "..", "..", "..", "helm", "korifi", "controllers", "crds"), + }, + ErrorIfCRDPathMissing: true, + WebhookInstallOptions: envtest.WebhookInstallOptions{ + Paths: []string{filepath.Join("..", "..", "..", "..", "helm", "korifi", "controllers", "manifests.yaml")}, + }, + } + + adminConfig, err := testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(adminConfig).NotTo(BeNil()) + + Expect(korifiv1alpha1.AddToScheme(scheme.Scheme)).To(Succeed()) + Expect(corev1.AddToScheme(scheme.Scheme)).To(Succeed()) + + k8sManager := helpers.NewK8sManager(testEnv, filepath.Join("helm", "korifi", "controllers", "role.yaml")) + Expect(shared.SetupIndexWithManager(k8sManager)).To(Succeed()) + + adminNonSyncClient, err = client.New(testEnv.Config, client.Options{ + Scheme: scheme.Scheme, + }) + Expect(err).NotTo(HaveOccurred()) + + adminClient, stopClientCache = helpers.NewCachedClient(testEnv.Config) + + version.NewVersionWebhook("some-version").SetupWebhookWithManager(k8sManager) + finalizer.NewControllersFinalizerWebhook().SetupWebhookWithManager(k8sManager) + + (&apps.AppRevWebhook{}).SetupWebhookWithManager(k8sManager) + + uncachedClient := helpers.NewUncachedClient(k8sManager.GetConfig()) + appNameDuplicateValidator := validation.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, apps.AppEntityType)) + Expect(apps.NewValidator(appNameDuplicateValidator).SetupWebhookWithManager(k8sManager)).To(Succeed()) + + stopManager = helpers.StartK8sManager(k8sManager) +}) + +var _ = BeforeEach(func() { + ctx = context.Background() + + testNamespace = uuid.NewString() + + Expect(adminClient.Create(ctx, &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: testNamespace, + }, + })).To(Succeed()) +}) + +var _ = AfterSuite(func() { + stopClientCache() + stopManager() + Expect(testEnv.Stop()).To(Succeed()) +}) diff --git a/controllers/webhooks/workloads/cfapp_validator.go b/controllers/webhooks/workloads/apps/validator.go similarity index 76% rename from controllers/webhooks/workloads/cfapp_validator.go rename to controllers/webhooks/workloads/apps/validator.go index 1db7ea5dc..44ef1987f 100644 --- a/controllers/webhooks/workloads/cfapp_validator.go +++ b/controllers/webhooks/workloads/apps/validator.go @@ -1,4 +1,4 @@ -package workloads +package apps import ( "context" @@ -6,6 +6,7 @@ import ( korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" "code.cloudfoundry.org/korifi/controllers/webhooks" + "code.cloudfoundry.org/korifi/controllers/webhooks/validation" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" @@ -24,26 +25,26 @@ var cfapplog = logf.Log.WithName("cfapp-validate") //+kubebuilder:webhook:path=/validate-korifi-cloudfoundry-org-v1alpha1-cfapp,mutating=false,failurePolicy=fail,sideEffects=NoneOnDryRun,groups=korifi.cloudfoundry.org,resources=cfapps,verbs=create;update;delete,versions=v1alpha1,name=vcfapp.korifi.cloudfoundry.org,admissionReviewVersions={v1,v1beta1} -type CFAppValidator struct { +type Validator struct { duplicateValidator webhooks.NameValidator } -var _ webhook.CustomValidator = &CFAppValidator{} +var _ webhook.CustomValidator = &Validator{} -func NewCFAppValidator(duplicateValidator webhooks.NameValidator) *CFAppValidator { - return &CFAppValidator{ +func NewValidator(duplicateValidator webhooks.NameValidator) *Validator { + return &Validator{ duplicateValidator: duplicateValidator, } } -func (v *CFAppValidator) SetupWebhookWithManager(mgr ctrl.Manager) error { +func (v *Validator) SetupWebhookWithManager(mgr ctrl.Manager) error { return ctrl.NewWebhookManagedBy(mgr). For(&korifiv1alpha1.CFApp{}). WithValidator(v). Complete() } -func (v *CFAppValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { +func (v *Validator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { app, ok := obj.(*korifiv1alpha1.CFApp) if !ok { return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a CFApp but got a %T", obj)) @@ -52,7 +53,7 @@ func (v *CFAppValidator) ValidateCreate(ctx context.Context, obj runtime.Object) return nil, v.duplicateValidator.ValidateCreate(ctx, cfapplog, app.Namespace, app) } -func (v *CFAppValidator) ValidateUpdate(ctx context.Context, oldObj, obj runtime.Object) (admission.Warnings, error) { +func (v *Validator) ValidateUpdate(ctx context.Context, oldObj, obj runtime.Object) (admission.Warnings, error) { app, ok := obj.(*korifiv1alpha1.CFApp) if !ok { return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a CFApp but got a %T", obj)) @@ -68,7 +69,7 @@ func (v *CFAppValidator) ValidateUpdate(ctx context.Context, oldObj, obj runtime } if app.Spec.Lifecycle.Type != oldApp.Spec.Lifecycle.Type { - return nil, webhooks.ValidationError{ + return nil, validation.ValidationError{ Type: "ImmutableFieldError", Message: fmt.Sprintf("Lifecycle type cannot be changed from %s to %s", oldApp.Spec.Lifecycle.Type, app.Spec.Lifecycle.Type), }.ExportJSONError() @@ -77,7 +78,7 @@ func (v *CFAppValidator) ValidateUpdate(ctx context.Context, oldObj, obj runtime return nil, v.duplicateValidator.ValidateUpdate(ctx, cfapplog, app.Namespace, oldApp, app) } -func (v *CFAppValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { +func (v *Validator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { app, ok := obj.(*korifiv1alpha1.CFApp) if !ok { return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a CFApp but got a %T", obj)) diff --git a/controllers/webhooks/workloads/apps/validator_test.go b/controllers/webhooks/workloads/apps/validator_test.go new file mode 100644 index 000000000..5991c78fb --- /dev/null +++ b/controllers/webhooks/workloads/apps/validator_test.go @@ -0,0 +1,257 @@ +package apps_test + +import ( + "context" + "fmt" + "strings" + + korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" + "code.cloudfoundry.org/korifi/tools/k8s" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/google/uuid" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ = Describe("CFAppValidatingWebhook", func() { + var ( + app *korifiv1alpha1.CFApp + createErr error + ) + + BeforeEach(func() { + app = &korifiv1alpha1.CFApp{ + ObjectMeta: metav1.ObjectMeta{ + Name: uuid.NewString(), + Namespace: testNamespace, + }, + Spec: korifiv1alpha1.CFAppSpec{ + DisplayName: "app-" + uuid.NewString(), + DesiredState: "STOPPED", + Lifecycle: korifiv1alpha1.Lifecycle{ + Type: "buildpack", + }, + }, + } + }) + + JustBeforeEach(func() { + createErr = adminClient.Create(ctx, app) + }) + + Describe("Create", func() { + It("should succeed", func() { + Expect(createErr).NotTo(HaveOccurred()) + }) + + When("another CFApp exists with a different name in the same namespace", func() { + BeforeEach(func() { + Expect(adminClient.Create(ctx, &korifiv1alpha1.CFApp{ + ObjectMeta: metav1.ObjectMeta{ + Name: uuid.NewString(), + Namespace: testNamespace, + }, + Spec: korifiv1alpha1.CFAppSpec{ + DisplayName: uuid.NewString(), + DesiredState: "STOPPED", + Lifecycle: korifiv1alpha1.Lifecycle{ + Type: "buildpack", + }, + }, + })).To(Succeed()) + }) + + It("should succeed", func() { + Expect(createErr).NotTo(HaveOccurred()) + }) + }) + + When("another CFApp exists with the same name in a different namespace", func() { + BeforeEach(func() { + anotherNamespace := uuid.NewString() + Expect(adminClient.Create(ctx, &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: anotherNamespace, + }, + })).To(Succeed()) + + Expect(adminClient.Create(ctx, &korifiv1alpha1.CFApp{ + ObjectMeta: metav1.ObjectMeta{ + Name: uuid.NewString(), + Namespace: anotherNamespace, + }, + Spec: korifiv1alpha1.CFAppSpec{ + DisplayName: app.Spec.DisplayName, + DesiredState: "STOPPED", + Lifecycle: korifiv1alpha1.Lifecycle{ + Type: "buildpack", + }, + }, + })).To(Succeed()) + }) + + It("should succeed", func() { + Expect(createErr).NotTo(HaveOccurred()) + }) + }) + + When("another CFApp exists with the same name in the same namespace", func() { + BeforeEach(func() { + Expect(adminClient.Create(ctx, &korifiv1alpha1.CFApp{ + ObjectMeta: metav1.ObjectMeta{ + Name: uuid.NewString(), + Namespace: testNamespace, + }, + Spec: korifiv1alpha1.CFAppSpec{ + DisplayName: app.Spec.DisplayName, + DesiredState: "STOPPED", + Lifecycle: korifiv1alpha1.Lifecycle{ + Type: "buildpack", + }, + }, + })).To(Succeed()) + }) + + It("should fail", func() { + Expect(createErr).To(MatchError(ContainSubstring("App with the name '%s' already exists.", app.Spec.DisplayName))) + }) + }) + + When("another CFApp exists with the same name(case insensitive) in the same namespace", func() { + BeforeEach(func() { + Expect(adminClient.Create(ctx, &korifiv1alpha1.CFApp{ + ObjectMeta: metav1.ObjectMeta{ + Name: uuid.NewString(), + Namespace: testNamespace, + }, + Spec: korifiv1alpha1.CFAppSpec{ + DisplayName: strings.ToUpper(app.Spec.DisplayName), + DesiredState: "STOPPED", + Lifecycle: korifiv1alpha1.Lifecycle{ + Type: "buildpack", + }, + }, + })).To(Succeed()) + }) + + It("should fail", func() { + Expect(createErr).To(MatchError(ContainSubstring(fmt.Sprintf("App with the name '%s' already exists.", app.Spec.DisplayName)))) + }) + }) + }) + + Describe("Update", func() { + var updateErr error + + Describe("changing the name", func() { + var newName string + + BeforeEach(func() { + newName = uuid.NewString() + }) + + JustBeforeEach(func() { + updateErr = k8s.Patch(ctx, adminClient, app, func() { + app.Spec.DisplayName = newName + }) + }) + + It("should succeed", func() { + Expect(updateErr).NotTo(HaveOccurred()) + + Expect(adminClient.Get(context.Background(), client.ObjectKeyFromObject(app), app)).To(Succeed()) + Expect(app.Spec.DisplayName).To(Equal(newName)) + }) + + When("reusing an old name", func() { + var oldName string + + BeforeEach(func() { + oldName = app.Spec.DisplayName + }) + + It("allows creating another app with the old name", func() { + Expect(updateErr).NotTo(HaveOccurred()) + + Expect(adminClient.Create(ctx, &korifiv1alpha1.CFApp{ + ObjectMeta: metav1.ObjectMeta{ + Name: uuid.NewString(), + Namespace: testNamespace, + }, + Spec: korifiv1alpha1.CFAppSpec{ + DisplayName: oldName, + DesiredState: "STOPPED", + Lifecycle: korifiv1alpha1.Lifecycle{ + Type: "buildpack", + }, + }, + })).To(Succeed()) + }) + }) + + When("an app with the new name already exists", func() { + BeforeEach(func() { + Expect(adminClient.Create(ctx, &korifiv1alpha1.CFApp{ + ObjectMeta: metav1.ObjectMeta{ + Name: uuid.NewString(), + Namespace: testNamespace, + }, + Spec: korifiv1alpha1.CFAppSpec{ + DisplayName: newName, + DesiredState: "STOPPED", + Lifecycle: korifiv1alpha1.Lifecycle{ + Type: "buildpack", + }, + }, + })).To(Succeed()) + }) + + It("should fail", func() { + Expect(updateErr).To(MatchError(ContainSubstring(fmt.Sprintf("App with the name '%s' already exists.", newName)))) + }) + }) + }) + + Describe("not changing the name", func() { + JustBeforeEach(func() { + updateErr = k8s.Patch(ctx, adminClient, app, func() { + app.Spec.DesiredState = korifiv1alpha1.StartedState + }) + }) + + It("should succeed", func() { + Expect(updateErr).NotTo(HaveOccurred()) + + Expect(adminClient.Get(context.Background(), client.ObjectKeyFromObject(app), app)).To(Succeed()) + Expect(app.Spec.DesiredState).To(Equal(korifiv1alpha1.StartedState)) + }) + }) + + Describe("changing the lifecycle type", func() { + JustBeforeEach(func() { + updateErr = k8s.Patch(ctx, adminClient, app, func() { + app.Spec.Lifecycle.Type = korifiv1alpha1.LifecycleType("docker") + }) + }) + + It("should fail", func() { + Expect(updateErr).To(MatchError(ContainSubstring("cannot be changed from buildpack to docker"))) + }) + }) + }) + + Describe("Delete", func() { + var deleteErr error + + JustBeforeEach(func() { + deleteErr = adminNonSyncClient.Delete(ctx, app) + }) + + It("succeeds", func() { + Expect(deleteErr).NotTo(HaveOccurred()) + }) + }) +}) diff --git a/controllers/webhooks/workloads/cfapp_validator_test.go b/controllers/webhooks/workloads/cfapp_validator_test.go deleted file mode 100644 index f07776088..000000000 --- a/controllers/webhooks/workloads/cfapp_validator_test.go +++ /dev/null @@ -1,226 +0,0 @@ -package workloads_test - -import ( - "context" - "fmt" - "strings" - - korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" - "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/google/uuid" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -var _ = Describe("CFAppValidatingWebhook", func() { - var ( - ctx context.Context - namespace1 string - namespace2 string - app1Guid string - app2Guid string - app1Name string - app2Name string - app1 *korifiv1alpha1.CFApp - ) - - BeforeEach(func() { - ctx = context.Background() - - namespace1 = "ns-1-" + uuid.NewString() - namespace2 = "ns-2-" + uuid.NewString() - - Expect(adminClient.Create(ctx, &v1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: namespace1, - }, - })).To(Succeed()) - Expect(adminClient.Create(ctx, &v1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: namespace2, - }, - })).To(Succeed()) - - app1Guid = "guid-1-" + uuid.NewString() - app2Guid = "guid-2-" + uuid.NewString() - app1Name = "name-1-" + uuid.NewString() - app2Name = "name-2-" + uuid.NewString() - }) - - Describe("Create", func() { - var createErr error - - BeforeEach(func() { - app1 = makeCFApp(app1Guid, namespace1, app1Name) - }) - - JustBeforeEach(func() { - createErr = adminClient.Create(ctx, app1) - }) - - It("should succeed", func() { - Expect(createErr).NotTo(HaveOccurred()) - }) - - When("another CFApp exists with a different name in the same namespace", func() { - BeforeEach(func() { - app2 := makeCFApp(app2Guid, namespace1, app2Name) - Expect(adminClient.Create(ctx, app2)).To(Succeed()) - }) - - It("should succeed", func() { - Expect(createErr).NotTo(HaveOccurred()) - }) - }) - - When("another CFApp exists with the same name in a different namespace", func() { - BeforeEach(func() { - app2 := makeCFApp(app2Guid, namespace2, app1Name) - Expect(adminClient.Create(ctx, app2)).To(Succeed()) - }) - - It("should succeed", func() { - Expect(createErr).NotTo(HaveOccurred()) - }) - }) - - When("another CFApp exists with the same name in the same namespace", func() { - BeforeEach(func() { - app2 := makeCFApp(app2Guid, namespace1, app1Name) - Expect(adminClient.Create(ctx, app2)).To(Succeed()) - }) - - It("should fail", func() { - Expect(createErr).To(MatchError(ContainSubstring("App with the name '%s' already exists.", app1Name))) - }) - }) - - When("another CFApp exists with the same name(case insensitive) in the same namespace", func() { - BeforeEach(func() { - app2 := makeCFApp(app2Guid, namespace1, strings.ToUpper(app1Name)) - Expect( - adminClient.Create(ctx, app2), - ).To(Succeed()) - }) - - It("should fail", func() { - Expect(createErr).To(MatchError(ContainSubstring(fmt.Sprintf("App with the name '%s' already exists.", app1Name)))) - }) - }) - }) - - Describe("Update", func() { - var ( - updateErr error - originalApp1 *korifiv1alpha1.CFApp - ) - - BeforeEach(func() { - app1 = makeCFApp(app1Guid, namespace1, app1Name) - Expect(adminClient.Create(ctx, app1)).To(Succeed()) - originalApp1 = app1.DeepCopy() - }) - - JustBeforeEach(func() { - updateErr = adminClient.Patch(context.Background(), app1, client.MergeFrom(originalApp1)) - }) - - When("changing the name", func() { - var newName string - - BeforeEach(func() { - newName = uuid.NewString() - app1.Spec.DisplayName = newName - }) - - It("should succeed", func() { - Expect(updateErr).NotTo(HaveOccurred()) - - app1Actual := korifiv1alpha1.CFApp{} - Expect(adminClient.Get(context.Background(), client.ObjectKeyFromObject(app1), &app1Actual)).To(Succeed()) - Expect(app1Actual.Spec.DisplayName).To(Equal(newName)) - }) - - When("reusing an old name", func() { - It("allows creating another app with the old name", func() { - Expect(updateErr).NotTo(HaveOccurred()) - - reuseOldNameApp := makeCFApp(uuid.NewString(), namespace1, app1Name) - Expect(adminClient.Create(ctx, reuseOldNameApp)).To(Succeed()) - }) - }) - }) - - When("not changing the name", func() { - BeforeEach(func() { - app1.Spec.DesiredState = korifiv1alpha1.StartedState - }) - - It("should succeed", func() { - Expect(updateErr).NotTo(HaveOccurred()) - - app1Actual := korifiv1alpha1.CFApp{} - Expect(adminClient.Get(context.Background(), client.ObjectKeyFromObject(app1), &app1Actual)).To(Succeed()) - Expect(app1Actual.Spec.DesiredState).To(Equal(korifiv1alpha1.StartedState)) - }) - }) - - When("modifying spec.DisplayName to match another CFApp spec.DisplayName", func() { - BeforeEach(func() { - app2 := makeCFApp(app2Guid, namespace1, app2Name) - app1.Spec.DisplayName = app2Name - Expect(adminClient.Create(ctx, app2)).To(Succeed()) - }) - - It("should fail", func() { - Expect(updateErr).To(MatchError(ContainSubstring(fmt.Sprintf("App with the name '%s' already exists.", app2Name)))) - }) - }) - - When("changing the lifecycle type", func() { - BeforeEach(func() { - app1.Spec.Lifecycle.Type = korifiv1alpha1.LifecycleType("docker") - }) - - It("should fail", func() { - Expect(updateErr).To(MatchError(ContainSubstring("cannot be changed from buildpack to docker"))) - }) - }) - }) - - Describe("Delete", func() { - var deleteErr error - - BeforeEach(func() { - app1 = makeCFApp(app1Guid, namespace1, app1Name) - Expect(adminClient.Create(ctx, app1)).To(Succeed()) - }) - - JustBeforeEach(func() { - deleteErr = adminNonSyncClient.Delete(ctx, app1) - }) - - It("succeeds", func() { - Expect(deleteErr).NotTo(HaveOccurred()) - }) - }) -}) - -func makeCFApp(cfAppGUID string, namespace string, name string) *korifiv1alpha1.CFApp { - return &korifiv1alpha1.CFApp{ - ObjectMeta: metav1.ObjectMeta{ - Name: cfAppGUID, - Namespace: namespace, - }, - Spec: korifiv1alpha1.CFAppSpec{ - DisplayName: name, - DesiredState: "STOPPED", - Lifecycle: korifiv1alpha1.Lifecycle{ - Type: "buildpack", - }, - }, - } -} diff --git a/controllers/webhooks/workloads/cforg_validator_test.go b/controllers/webhooks/workloads/cforg_validator_test.go deleted file mode 100644 index edba0aed9..000000000 --- a/controllers/webhooks/workloads/cforg_validator_test.go +++ /dev/null @@ -1,202 +0,0 @@ -package workloads_test - -import ( - "context" - "fmt" - "strings" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" - - "github.com/google/uuid" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -var _ = Describe("CFOrgValidatingWebhook", func() { - var ( - ctx context.Context - org1Guid string - org2Guid string - org1Name string - org2Name string - org1 *korifiv1alpha1.CFOrg - ) - - BeforeEach(func() { - ctx = context.Background() - - org1Guid = "guid-1-" + uuid.NewString() - org2Guid = "guid-2-" + uuid.NewString() - org1Name = "name-1-" + uuid.NewString() - org2Name = "name-2-" + uuid.NewString() - }) - - Describe("Create", func() { - var createErr error - - BeforeEach(func() { - org1 = makeCFOrg(org1Guid, rootNamespace, org1Name) - }) - - JustBeforeEach(func() { - createErr = adminClient.Create(ctx, org1) - }) - - It("should succeed", func() { - Expect(createErr).NotTo(HaveOccurred()) - }) - - When("CFOrg is requested outside of root namespace", func() { - BeforeEach(func() { - org1.Namespace = "default" - }) - - It("should fail", func() { - Expect(createErr).To(MatchError(ContainSubstring(fmt.Sprintf("Organization '%s' must be placed in the root 'cf' namespace", org1Name)))) - }) - }) - - When("the CFOrg name would not be a valid label value (>63 chars)", func() { - BeforeEach(func() { - org1.Name = strings.Repeat("a", 64) - }) - - It("should fail", func() { - Expect(createErr).To(MatchError(ContainSubstring("org name cannot be longer than 63 chars"))) - }) - }) - - When("another CFOrg exists with a different name in the same namespace", func() { - BeforeEach(func() { - org2 := makeCFOrg(org2Guid, rootNamespace, org2Name) - Expect(adminClient.Create(ctx, org2)).To(Succeed()) - }) - - It("should succeed", func() { - Expect(createErr).NotTo(HaveOccurred()) - }) - }) - - When("another CFOrg exists with the same name in the same namespace", func() { - BeforeEach(func() { - org2 := makeCFOrg(org2Guid, rootNamespace, org1Name) - Expect(adminClient.Create(ctx, org2)).To(Succeed()) - }) - - It("should fail", func() { - Expect(createErr).To(MatchError(ContainSubstring("Organization '%s' already exists.", org1Name))) - }) - }) - - When("another CFOrg exists with the same name(case insensitive) in the same namespace", func() { - BeforeEach(func() { - org2 := makeCFOrg(org2Guid, rootNamespace, strings.ToUpper(org1Name)) - Expect( - adminClient.Create(ctx, org2), - ).To(Succeed()) - }) - - It("should fail", func() { - Expect(createErr).To(MatchError(ContainSubstring(fmt.Sprintf("Organization '%s' already exists.", org1Name)))) - }) - }) - }) - - Describe("Update", func() { - var ( - originalOrg1 *korifiv1alpha1.CFOrg - updateErr error - ) - - BeforeEach(func() { - org1 = makeCFOrg(org1Guid, rootNamespace, org1Name) - Expect(adminClient.Create(ctx, org1)).To(Succeed()) - originalOrg1 = org1.DeepCopy() - }) - - JustBeforeEach(func() { - updateErr = adminClient.Patch(context.Background(), org1, client.MergeFrom(originalOrg1)) - }) - - When("changing the name", func() { - var newName string - - BeforeEach(func() { - newName = uuid.NewString() - org1.Spec.DisplayName = newName - }) - - It("should succeed", func() { - Expect(updateErr).NotTo(HaveOccurred()) - org1Actual := korifiv1alpha1.CFOrg{} - Expect(adminClient.Get(context.Background(), client.ObjectKeyFromObject(org1), &org1Actual)).To(Succeed()) - Expect(org1Actual.Spec.DisplayName).To(Equal(newName)) - }) - - When("reusing an old name", func() { - It("allows creating another org with the old name", func() { - Expect(updateErr).NotTo(HaveOccurred()) - - reuseOldNameOrg := makeCFOrg(uuid.NewString(), rootNamespace, org1Name) - Expect(adminClient.Create(ctx, reuseOldNameOrg)).To(Succeed()) - }) - }) - }) - - When("not changing the name", func() { - It("should succeed", func() { - Expect(updateErr).NotTo(HaveOccurred()) - org1Actual := korifiv1alpha1.CFOrg{} - Expect(adminClient.Get(context.Background(), client.ObjectKeyFromObject(org1), &org1Actual)).To(Succeed()) - }) - }) - - When("modifying spec.DisplayName to match another CFOrg spec.DisplayName", func() { - BeforeEach(func() { - org2 := makeCFOrg(org2Guid, rootNamespace, org2Name) - org1.Spec.DisplayName = org2Name - Expect(adminClient.Create(ctx, org2)).To(Succeed()) - }) - - It("should fail", func() { - Expect(updateErr).To(MatchError(ContainSubstring(fmt.Sprintf("Organization '%s' already exists.", org2Name)))) - }) - }) - }) - - Describe("Delete", func() { - var deleteErr error - - BeforeEach(func() { - org1 = makeCFOrg(org1Guid, rootNamespace, org1Name) - Expect(adminClient.Create(ctx, org1)).To(Succeed()) - }) - - JustBeforeEach(func() { - deleteErr = adminNonSyncClient.Delete(ctx, org1) - }) - - It("succeeds", func() { - Expect(deleteErr).NotTo(HaveOccurred()) - }) - }) -}) - -func makeCFOrg(cfOrgGUID string, namespace string, name string) *korifiv1alpha1.CFOrg { - return &korifiv1alpha1.CFOrg{ - TypeMeta: metav1.TypeMeta{ - Kind: "CFOrg", - APIVersion: korifiv1alpha1.GroupVersion.Identifier(), - }, - ObjectMeta: metav1.ObjectMeta{ - Name: cfOrgGUID, - Namespace: namespace, - }, - Spec: korifiv1alpha1.CFOrgSpec{ - DisplayName: name, - }, - } -} diff --git a/controllers/webhooks/workloads/cfspace_validator_test.go b/controllers/webhooks/workloads/cfspace_validator_test.go deleted file mode 100644 index d1a44939b..000000000 --- a/controllers/webhooks/workloads/cfspace_validator_test.go +++ /dev/null @@ -1,161 +0,0 @@ -package workloads_test - -import ( - "context" - "fmt" - "strings" - - korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" - "code.cloudfoundry.org/korifi/tools/k8s" - - "github.com/google/uuid" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -var _ = Describe("CFSpaceValidatingWebhook", func() { - var ( - ctx context.Context - cfSpace, cfSpace2 *korifiv1alpha1.CFSpace - orgNamespace string - ) - - BeforeEach(func() { - ctx = context.Background() - - orgNamespace = "test-org-" + uuid.NewString() - Expect(adminClient.Create(ctx, &korifiv1alpha1.CFOrg{ - ObjectMeta: metav1.ObjectMeta{ - Name: orgNamespace, - Namespace: rootNamespace, - }, - Spec: korifiv1alpha1.CFOrgSpec{ - DisplayName: orgNamespace, - }, - })).To(Succeed()) - - Expect(adminClient.Create(ctx, &v1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: orgNamespace, - Labels: map[string]string{korifiv1alpha1.OrgNameKey: orgNamespace}, - }, - })).To(Succeed()) - }) - - Describe("creating a space", func() { - var err error - - BeforeEach(func() { - cfSpace = makeCFSpace(orgNamespace, "my-space") - }) - - JustBeforeEach(func() { - err = adminClient.Create(ctx, cfSpace) - }) - - It("succeeds", func() { - Expect(err).To(Succeed()) - }) - - When("a corresponding CFOrg does not exist", func() { - BeforeEach(func() { - cfSpace.Namespace = "not-an-org" - Expect(adminClient.Create(ctx, &v1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: "not-an-org", - }, - })).To(Succeed()) - }) - - It("fails", func() { - Expect(err).To(MatchError(ContainSubstring("Organization 'not-an-org' does not exist for Space 'my-space'"))) - }) - }) - - When("the CFSpace name would not be a valid label value (>63 chars)", func() { - BeforeEach(func() { - cfSpace.Name = strings.Repeat("a", 64) - }) - - It("should fail", func() { - Expect(err).To(MatchError(ContainSubstring("space name cannot be longer than 63 chars"))) - }) - }) - - When("the name already exists in the org namespace", func() { - BeforeEach(func() { - cfSpace2 = makeCFSpace(orgNamespace, "my-space") - Expect(adminClient.Create(ctx, cfSpace2)).To(Succeed()) - }) - - It("fails", func() { - Expect(err).To(MatchError(ContainSubstring("Name must be unique per organization"))) - }) - }) - - When("another CFSpace exists with the same name(case insensitive) in the same namespace", func() { - BeforeEach(func() { - cfSpace2 = makeCFSpace(orgNamespace, "My-Space") - Expect(adminClient.Create(ctx, cfSpace2)).To(Succeed()) - }) - - It("should fail", func() { - Expect(err).To(MatchError(ContainSubstring(fmt.Sprintf("Space '%s' already exists.", cfSpace.Spec.DisplayName)))) - }) - }) - }) - - Describe("updating a space", func() { - BeforeEach(func() { - cfSpace = makeCFSpace(orgNamespace, "my-space") - Expect(adminClient.Create(ctx, cfSpace)).To(Succeed()) - }) - - When("the space name is changed to another which is unique in the root CF namespace", func() { - It("succeeds", func() { - Expect(k8s.Patch(ctx, adminClient, cfSpace, func() { - cfSpace.Spec.DisplayName = "another-space" - })).To(Succeed()) - }) - }) - - When("the new space name already exists in the org namespace", func() { - BeforeEach(func() { - cfSpace2 = makeCFSpace(orgNamespace, "another-space") - Expect(adminClient.Create(ctx, cfSpace2)).To(Succeed()) - }) - - It("fails", func() { - Expect(k8s.Patch(ctx, adminClient, cfSpace, func() { - cfSpace.Spec.DisplayName = "another-space" - })).To(MatchError(ContainSubstring("Name must be unique per organization"))) - }) - }) - }) - - Describe("deleting a space", func() { - BeforeEach(func() { - cfSpace = makeCFSpace(orgNamespace, "my-space") - Expect(adminClient.Create(ctx, cfSpace)).To(Succeed()) - }) - - It("can delete the space", func() { - Expect(adminNonSyncClient.Delete(ctx, cfSpace)).To(Succeed()) - }) - }) -}) - -func makeCFSpace(namespace string, displayName string) *korifiv1alpha1.CFSpace { - return &korifiv1alpha1.CFSpace{ - ObjectMeta: metav1.ObjectMeta{ - Name: uuid.NewString(), - Namespace: namespace, - Labels: map[string]string{korifiv1alpha1.SpaceNameKey: displayName}, - }, - Spec: korifiv1alpha1.CFSpaceSpec{ - DisplayName: displayName, - }, - } -} diff --git a/controllers/webhooks/workloads/cftask_validator_test.go b/controllers/webhooks/workloads/cftask_validator_test.go deleted file mode 100644 index 1b5337de8..000000000 --- a/controllers/webhooks/workloads/cftask_validator_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package workloads_test - -import ( - "context" - - korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" - "code.cloudfoundry.org/korifi/controllers/controllers/workloads/testutils" - "code.cloudfoundry.org/korifi/controllers/webhooks" - "code.cloudfoundry.org/korifi/controllers/webhooks/workloads" - "code.cloudfoundry.org/korifi/tools/k8s" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -var _ = Describe("CFTask Creation", func() { - var ( - cfTask *korifiv1alpha1.CFTask - creationErr error - ) - - BeforeEach(func() { - cfApp := makeCFApp(testutils.PrefixedGUID("cfapp"), rootNamespace, testutils.PrefixedGUID("appName")) - Expect(adminClient.Create(context.Background(), cfApp)).To(Succeed()) - - cfTask = &korifiv1alpha1.CFTask{ - ObjectMeta: metav1.ObjectMeta{ - Name: testutils.GenerateGUID(), - Namespace: rootNamespace, - }, - Spec: korifiv1alpha1.CFTaskSpec{ - Command: "echo hello", - AppRef: corev1.LocalObjectReference{ - Name: cfApp.Name, - }, - }, - } - }) - - JustBeforeEach(func() { - creationErr = adminClient.Create(context.Background(), cfTask) - }) - - It("suceeds", func() { - Expect(creationErr).NotTo(HaveOccurred()) - }) - - When("command is missing", func() { - BeforeEach(func() { - cfTask.Spec.Command = "" - }) - - It("returns a validation error", func() { - validationErr, ok := webhooks.WebhookErrorToValidationError(creationErr) - Expect(ok).To(BeTrue()) - - Expect(validationErr.Type).To(Equal(workloads.MissingRequredFieldErrorType)) - Expect(validationErr.Message).To(ContainSubstring("missing required field 'Spec.Command'")) - }) - }) - - When("the app reference is not set", func() { - BeforeEach(func() { - cfTask.Spec.AppRef = corev1.LocalObjectReference{} - }) - - It("returns a validation error", func() { - validationErr, ok := webhooks.WebhookErrorToValidationError(creationErr) - Expect(ok).To(BeTrue()) - - Expect(validationErr.Type).To(Equal(workloads.MissingRequredFieldErrorType)) - Expect(validationErr.Message).To(ContainSubstring("missing required field 'Spec.AppRef.Name'")) - }) - }) - - When("the task status is created", func() { - var seqId int64 - - BeforeEach(func() { - seqId = 0 - }) - - JustBeforeEach(func() { - Expect(creationErr).NotTo(HaveOccurred()) - - originalCfTask := cfTask.DeepCopy() - cfTask.Status = korifiv1alpha1.CFTaskStatus{ - SequenceID: seqId, - } - - creationErr = adminClient.Status().Patch(context.Background(), cfTask, client.MergeFrom(originalCfTask)) - }) - - It("suceeds", func() { - Expect(creationErr).NotTo(HaveOccurred()) - }) - - When("the sequenceID is set to a negative value", func() { - BeforeEach(func() { - seqId = -1 - }) - - It("returns a validation error", func() { - validationErr, ok := webhooks.WebhookErrorToValidationError(creationErr) - Expect(ok).To(BeTrue()) - - Expect(validationErr.Type).To(Equal(workloads.InvalidFieldValueErrorType)) - Expect(validationErr.Message).To(ContainSubstring("SequenceID cannot be negative")) - }) - }) - }) -}) - -var _ = Describe("CFTask Update", func() { - var ( - cfTask *korifiv1alpha1.CFTask - updateErr error - updateFunc func() - ) - - BeforeEach(func() { - cfApp := makeCFApp(testutils.PrefixedGUID("cfapp"), rootNamespace, testutils.PrefixedGUID("appName")) - Expect(adminClient.Create(context.Background(), cfApp)).To(Succeed()) - updateFunc = func() {} - - cfTask = &korifiv1alpha1.CFTask{ - ObjectMeta: metav1.ObjectMeta{ - Name: testutils.GenerateGUID(), - Namespace: rootNamespace, - }, - Spec: korifiv1alpha1.CFTaskSpec{ - Command: "echo hello", - AppRef: corev1.LocalObjectReference{ - Name: cfApp.Name, - }, - }, - } - Expect(adminClient.Create(context.Background(), cfTask)).To(Succeed()) - Expect(k8s.Patch(context.Background(), adminClient, cfTask, func() { - cfTask.Status = korifiv1alpha1.CFTaskStatus{} - })).To(Succeed()) - }) - - JustBeforeEach(func() { - updateErr = k8s.Patch(context.Background(), adminClient, cfTask, updateFunc) - }) - - When("canceled is not changed", func() { - BeforeEach(func() { - updateFunc = func() { - cfTask.Spec.Command = "echo ok" - } - }) - - It("succeeds", func() { - Expect(updateErr).NotTo(HaveOccurred()) - }) - }) - - When("the task gets canceled", func() { - BeforeEach(func() { - updateFunc = func() { - cfTask.Spec.Canceled = true - } - }) - - It("succeeds", func() { - Expect(updateErr).NotTo(HaveOccurred()) - }) - - When("the cftask has a succeeded contdition", func() { - BeforeEach(func() { - setStatusCondition(cfTask, korifiv1alpha1.TaskSucceededConditionType) - }) - - It("fails", func() { - Expect(updateErr).To(HaveOccurred()) - validationErr, ok := webhooks.WebhookErrorToValidationError(updateErr) - Expect(ok).To(BeTrue()) - - Expect(validationErr.Type).To(Equal(workloads.CancelationNotPossibleErrorType)) - Expect(validationErr.Message).To(ContainSubstring("cannot be canceled")) - }) - }) - - When("the cftask has a failed contdition", func() { - BeforeEach(func() { - setStatusCondition(cfTask, korifiv1alpha1.TaskFailedConditionType) - }) - - It("fails", func() { - Expect(updateErr).To(HaveOccurred()) - validationErr, ok := webhooks.WebhookErrorToValidationError(updateErr) - Expect(ok).To(BeTrue()) - - Expect(validationErr.Type).To(Equal(workloads.CancelationNotPossibleErrorType)) - Expect(validationErr.Message).To(ContainSubstring("cannot be canceled")) - }) - }) - - When("the task is already canceled before an update", func() { - BeforeEach(func() { - Expect(k8s.Patch(context.Background(), adminClient, cfTask, func() { - cfTask.Spec.Canceled = true - })).To(Succeed()) - - updateFunc = func() { - cfTask.Spec.Command = "echo foo" - } - }) - - It("succeeds", func() { - Expect(updateErr).NotTo(HaveOccurred()) - }) - }) - }) - - When("the task Status.SequenceID is updated", func() { - BeforeEach(func() { - updateFunc = func() { - cfTask.Status.SequenceID = 1 - } - }) - - It("denies the request", func() { - Expect(updateErr).To(HaveOccurred()) - validationErr, ok := webhooks.WebhookErrorToValidationError(updateErr) - Expect(ok).To(BeTrue()) - - Expect(validationErr.Type).To(Equal(workloads.ImmutableFieldModificationErrorType)) - Expect(validationErr.Message).To(ContainSubstring("SequenceID is immutable")) - }) - }) -}) - -func setStatusCondition(cftask *korifiv1alpha1.CFTask, conditionType string) { - clone := cftask.DeepCopy() - meta.SetStatusCondition(&cftask.Status.Conditions, metav1.Condition{ - Type: conditionType, - Status: metav1.ConditionTrue, - Reason: "foo", - Message: "bar", - }) - Expect(adminClient.Status().Patch(context.Background(), cftask, client.MergeFrom(clone))).To(Succeed()) - - // the status update clears any unapplied changes to the rest of the object, so reset spec changes: - cftask.Spec = clone.Spec -} diff --git a/controllers/webhooks/workloads/orgs/suite_test.go b/controllers/webhooks/workloads/orgs/suite_test.go new file mode 100644 index 000000000..fad4cec2c --- /dev/null +++ b/controllers/webhooks/workloads/orgs/suite_test.go @@ -0,0 +1,105 @@ +package orgs_test + +import ( + "context" + "path/filepath" + "testing" + "time" + + korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" + "code.cloudfoundry.org/korifi/controllers/controllers/shared" + "code.cloudfoundry.org/korifi/controllers/coordination" + "code.cloudfoundry.org/korifi/tests/helpers" + + "code.cloudfoundry.org/korifi/controllers/webhooks/finalizer" + "code.cloudfoundry.org/korifi/controllers/webhooks/validation" + "code.cloudfoundry.org/korifi/controllers/webhooks/version" + "code.cloudfoundry.org/korifi/controllers/webhooks/workloads/orgs" + "github.com/google/uuid" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/scheme" + "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" + //+kubebuilder:scaffold:imports +) + +var ( + stopManager context.CancelFunc + stopClientCache context.CancelFunc + testEnv *envtest.Environment + adminClient client.Client + adminNonSyncClient client.Client + + ctx context.Context + rootNamespace string +) + +func TestWorkloadsWebhooks(t *testing.T) { + SetDefaultEventuallyTimeout(10 * time.Second) + SetDefaultEventuallyPollingInterval(250 * time.Millisecond) + + RegisterFailHandler(Fail) + RunSpecs(t, "CFOrg Webhooks Integration Test Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + ctx = context.Background() + + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{ + filepath.Join("..", "..", "..", "..", "helm", "korifi", "controllers", "crds"), + }, + ErrorIfCRDPathMissing: true, + WebhookInstallOptions: envtest.WebhookInstallOptions{ + Paths: []string{filepath.Join("..", "..", "..", "..", "helm", "korifi", "controllers", "manifests.yaml")}, + }, + } + + adminConfig, err := testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(adminConfig).NotTo(BeNil()) + + Expect(korifiv1alpha1.AddToScheme(scheme.Scheme)).To(Succeed()) + Expect(corev1.AddToScheme(scheme.Scheme)).To(Succeed()) + + k8sManager := helpers.NewK8sManager(testEnv, filepath.Join("helm", "korifi", "controllers", "role.yaml")) + Expect(shared.SetupIndexWithManager(k8sManager)).To(Succeed()) + + adminNonSyncClient, err = client.New(testEnv.Config, client.Options{ + Scheme: scheme.Scheme, + }) + Expect(err).NotTo(HaveOccurred()) + + adminClient, stopClientCache = helpers.NewCachedClient(testEnv.Config) + + version.NewVersionWebhook("some-version").SetupWebhookWithManager(k8sManager) + finalizer.NewControllersFinalizerWebhook().SetupWebhookWithManager(k8sManager) + + uncachedClient := helpers.NewUncachedClient(k8sManager.GetConfig()) + + rootNamespace = uuid.NewString() + Expect(adminClient.Create(ctx, &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: rootNamespace, + }, + })).To(Succeed()) + + orgNameDuplicateValidator := validation.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, orgs.CFOrgEntityType)) + orgPlacementValidator := validation.NewPlacementValidator(uncachedClient, rootNamespace) + Expect(orgs.NewValidator(orgNameDuplicateValidator, orgPlacementValidator).SetupWebhookWithManager(k8sManager)).To(Succeed()) + + stopManager = helpers.StartK8sManager(k8sManager) +}) + +var _ = AfterSuite(func() { + stopClientCache() + stopManager() + Expect(testEnv.Stop()).To(Succeed()) +}) diff --git a/controllers/webhooks/workloads/cforg_validator.go b/controllers/webhooks/workloads/orgs/validator.go similarity index 75% rename from controllers/webhooks/workloads/cforg_validator.go rename to controllers/webhooks/workloads/orgs/validator.go index d603e5867..337efda10 100644 --- a/controllers/webhooks/workloads/cforg_validator.go +++ b/controllers/webhooks/workloads/orgs/validator.go @@ -1,4 +1,4 @@ -package workloads +package orgs import ( "context" @@ -19,41 +19,40 @@ import ( const ( CFOrgEntityType = "cforg" OrgDecodingErrorType = "OrgDecodingError" - maxLabelLength = 63 ) var cfOrgLog = logf.Log.WithName("cforg-validate") //+kubebuilder:webhook:path=/validate-korifi-cloudfoundry-org-v1alpha1-cforg,mutating=false,failurePolicy=fail,sideEffects=NoneOnDryRun,groups=korifi.cloudfoundry.org,resources=cforgs,verbs=create;update;delete,versions=v1alpha1,name=vcforg.korifi.cloudfoundry.org,admissionReviewVersions={v1,v1beta1} -type CFOrgValidator struct { +type Validator struct { duplicateValidator webhooks.NameValidator placementValidator webhooks.NamespaceValidator } -var _ webhook.CustomValidator = &CFOrgValidator{} +var _ webhook.CustomValidator = &Validator{} -func NewCFOrgValidator(duplicateValidator webhooks.NameValidator, placementValidator webhooks.NamespaceValidator) *CFOrgValidator { - return &CFOrgValidator{ +func NewValidator(duplicateValidator webhooks.NameValidator, placementValidator webhooks.NamespaceValidator) *Validator { + return &Validator{ duplicateValidator: duplicateValidator, placementValidator: placementValidator, } } -func (v *CFOrgValidator) SetupWebhookWithManager(mgr ctrl.Manager) error { +func (v *Validator) SetupWebhookWithManager(mgr ctrl.Manager) error { return ctrl.NewWebhookManagedBy(mgr). For(&korifiv1alpha1.CFOrg{}). WithValidator(v). Complete() } -func (v *CFOrgValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { +func (v *Validator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { org, ok := obj.(*korifiv1alpha1.CFOrg) if !ok { return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a CFOrg but got a %T", obj)) } - if len(org.Name) > maxLabelLength { + if len(org.Name) > webhooks.MaxLabelLength { return nil, errors.New("org name cannot be longer than 63 chars") } @@ -66,7 +65,7 @@ func (v *CFOrgValidator) ValidateCreate(ctx context.Context, obj runtime.Object) return nil, v.duplicateValidator.ValidateCreate(ctx, cfOrgLog, org.Namespace, org) } -func (v *CFOrgValidator) ValidateUpdate(ctx context.Context, oldObj, obj runtime.Object) (admission.Warnings, error) { +func (v *Validator) ValidateUpdate(ctx context.Context, oldObj, obj runtime.Object) (admission.Warnings, error) { org, ok := obj.(*korifiv1alpha1.CFOrg) if !ok { return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a CFOrg but got a %T", obj)) @@ -84,7 +83,7 @@ func (v *CFOrgValidator) ValidateUpdate(ctx context.Context, oldObj, obj runtime return nil, v.duplicateValidator.ValidateUpdate(ctx, cfOrgLog, org.Namespace, oldOrg, org) } -func (v *CFOrgValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { +func (v *Validator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { org, ok := obj.(*korifiv1alpha1.CFOrg) if !ok { return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a CFOrg but got a %T", obj)) diff --git a/controllers/webhooks/workloads/orgs/validator_test.go b/controllers/webhooks/workloads/orgs/validator_test.go new file mode 100644 index 000000000..0d1063335 --- /dev/null +++ b/controllers/webhooks/workloads/orgs/validator_test.go @@ -0,0 +1,209 @@ +package orgs_test + +import ( + "context" + "fmt" + "strings" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" + "code.cloudfoundry.org/korifi/tools/k8s" + + "github.com/google/uuid" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var _ = Describe("CFOrgValidatingWebhook", func() { + var ( + org *korifiv1alpha1.CFOrg + createErr error + ) + + BeforeEach(func() { + org = &korifiv1alpha1.CFOrg{ + ObjectMeta: metav1.ObjectMeta{ + Name: uuid.NewString(), + Namespace: rootNamespace, + }, + Spec: korifiv1alpha1.CFOrgSpec{ + DisplayName: "org-" + uuid.NewString(), + }, + } + }) + + JustBeforeEach(func() { + createErr = adminClient.Create(ctx, org) + }) + + Describe("Create", func() { + It("should succeed", func() { + Expect(createErr).NotTo(HaveOccurred()) + }) + + When("CFOrg is requested outside of root namespace", func() { + BeforeEach(func() { + org.Namespace = "default" + }) + + It("should fail", func() { + Expect(createErr).To(MatchError(ContainSubstring(fmt.Sprintf("Organization '%s' must be placed in the root 'cf' namespace", org.Spec.DisplayName)))) + }) + }) + + When("the CFOrg name would not be a valid label value (>63 chars)", func() { + BeforeEach(func() { + org.Name = strings.Repeat("a", 64) + }) + + It("should fail", func() { + Expect(createErr).To(MatchError(ContainSubstring("org name cannot be longer than 63 chars"))) + }) + }) + + When("another CFOrg exists with a different name", func() { + BeforeEach(func() { + Expect(adminClient.Create(ctx, &korifiv1alpha1.CFOrg{ + ObjectMeta: metav1.ObjectMeta{ + Name: uuid.NewString(), + Namespace: rootNamespace, + }, + Spec: korifiv1alpha1.CFOrgSpec{ + DisplayName: uuid.NewString(), + }, + })).To(Succeed()) + }) + + It("should succeed", func() { + Expect(createErr).NotTo(HaveOccurred()) + }) + }) + + When("another CFOrg exists with the same name", func() { + BeforeEach(func() { + Expect(adminClient.Create(ctx, &korifiv1alpha1.CFOrg{ + ObjectMeta: metav1.ObjectMeta{ + Name: uuid.NewString(), + Namespace: rootNamespace, + }, + Spec: korifiv1alpha1.CFOrgSpec{ + DisplayName: org.Spec.DisplayName, + }, + })).To(Succeed()) + }) + + It("should fail", func() { + Expect(createErr).To(MatchError(ContainSubstring("Organization '%s' already exists.", org.Spec.DisplayName))) + }) + }) + + When("another CFOrg exists with the same name(case insensitive)", func() { + BeforeEach(func() { + Expect(adminClient.Create(ctx, &korifiv1alpha1.CFOrg{ + ObjectMeta: metav1.ObjectMeta{ + Name: uuid.NewString(), + Namespace: rootNamespace, + }, + Spec: korifiv1alpha1.CFOrgSpec{ + DisplayName: strings.ToUpper(org.Spec.DisplayName), + }, + })).To(Succeed()) + }) + + It("should fail", func() { + Expect(createErr).To(MatchError(ContainSubstring(fmt.Sprintf("Organization '%s' already exists.", org.Spec.DisplayName)))) + }) + }) + }) + + Describe("Update", func() { + var updateErr error + + Describe("changing the name", func() { + var newName string + + BeforeEach(func() { + newName = uuid.NewString() + }) + + JustBeforeEach(func() { + updateErr = k8s.Patch(ctx, adminClient, org, func() { + org.Spec.DisplayName = newName + }) + }) + + It("should succeed", func() { + Expect(updateErr).NotTo(HaveOccurred()) + Expect(adminClient.Get(context.Background(), client.ObjectKeyFromObject(org), org)).To(Succeed()) + Expect(org.Spec.DisplayName).To(Equal(newName)) + }) + + When("reusing an old name", func() { + var oldName string + + BeforeEach(func() { + oldName = org.Spec.DisplayName + }) + + It("allows creating another org with the old name", func() { + Expect(updateErr).NotTo(HaveOccurred()) + + Expect(adminClient.Create(ctx, &korifiv1alpha1.CFOrg{ + ObjectMeta: metav1.ObjectMeta{ + Name: uuid.NewString(), + Namespace: rootNamespace, + }, + Spec: korifiv1alpha1.CFOrgSpec{ + DisplayName: oldName, + }, + })).To(Succeed()) + }) + }) + + When("an org with the same name already exists", func() { + BeforeEach(func() { + Expect(adminClient.Create(ctx, &korifiv1alpha1.CFOrg{ + ObjectMeta: metav1.ObjectMeta{ + Name: uuid.NewString(), + Namespace: rootNamespace, + }, + Spec: korifiv1alpha1.CFOrgSpec{ + DisplayName: newName, + }, + })).To(Succeed()) + }) + + It("should fail", func() { + Expect(updateErr).To(MatchError(ContainSubstring(fmt.Sprintf("Organization '%s' already exists.", newName)))) + }) + }) + }) + + When("not changing the name", func() { + JustBeforeEach(func() { + updateErr = k8s.Patch(ctx, adminClient, org, func() { + org.Labels = map[string]string{"foo": "bar"} + }) + }) + It("should succeed", func() { + Expect(updateErr).NotTo(HaveOccurred()) + Expect(adminClient.Get(context.Background(), client.ObjectKeyFromObject(org), org)).To(Succeed()) + Expect(org.Labels).To(HaveKeyWithValue("foo", "bar")) + }) + }) + }) + + Describe("Delete", func() { + var deleteErr error + + JustBeforeEach(func() { + deleteErr = adminNonSyncClient.Delete(ctx, org) + }) + + It("succeeds", func() { + Expect(deleteErr).NotTo(HaveOccurred()) + }) + }) +}) diff --git a/controllers/webhooks/workloads/packages/suite_test.go b/controllers/webhooks/workloads/packages/suite_test.go new file mode 100644 index 000000000..296efe5f0 --- /dev/null +++ b/controllers/webhooks/workloads/packages/suite_test.go @@ -0,0 +1,102 @@ +package packages_test + +import ( + "context" + "path/filepath" + "testing" + "time" + + korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" + "code.cloudfoundry.org/korifi/controllers/controllers/shared" + "code.cloudfoundry.org/korifi/tests/helpers" + + "code.cloudfoundry.org/korifi/controllers/webhooks/finalizer" + "code.cloudfoundry.org/korifi/controllers/webhooks/version" + "code.cloudfoundry.org/korifi/controllers/webhooks/workloads/packages" + "github.com/google/uuid" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/scheme" + "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" + //+kubebuilder:scaffold:imports +) + +var ( + stopManager context.CancelFunc + stopClientCache context.CancelFunc + testEnv *envtest.Environment + adminClient client.Client + adminNonSyncClient client.Client + + ctx context.Context + testNamespace string +) + +func TestWorkloadsWebhooks(t *testing.T) { + SetDefaultEventuallyTimeout(10 * time.Second) + SetDefaultEventuallyPollingInterval(250 * time.Millisecond) + + RegisterFailHandler(Fail) + RunSpecs(t, "CFPackage Webhooks Integration Test Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{ + filepath.Join("..", "..", "..", "..", "helm", "korifi", "controllers", "crds"), + }, + ErrorIfCRDPathMissing: true, + WebhookInstallOptions: envtest.WebhookInstallOptions{ + Paths: []string{filepath.Join("..", "..", "..", "..", "helm", "korifi", "controllers", "manifests.yaml")}, + }, + } + + adminConfig, err := testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(adminConfig).NotTo(BeNil()) + + Expect(korifiv1alpha1.AddToScheme(scheme.Scheme)).To(Succeed()) + Expect(corev1.AddToScheme(scheme.Scheme)).To(Succeed()) + + k8sManager := helpers.NewK8sManager(testEnv, filepath.Join("helm", "korifi", "controllers", "role.yaml")) + Expect(shared.SetupIndexWithManager(k8sManager)).To(Succeed()) + + adminNonSyncClient, err = client.New(testEnv.Config, client.Options{ + Scheme: scheme.Scheme, + }) + Expect(err).NotTo(HaveOccurred()) + + adminClient, stopClientCache = helpers.NewCachedClient(testEnv.Config) + + version.NewVersionWebhook("some-version").SetupWebhookWithManager(k8sManager) + finalizer.NewControllersFinalizerWebhook().SetupWebhookWithManager(k8sManager) + + Expect(packages.NewValidator().SetupWebhookWithManager(k8sManager)).To(Succeed()) + + stopManager = helpers.StartK8sManager(k8sManager) +}) + +var _ = BeforeEach(func() { + ctx = context.Background() + + testNamespace = uuid.NewString() + + Expect(adminClient.Create(ctx, &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: testNamespace, + }, + })).To(Succeed()) +}) + +var _ = AfterSuite(func() { + stopClientCache() + stopManager() + Expect(testEnv.Stop()).To(Succeed()) +}) diff --git a/controllers/webhooks/workloads/cfpackage_validator.go b/controllers/webhooks/workloads/packages/validator.go similarity index 74% rename from controllers/webhooks/workloads/cfpackage_validator.go rename to controllers/webhooks/workloads/packages/validator.go index 583777994..9f43c4d4d 100644 --- a/controllers/webhooks/workloads/cfpackage_validator.go +++ b/controllers/webhooks/workloads/packages/validator.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package workloads +package packages import ( "context" @@ -22,6 +22,7 @@ import ( "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" "code.cloudfoundry.org/korifi/controllers/webhooks" + validationwebhook "code.cloudfoundry.org/korifi/controllers/webhooks/validation" apierrors "k8s.io/apimachinery/pkg/api/errors" runtime "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" @@ -38,17 +39,17 @@ var ( //+kubebuilder:webhook:path=/validate-korifi-cloudfoundry-org-v1alpha1-cfpackage,mutating=false,failurePolicy=fail,sideEffects=None,groups=korifi.cloudfoundry.org,resources=cfpackages,verbs=update,versions=v1alpha1,name=vcfpackage.korifi.cloudfoundry.org,admissionReviewVersions={v1,v1beta1} -type CFPackageValidator struct { +type Validator struct { client client.Client } -var _ webhook.CustomValidator = &CFPackageValidator{} +var _ webhook.CustomValidator = &Validator{} -func NewCFPackageValidator() *CFPackageValidator { - return &CFPackageValidator{} +func NewValidator() *Validator { + return &Validator{} } -func (v *CFPackageValidator) SetupWebhookWithManager(mgr ctrl.Manager) error { +func (v *Validator) SetupWebhookWithManager(mgr ctrl.Manager) error { v.client = mgr.GetClient() return ctrl.NewWebhookManagedBy(mgr). @@ -57,13 +58,13 @@ func (v *CFPackageValidator) SetupWebhookWithManager(mgr ctrl.Manager) error { Complete() } -var _ webhook.CustomValidator = &CFPackageValidator{} +var _ webhook.CustomValidator = &Validator{} -func (v *CFPackageValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { +func (v *Validator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { return nil, nil } -func (v *CFPackageValidator) ValidateUpdate(ctx context.Context, oldObj runtime.Object, obj runtime.Object) (admission.Warnings, error) { +func (v *Validator) ValidateUpdate(ctx context.Context, oldObj runtime.Object, obj runtime.Object) (admission.Warnings, error) { newCFPackage, ok := obj.(*v1alpha1.CFPackage) if !ok { return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a CFPackage but got a %T", obj)) @@ -81,8 +82,8 @@ func (v *CFPackageValidator) ValidateUpdate(ctx context.Context, oldObj runtime. } if newCFPackage.Spec.Type != oldCFPackage.Spec.Type { - return nil, webhooks.ValidationError{ - Type: ImmutableFieldModificationErrorType, + return nil, validationwebhook.ValidationError{ + Type: webhooks.ImmutableFieldModificationErrorType, Message: fmt.Sprintf("package %s:%s Spec.Type is immutable", newCFPackage.Namespace, newCFPackage.Name), }.ExportJSONError() } @@ -90,6 +91,6 @@ func (v *CFPackageValidator) ValidateUpdate(ctx context.Context, oldObj runtime. return nil, nil } -func (v *CFPackageValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { +func (v *Validator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { return nil, nil } diff --git a/controllers/webhooks/workloads/cfpackage_validator_test.go b/controllers/webhooks/workloads/packages/validator_test.go similarity index 71% rename from controllers/webhooks/workloads/cfpackage_validator_test.go rename to controllers/webhooks/workloads/packages/validator_test.go index 9ac24d5fa..fe492935b 100644 --- a/controllers/webhooks/workloads/cfpackage_validator_test.go +++ b/controllers/webhooks/workloads/packages/validator_test.go @@ -1,36 +1,26 @@ -package workloads_test +package packages_test import ( "context" korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" - "code.cloudfoundry.org/korifi/controllers/controllers/workloads/testutils" "code.cloudfoundry.org/korifi/tools/k8s" + "github.com/google/uuid" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) var _ = Describe("CFPackage Validation", func() { - var ( - cfApp *korifiv1alpha1.CFApp - cfPackage *korifiv1alpha1.CFPackage - ) + var cfPackage *korifiv1alpha1.CFPackage BeforeEach(func() { - cfApp = makeCFApp(testutils.PrefixedGUID("cfapp"), rootNamespace, testutils.PrefixedGUID("appName")) - Expect(adminClient.Create(context.Background(), cfApp)).To(Succeed()) - cfPackage = &korifiv1alpha1.CFPackage{ ObjectMeta: metav1.ObjectMeta{ - Namespace: cfApp.Namespace, - Name: testutils.PrefixedGUID("cfpackage"), + Namespace: testNamespace, + Name: uuid.NewString(), }, Spec: korifiv1alpha1.CFPackageSpec{ - AppRef: v1.LocalObjectReference{ - Name: cfApp.Name, - }, Type: "bits", }, } diff --git a/controllers/webhooks/workloads/suite_integration_test.go b/controllers/webhooks/workloads/spaces/suite_test.go similarity index 52% rename from controllers/webhooks/workloads/suite_integration_test.go rename to controllers/webhooks/workloads/spaces/suite_test.go index ade5b5ea3..622510d3b 100644 --- a/controllers/webhooks/workloads/suite_integration_test.go +++ b/controllers/webhooks/workloads/spaces/suite_test.go @@ -1,4 +1,4 @@ -package workloads_test +package spaces_test import ( "context" @@ -7,21 +7,19 @@ import ( "time" korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" - "code.cloudfoundry.org/korifi/controllers/config" "code.cloudfoundry.org/korifi/controllers/controllers/shared" "code.cloudfoundry.org/korifi/controllers/coordination" - "code.cloudfoundry.org/korifi/controllers/webhooks" "code.cloudfoundry.org/korifi/tests/helpers" "code.cloudfoundry.org/korifi/controllers/webhooks/finalizer" + "code.cloudfoundry.org/korifi/controllers/webhooks/validation" "code.cloudfoundry.org/korifi/controllers/webhooks/version" - "code.cloudfoundry.org/korifi/controllers/webhooks/workloads" + "code.cloudfoundry.org/korifi/controllers/webhooks/workloads/orgs" + "code.cloudfoundry.org/korifi/controllers/webhooks/workloads/spaces" + "github.com/google/uuid" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - admissionv1beta1 "k8s.io/api/admission/v1beta1" - coordinationv1 "k8s.io/api/coordination/v1" corev1 "k8s.io/api/core/v1" - rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/scheme" "sigs.k8s.io/controller-runtime/pkg/client" @@ -37,28 +35,31 @@ var ( testEnv *envtest.Environment adminClient client.Client adminNonSyncClient client.Client -) -const rootNamespace = "cf" + ctx context.Context + rootNamespace string +) func TestWorkloadsWebhooks(t *testing.T) { SetDefaultEventuallyTimeout(10 * time.Second) SetDefaultEventuallyPollingInterval(250 * time.Millisecond) RegisterFailHandler(Fail) - RunSpecs(t, "Workloads Validating Webhooks Integration Test Suite") + RunSpecs(t, "CFSpace Webhooks Integration Test Suite") } var _ = BeforeSuite(func() { logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + ctx = context.Background() + testEnv = &envtest.Environment{ CRDDirectoryPaths: []string{ - filepath.Join("..", "..", "..", "helm", "korifi", "controllers", "crds"), + filepath.Join("..", "..", "..", "..", "helm", "korifi", "controllers", "crds"), }, ErrorIfCRDPathMissing: true, WebhookInstallOptions: envtest.WebhookInstallOptions{ - Paths: []string{filepath.Join("..", "..", "..", "helm", "korifi", "controllers", "manifests.yaml")}, + Paths: []string{filepath.Join("..", "..", "..", "..", "helm", "korifi", "controllers", "manifests.yaml")}, }, } @@ -67,10 +68,7 @@ var _ = BeforeSuite(func() { Expect(adminConfig).NotTo(BeNil()) Expect(korifiv1alpha1.AddToScheme(scheme.Scheme)).To(Succeed()) - Expect(admissionv1beta1.AddToScheme(scheme.Scheme)).To(Succeed()) Expect(corev1.AddToScheme(scheme.Scheme)).To(Succeed()) - Expect(coordinationv1.AddToScheme(scheme.Scheme)).To(Succeed()) - Expect(rbacv1.AddToScheme(scheme.Scheme)).To(Succeed()) k8sManager := helpers.NewK8sManager(testEnv, filepath.Join("helm", "korifi", "controllers", "role.yaml")) Expect(shared.SetupIndexWithManager(k8sManager)).To(Succeed()) @@ -82,38 +80,27 @@ var _ = BeforeSuite(func() { adminClient, stopClientCache = helpers.NewCachedClient(testEnv.Config) - Expect((&korifiv1alpha1.CFApp{}).SetupWebhookWithManager(k8sManager)).To(Succeed()) - - (&workloads.AppRevWebhook{}).SetupWebhookWithManager(k8sManager) + rootNamespace = uuid.NewString() + Expect(adminClient.Create(ctx, &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: rootNamespace, + }, + })).To(Succeed()) uncachedClient := helpers.NewUncachedClient(k8sManager.GetConfig()) - appNameDuplicateValidator := webhooks.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, workloads.AppEntityType)) - Expect(workloads.NewCFAppValidator(appNameDuplicateValidator).SetupWebhookWithManager(k8sManager)).To(Succeed()) - - orgNameDuplicateValidator := webhooks.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, workloads.CFOrgEntityType)) - orgPlacementValidator := webhooks.NewPlacementValidator(uncachedClient, rootNamespace) - Expect(workloads.NewCFOrgValidator(orgNameDuplicateValidator, orgPlacementValidator).SetupWebhookWithManager(k8sManager)).To(Succeed()) - - spaceNameDuplicateValidator := webhooks.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, workloads.CFSpaceEntityType)) - spacePlacementValidator := webhooks.NewPlacementValidator(uncachedClient, rootNamespace) - Expect(workloads.NewCFSpaceValidator(spaceNameDuplicateValidator, spacePlacementValidator).SetupWebhookWithManager(k8sManager)).To(Succeed()) - - Expect(workloads.NewCFTaskDefaulter(config.CFProcessDefaults{ - MemoryMB: 500, - DiskQuotaMB: 512, - }).SetupWebhookWithManager(k8sManager)).To(Succeed()) - Expect(workloads.NewCFTaskValidator().SetupWebhookWithManager(k8sManager)).To(Succeed()) + version.NewVersionWebhook("some-version").SetupWebhookWithManager(k8sManager) finalizer.NewControllersFinalizerWebhook().SetupWebhookWithManager(k8sManager) - Expect(workloads.NewCFPackageValidator().SetupWebhookWithManager(k8sManager)).To(Succeed()) - stopManager = helpers.StartK8sManager(k8sManager) + orgNameDuplicateValidator := validation.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, orgs.CFOrgEntityType)) + orgPlacementValidator := validation.NewPlacementValidator(uncachedClient, rootNamespace) + Expect(orgs.NewValidator(orgNameDuplicateValidator, orgPlacementValidator).SetupWebhookWithManager(k8sManager)).To(Succeed()) - Expect(adminClient.Create(context.Background(), &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: rootNamespace, - }, - })).To(Succeed()) + spaceNameDuplicateValidator := validation.NewDuplicateValidator(coordination.NewNameRegistry(uncachedClient, spaces.CFSpaceEntityType)) + spacePlacementValidator := validation.NewPlacementValidator(uncachedClient, rootNamespace) + Expect(spaces.NewValidator(spaceNameDuplicateValidator, spacePlacementValidator).SetupWebhookWithManager(k8sManager)).To(Succeed()) + + stopManager = helpers.StartK8sManager(k8sManager) }) var _ = AfterSuite(func() { diff --git a/controllers/webhooks/workloads/cfspace_validator.go b/controllers/webhooks/workloads/spaces/validator.go similarity index 75% rename from controllers/webhooks/workloads/cfspace_validator.go rename to controllers/webhooks/workloads/spaces/validator.go index 126869093..86e2a7a36 100644 --- a/controllers/webhooks/workloads/cfspace_validator.go +++ b/controllers/webhooks/workloads/spaces/validator.go @@ -1,4 +1,4 @@ -package workloads +package spaces import ( "context" @@ -24,34 +24,34 @@ var spaceLogger = logf.Log.WithName("cfspace-validate") //+kubebuilder:webhook:path=/validate-korifi-cloudfoundry-org-v1alpha1-cfspace,mutating=false,failurePolicy=fail,sideEffects=NoneOnDryRun,groups=korifi.cloudfoundry.org,resources=cfspaces,verbs=create;update;delete,versions=v1alpha1,name=vcfspace.korifi.cloudfoundry.org,admissionReviewVersions={v1,v1beta1} -type CFSpaceValidator struct { +type Validator struct { duplicateValidator webhooks.NameValidator placementValidator webhooks.NamespaceValidator } -var _ webhook.CustomValidator = &CFSpaceValidator{} +var _ webhook.CustomValidator = &Validator{} -func NewCFSpaceValidator(duplicateSpaceValidator webhooks.NameValidator, placementValidator webhooks.NamespaceValidator) *CFSpaceValidator { - return &CFSpaceValidator{ +func NewValidator(duplicateSpaceValidator webhooks.NameValidator, placementValidator webhooks.NamespaceValidator) *Validator { + return &Validator{ duplicateValidator: duplicateSpaceValidator, placementValidator: placementValidator, } } -func (v *CFSpaceValidator) SetupWebhookWithManager(mgr ctrl.Manager) error { +func (v *Validator) SetupWebhookWithManager(mgr ctrl.Manager) error { return ctrl.NewWebhookManagedBy(mgr). For(&korifiv1alpha1.CFSpace{}). WithValidator(v). Complete() } -func (v *CFSpaceValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { +func (v *Validator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { space, ok := obj.(*korifiv1alpha1.CFSpace) if !ok { return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a CFSpace but got a %T", obj)) } - if len(space.Name) > maxLabelLength { + if len(space.Name) > webhooks.MaxLabelLength { return nil, errors.New("space name cannot be longer than 63 chars") } @@ -63,7 +63,7 @@ func (v *CFSpaceValidator) ValidateCreate(ctx context.Context, obj runtime.Objec return nil, v.placementValidator.ValidateSpaceCreate(*space) } -func (v *CFSpaceValidator) ValidateUpdate(ctx context.Context, oldObj, obj runtime.Object) (admission.Warnings, error) { +func (v *Validator) ValidateUpdate(ctx context.Context, oldObj, obj runtime.Object) (admission.Warnings, error) { space, ok := obj.(*korifiv1alpha1.CFSpace) if !ok { return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a CFSpace but got a %T", obj)) @@ -81,7 +81,7 @@ func (v *CFSpaceValidator) ValidateUpdate(ctx context.Context, oldObj, obj runti return nil, v.duplicateValidator.ValidateUpdate(ctx, spaceLogger, oldSpace.Namespace, oldSpace, space) } -func (v *CFSpaceValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { +func (v *Validator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { space, ok := obj.(*korifiv1alpha1.CFSpace) if !ok { return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a CFSpace but got a %T", obj)) diff --git a/controllers/webhooks/workloads/spaces/validator_test.go b/controllers/webhooks/workloads/spaces/validator_test.go new file mode 100644 index 000000000..770e256f1 --- /dev/null +++ b/controllers/webhooks/workloads/spaces/validator_test.go @@ -0,0 +1,161 @@ +package spaces_test + +import ( + "fmt" + "strings" + + korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" + "code.cloudfoundry.org/korifi/tools/k8s" + + "github.com/google/uuid" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ = Describe("CFSpaceValidatingWebhook", func() { + var ( + cfSpace *korifiv1alpha1.CFSpace + orgNamespace string + createErr error + ) + + BeforeEach(func() { + orgNamespace = uuid.NewString() + Expect(adminClient.Create(ctx, &korifiv1alpha1.CFOrg{ + ObjectMeta: metav1.ObjectMeta{ + Name: orgNamespace, + Namespace: rootNamespace, + }, + Spec: korifiv1alpha1.CFOrgSpec{ + DisplayName: orgNamespace, + }, + })).To(Succeed()) + + Expect(adminClient.Create(ctx, &v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: orgNamespace, + }, + })).To(Succeed()) + + cfSpace = &korifiv1alpha1.CFSpace{ + ObjectMeta: metav1.ObjectMeta{ + Name: uuid.NewString(), + Namespace: orgNamespace, + }, + Spec: korifiv1alpha1.CFSpaceSpec{ + DisplayName: "my-space", + }, + } + }) + + JustBeforeEach(func() { + createErr = adminClient.Create(ctx, cfSpace) + }) + + Describe("creating a space", func() { + It("succeeds", func() { + Expect(createErr).To(Succeed()) + }) + + When("a corresponding CFOrg does not exist", func() { + BeforeEach(func() { + cfSpace.Namespace = "not-an-org" + Expect(adminClient.Create(ctx, &v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "not-an-org", + }, + })).To(Succeed()) + }) + + It("fails", func() { + Expect(createErr).To(MatchError(ContainSubstring("Organization 'not-an-org' does not exist for Space 'my-space'"))) + }) + }) + + When("the CFSpace name is not a valid label value (>63 chars)", func() { + BeforeEach(func() { + cfSpace.Name = strings.Repeat("a", 64) + }) + + It("should fail", func() { + Expect(createErr).To(MatchError(ContainSubstring("space name cannot be longer than 63 chars"))) + }) + }) + + When("the name already exists in the org namespace", func() { + BeforeEach(func() { + Expect(adminClient.Create(ctx, &korifiv1alpha1.CFSpace{ + ObjectMeta: metav1.ObjectMeta{ + Name: uuid.NewString(), + Namespace: orgNamespace, + }, + Spec: korifiv1alpha1.CFSpaceSpec{ + DisplayName: "my-space", + }, + })).To(Succeed()) + }) + + It("fails", func() { + Expect(createErr).To(MatchError(ContainSubstring("Name must be unique per organization"))) + }) + }) + + When("another CFSpace exists with the same name(case insensitive) in the same namespace", func() { + BeforeEach(func() { + Expect(adminClient.Create(ctx, &korifiv1alpha1.CFSpace{ + ObjectMeta: metav1.ObjectMeta{ + Name: uuid.NewString(), + Namespace: orgNamespace, + }, + Spec: korifiv1alpha1.CFSpaceSpec{ + DisplayName: strings.ToUpper("my-space"), + }, + })).To(Succeed()) + }) + + It("should fail", func() { + Expect(createErr).To(MatchError(ContainSubstring(fmt.Sprintf("Space '%s' already exists.", cfSpace.Spec.DisplayName)))) + }) + }) + }) + + Describe("renaming a space", func() { + var updateErr error + + JustBeforeEach(func() { + updateErr = k8s.Patch(ctx, adminClient, cfSpace, func() { + cfSpace.Spec.DisplayName = "another-space" + }) + }) + + It("succeeds", func() { + Expect(updateErr).NotTo(HaveOccurred()) + }) + + When("the new space name already exists in the org namespace", func() { + BeforeEach(func() { + Expect(adminClient.Create(ctx, &korifiv1alpha1.CFSpace{ + ObjectMeta: metav1.ObjectMeta{ + Name: uuid.NewString(), + Namespace: orgNamespace, + }, + Spec: korifiv1alpha1.CFSpaceSpec{ + DisplayName: "another-space", + }, + })).To(Succeed()) + }) + + It("fails", func() { + Expect(updateErr).To(MatchError(ContainSubstring("Name must be unique per organization"))) + }) + }) + }) + + Describe("deleting a space", func() { + It("can delete the space", func() { + Expect(adminNonSyncClient.Delete(ctx, cfSpace)).To(Succeed()) + }) + }) +}) diff --git a/controllers/webhooks/workloads/cftask_defaulter.go b/controllers/webhooks/workloads/tasks/defaulter.go similarity index 87% rename from controllers/webhooks/workloads/cftask_defaulter.go rename to controllers/webhooks/workloads/tasks/defaulter.go index 3b65709a2..0412540f1 100644 --- a/controllers/webhooks/workloads/cftask_defaulter.go +++ b/controllers/webhooks/workloads/tasks/defaulter.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package workloads +package tasks import ( "context" @@ -34,24 +34,24 @@ import ( var cfTaskLog = logf.Log.WithName("cftask-resource") -type CFTaskDefaulter struct { +type Defaulter struct { cfProcessDefaults config.CFProcessDefaults } -func NewCFTaskDefaulter(cfProcessDefaults config.CFProcessDefaults) *CFTaskDefaulter { - return &CFTaskDefaulter{ +func NewDefaulter(cfProcessDefaults config.CFProcessDefaults) *Defaulter { + return &Defaulter{ cfProcessDefaults: cfProcessDefaults, } } -func (d *CFTaskDefaulter) SetupWebhookWithManager(mgr ctrl.Manager) error { +func (d *Defaulter) SetupWebhookWithManager(mgr ctrl.Manager) error { return ctrl.NewWebhookManagedBy(mgr). For(&korifiv1alpha1.CFTask{}). WithDefaulter(d). Complete() } -func (d *CFTaskDefaulter) Default(ctx context.Context, obj runtime.Object) error { +func (d *Defaulter) Default(ctx context.Context, obj runtime.Object) error { cfTaskLog.V(1).Info("mutating CFTask webhook handler") cfTask, ok := obj.(*korifiv1alpha1.CFTask) diff --git a/controllers/webhooks/workloads/cftask_defaulter_test.go b/controllers/webhooks/workloads/tasks/defaulter_test.go similarity index 91% rename from controllers/webhooks/workloads/cftask_defaulter_test.go rename to controllers/webhooks/workloads/tasks/defaulter_test.go index 9d8de3965..389916a93 100644 --- a/controllers/webhooks/workloads/cftask_defaulter_test.go +++ b/controllers/webhooks/workloads/tasks/defaulter_test.go @@ -1,4 +1,4 @@ -package workloads_test +package tasks_test import ( "context" @@ -6,12 +6,12 @@ import ( "time" korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" - "code.cloudfoundry.org/korifi/controllers/controllers/workloads/testutils" "code.cloudfoundry.org/korifi/tools/k8s" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/google/uuid" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) @@ -22,13 +22,13 @@ var _ = Describe("CFTaskMutatingWebhook", func() { BeforeEach(func() { cfTask = &korifiv1alpha1.CFTask{ ObjectMeta: metav1.ObjectMeta{ - Name: testutils.GenerateGUID(), - Namespace: rootNamespace, + Name: uuid.NewString(), + Namespace: testNamespace, }, Spec: korifiv1alpha1.CFTaskSpec{ Command: "echo", AppRef: corev1.LocalObjectReference{ - Name: testutils.GenerateGUID(), + Name: uuid.NewString(), }, }, } diff --git a/controllers/webhooks/workloads/tasks/suite_test.go b/controllers/webhooks/workloads/tasks/suite_test.go new file mode 100644 index 000000000..274f5d4d1 --- /dev/null +++ b/controllers/webhooks/workloads/tasks/suite_test.go @@ -0,0 +1,107 @@ +package tasks_test + +import ( + "context" + "path/filepath" + "testing" + "time" + + korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" + "code.cloudfoundry.org/korifi/controllers/config" + "code.cloudfoundry.org/korifi/controllers/controllers/shared" + "code.cloudfoundry.org/korifi/tests/helpers" + + "code.cloudfoundry.org/korifi/controllers/webhooks/finalizer" + "code.cloudfoundry.org/korifi/controllers/webhooks/version" + "code.cloudfoundry.org/korifi/controllers/webhooks/workloads/tasks" + "github.com/google/uuid" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/scheme" + "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" + //+kubebuilder:scaffold:imports +) + +var ( + stopManager context.CancelFunc + stopClientCache context.CancelFunc + testEnv *envtest.Environment + adminClient client.Client + adminNonSyncClient client.Client + + ctx context.Context + testNamespace string +) + +func TestWorkloadsWebhooks(t *testing.T) { + SetDefaultEventuallyTimeout(10 * time.Second) + SetDefaultEventuallyPollingInterval(250 * time.Millisecond) + + RegisterFailHandler(Fail) + RunSpecs(t, "CFTask Webhooks Integration Test Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{ + filepath.Join("..", "..", "..", "..", "helm", "korifi", "controllers", "crds"), + }, + ErrorIfCRDPathMissing: true, + WebhookInstallOptions: envtest.WebhookInstallOptions{ + Paths: []string{filepath.Join("..", "..", "..", "..", "helm", "korifi", "controllers", "manifests.yaml")}, + }, + } + + adminConfig, err := testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(adminConfig).NotTo(BeNil()) + + Expect(korifiv1alpha1.AddToScheme(scheme.Scheme)).To(Succeed()) + Expect(corev1.AddToScheme(scheme.Scheme)).To(Succeed()) + + k8sManager := helpers.NewK8sManager(testEnv, filepath.Join("helm", "korifi", "controllers", "role.yaml")) + Expect(shared.SetupIndexWithManager(k8sManager)).To(Succeed()) + + adminNonSyncClient, err = client.New(testEnv.Config, client.Options{ + Scheme: scheme.Scheme, + }) + Expect(err).NotTo(HaveOccurred()) + + adminClient, stopClientCache = helpers.NewCachedClient(testEnv.Config) + + version.NewVersionWebhook("some-version").SetupWebhookWithManager(k8sManager) + finalizer.NewControllersFinalizerWebhook().SetupWebhookWithManager(k8sManager) + + Expect(tasks.NewDefaulter(config.CFProcessDefaults{ + MemoryMB: 500, + DiskQuotaMB: 512, + }).SetupWebhookWithManager(k8sManager)).To(Succeed()) + Expect(tasks.NewValidator().SetupWebhookWithManager(k8sManager)).To(Succeed()) + + stopManager = helpers.StartK8sManager(k8sManager) +}) + +var _ = BeforeEach(func() { + ctx = context.Background() + + testNamespace = uuid.NewString() + + Expect(adminClient.Create(ctx, &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: testNamespace, + }, + })).To(Succeed()) +}) + +var _ = AfterSuite(func() { + stopClientCache() + stopManager() + Expect(testEnv.Stop()).To(Succeed()) +}) diff --git a/controllers/webhooks/workloads/cftask_validator.go b/controllers/webhooks/workloads/tasks/validator.go similarity index 72% rename from controllers/webhooks/workloads/cftask_validator.go rename to controllers/webhooks/workloads/tasks/validator.go index 6f439026c..93d3cccb9 100644 --- a/controllers/webhooks/workloads/cftask_validator.go +++ b/controllers/webhooks/workloads/tasks/validator.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package workloads +package tasks import ( "context" @@ -22,6 +22,7 @@ import ( "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" "code.cloudfoundry.org/korifi/controllers/webhooks" + "code.cloudfoundry.org/korifi/controllers/webhooks/validation" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" runtime "k8s.io/apimachinery/pkg/runtime" @@ -32,10 +33,7 @@ import ( ) const ( - MissingRequredFieldErrorType = "MissingRequiredFieldError" - InvalidFieldValueErrorType = "InvalidFieldValueError" - ImmutableFieldModificationErrorType = "ImmutableFieldModificationError" - CancelationNotPossibleErrorType = "CancelationNotPossibleError" + CancelationNotPossibleErrorType = "CancelationNotPossibleError" ) // log is for logging in this package. @@ -43,24 +41,24 @@ var cftasklog = logf.Log.WithName("cftask-resource") //+kubebuilder:webhook:path=/validate-korifi-cloudfoundry-org-v1alpha1-cftask,mutating=false,failurePolicy=fail,sideEffects=None,groups=korifi.cloudfoundry.org,resources=cftasks;cftasks/status,verbs=create;update,versions=v1alpha1,name=vcftask.korifi.cloudfoundry.org,admissionReviewVersions={v1,v1beta1} -type CFTaskValidator struct{} +type Validator struct{} -var _ webhook.CustomValidator = &CFTaskValidator{} +var _ webhook.CustomValidator = &Validator{} -func NewCFTaskValidator() *CFTaskValidator { - return &CFTaskValidator{} +func NewValidator() *Validator { + return &Validator{} } -func (v *CFTaskValidator) SetupWebhookWithManager(mgr ctrl.Manager) error { +func (v *Validator) SetupWebhookWithManager(mgr ctrl.Manager) error { return ctrl.NewWebhookManagedBy(mgr). For(&v1alpha1.CFTask{}). WithValidator(v). Complete() } -var _ webhook.CustomValidator = &CFTaskValidator{} +var _ webhook.CustomValidator = &Validator{} -func (v *CFTaskValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { +func (v *Validator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { task, ok := obj.(*v1alpha1.CFTask) if !ok { return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a CFTask but got a %T", obj)) @@ -69,15 +67,15 @@ func (v *CFTaskValidator) ValidateCreate(ctx context.Context, obj runtime.Object cftasklog.V(1).Info("validate task creation", "namespace", task.Namespace, "name", task.Name) if len(task.Spec.Command) == 0 { - return nil, webhooks.ValidationError{ - Type: MissingRequredFieldErrorType, + return nil, validation.ValidationError{ + Type: webhooks.MissingRequredFieldErrorType, Message: fmt.Sprintf("task %s:%s is missing required field 'Spec.Command'", task.Namespace, task.Name), }.ExportJSONError() } if task.Spec.AppRef.Name == "" { - return nil, webhooks.ValidationError{ - Type: MissingRequredFieldErrorType, + return nil, validation.ValidationError{ + Type: webhooks.MissingRequredFieldErrorType, Message: fmt.Sprintf("task %s:%s is missing required field 'Spec.AppRef.Name'", task.Namespace, task.Name), }.ExportJSONError() } @@ -85,7 +83,7 @@ func (v *CFTaskValidator) ValidateCreate(ctx context.Context, obj runtime.Object return nil, nil } -func (v *CFTaskValidator) ValidateUpdate(ctx context.Context, oldObj runtime.Object, obj runtime.Object) (admission.Warnings, error) { +func (v *Validator) ValidateUpdate(ctx context.Context, oldObj runtime.Object, obj runtime.Object) (admission.Warnings, error) { newTask, ok := obj.(*v1alpha1.CFTask) if !ok { return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a CFTask but got a %T", obj)) @@ -103,15 +101,15 @@ func (v *CFTaskValidator) ValidateUpdate(ctx context.Context, oldObj runtime.Obj } if newTask.Status.SequenceID < 0 { - return nil, webhooks.ValidationError{ - Type: InvalidFieldValueErrorType, + return nil, validation.ValidationError{ + Type: webhooks.InvalidFieldValueErrorType, Message: fmt.Sprintf("task %s:%s Status.SequenceID cannot be negative", newTask.Namespace, newTask.Name), }.ExportJSONError() } if oldTask.Status.SequenceID != 0 && newTask.Status.SequenceID != oldTask.Status.SequenceID { - return nil, webhooks.ValidationError{ - Type: ImmutableFieldModificationErrorType, + return nil, validation.ValidationError{ + Type: webhooks.ImmutableFieldModificationErrorType, Message: fmt.Sprintf("task %s:%s Status.SequenceID is immutable", newTask.Namespace, newTask.Name), }.ExportJSONError() } @@ -128,7 +126,7 @@ func (v *CFTaskValidator) ValidateUpdate(ctx context.Context, oldObj runtime.Obj } if state != "" { - return nil, webhooks.ValidationError{ + return nil, validation.ValidationError{ Type: CancelationNotPossibleErrorType, Message: fmt.Sprintf("Task state is %s and therefore cannot be canceled", state), }.ExportJSONError() @@ -137,6 +135,6 @@ func (v *CFTaskValidator) ValidateUpdate(ctx context.Context, oldObj runtime.Obj return nil, nil } -func (v *CFTaskValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { +func (v *Validator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { return nil, nil } diff --git a/controllers/webhooks/workloads/tasks/validator_test.go b/controllers/webhooks/workloads/tasks/validator_test.go new file mode 100644 index 000000000..bc75e87e0 --- /dev/null +++ b/controllers/webhooks/workloads/tasks/validator_test.go @@ -0,0 +1,231 @@ +package tasks_test + +import ( + "context" + + korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" + "code.cloudfoundry.org/korifi/controllers/webhooks" + "code.cloudfoundry.org/korifi/controllers/webhooks/validation" + "code.cloudfoundry.org/korifi/controllers/webhooks/workloads/tasks" + "code.cloudfoundry.org/korifi/tools/k8s" + "github.com/google/uuid" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ = Describe("CFTask Validator", func() { + var ( + cfTask *korifiv1alpha1.CFTask + creationErr error + ) + + BeforeEach(func() { + cfTask = &korifiv1alpha1.CFTask{ + ObjectMeta: metav1.ObjectMeta{ + Name: uuid.NewString(), + Namespace: testNamespace, + }, + Spec: korifiv1alpha1.CFTaskSpec{ + Command: "echo hello", + AppRef: corev1.LocalObjectReference{ + Name: uuid.NewString(), + }, + }, + } + }) + + Describe("create", func() { + JustBeforeEach(func() { + creationErr = adminClient.Create(context.Background(), cfTask) + }) + + It("suceeds", func() { + Expect(creationErr).NotTo(HaveOccurred()) + }) + + When("command is missing", func() { + BeforeEach(func() { + cfTask.Spec.Command = "" + }) + + It("returns a validation error", func() { + validationErr, ok := validation.WebhookErrorToValidationError(creationErr) + Expect(ok).To(BeTrue()) + + Expect(validationErr.Type).To(Equal(webhooks.MissingRequredFieldErrorType)) + Expect(validationErr.Message).To(ContainSubstring("missing required field 'Spec.Command'")) + }) + }) + + When("the app reference is not set", func() { + BeforeEach(func() { + cfTask.Spec.AppRef = corev1.LocalObjectReference{} + }) + + It("returns a validation error", func() { + validationErr, ok := validation.WebhookErrorToValidationError(creationErr) + Expect(ok).To(BeTrue()) + + Expect(validationErr.Type).To(Equal(webhooks.MissingRequredFieldErrorType)) + Expect(validationErr.Message).To(ContainSubstring("missing required field 'Spec.AppRef.Name'")) + }) + }) + + When("the task status is created", func() { + var seqId int64 + + BeforeEach(func() { + seqId = 0 + }) + + JustBeforeEach(func() { + Expect(creationErr).NotTo(HaveOccurred()) + + creationErr = k8s.Patch(ctx, adminClient, cfTask, func() { + cfTask.Status.SequenceID = seqId + }) + }) + + It("suceeds", func() { + Expect(creationErr).NotTo(HaveOccurred()) + }) + + When("the sequenceID is set to a negative value", func() { + BeforeEach(func() { + seqId = -1 + }) + + It("returns a validation error", func() { + validationErr, ok := validation.WebhookErrorToValidationError(creationErr) + Expect(ok).To(BeTrue()) + + Expect(validationErr.Type).To(Equal(webhooks.InvalidFieldValueErrorType)) + Expect(validationErr.Message).To(ContainSubstring("SequenceID cannot be negative")) + }) + }) + }) + }) + + Describe("update", func() { + var ( + updateErr error + updateFunc func() + ) + + BeforeEach(func() { + Expect(adminClient.Create(ctx, cfTask)).To(Succeed()) + Expect(k8s.Patch(ctx, adminClient, cfTask, func() { + cfTask.Status = korifiv1alpha1.CFTaskStatus{} + })).To(Succeed()) + + updateFunc = func() {} + }) + + JustBeforeEach(func() { + updateErr = k8s.Patch(context.Background(), adminClient, cfTask, updateFunc) + }) + + When("canceled is not changed", func() { + BeforeEach(func() { + updateFunc = func() { + cfTask.Spec.Command = "echo ok" + } + }) + + It("succeeds", func() { + Expect(updateErr).NotTo(HaveOccurred()) + }) + }) + + When("the task gets canceled", func() { + BeforeEach(func() { + updateFunc = func() { + cfTask.Spec.Canceled = true + } + }) + + It("succeeds", func() { + Expect(updateErr).NotTo(HaveOccurred()) + }) + + When("the cftask has a succeeded contdition", func() { + BeforeEach(func() { + setStatusCondition(cfTask, korifiv1alpha1.TaskSucceededConditionType) + }) + + It("fails", func() { + Expect(updateErr).To(HaveOccurred()) + validationErr, ok := validation.WebhookErrorToValidationError(updateErr) + Expect(ok).To(BeTrue()) + + Expect(validationErr.Type).To(Equal(tasks.CancelationNotPossibleErrorType)) + Expect(validationErr.Message).To(ContainSubstring("cannot be canceled")) + }) + }) + + When("the cftask has a failed condition", func() { + BeforeEach(func() { + setStatusCondition(cfTask, korifiv1alpha1.TaskFailedConditionType) + }) + + It("fails", func() { + Expect(updateErr).To(HaveOccurred()) + validationErr, ok := validation.WebhookErrorToValidationError(updateErr) + Expect(ok).To(BeTrue()) + + Expect(validationErr.Type).To(Equal(tasks.CancelationNotPossibleErrorType)) + Expect(validationErr.Message).To(ContainSubstring("cannot be canceled")) + }) + }) + + When("the task is already canceled before an update", func() { + BeforeEach(func() { + Expect(k8s.Patch(context.Background(), adminClient, cfTask, func() { + cfTask.Spec.Canceled = true + })).To(Succeed()) + + updateFunc = func() { + cfTask.Spec.Command = "echo foo" + } + }) + + It("succeeds", func() { + Expect(updateErr).NotTo(HaveOccurred()) + }) + }) + }) + + When("the task Status.SequenceID is updated", func() { + BeforeEach(func() { + updateFunc = func() { + cfTask.Status.SequenceID = 1 + } + }) + + It("denies the request", func() { + Expect(updateErr).To(HaveOccurred()) + validationErr, ok := validation.WebhookErrorToValidationError(updateErr) + Expect(ok).To(BeTrue()) + + Expect(validationErr.Type).To(Equal(webhooks.ImmutableFieldModificationErrorType)) + Expect(validationErr.Message).To(ContainSubstring("SequenceID is immutable")) + }) + }) + }) +}) + +func setStatusCondition(cftask *korifiv1alpha1.CFTask, conditionType string) { + GinkgoHelper() + + Expect(k8s.Patch(ctx, adminClient, cftask, func() { + meta.SetStatusCondition(&cftask.Status.Conditions, metav1.Condition{ + Type: conditionType, + Status: metav1.ConditionTrue, + Reason: "foo", + Message: "bar", + }) + })).To(Succeed()) +} diff --git a/kpack-image-builder/controllers/buildworkload_controller_test.go b/kpack-image-builder/controllers/buildworkload_controller_test.go index a8dbee540..dc9b638f0 100644 --- a/kpack-image-builder/controllers/buildworkload_controller_test.go +++ b/kpack-image-builder/controllers/buildworkload_controller_test.go @@ -6,7 +6,6 @@ import ( "strconv" korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" - "code.cloudfoundry.org/korifi/controllers/controllers/workloads/testutils" "code.cloudfoundry.org/korifi/kpack-image-builder/controllers" "code.cloudfoundry.org/korifi/tests/helpers" "code.cloudfoundry.org/korifi/tools/image" @@ -872,7 +871,7 @@ var _ = Describe("BuildWorkloadReconciler", func() { source2 := source source2.Registry.Image += "2" - buildWorkload2 = buildWorkloadObject(testutils.GenerateGUID(), namespaceGUID, source2, env, services, reconcilerName, buildpacks) + buildWorkload2 = buildWorkloadObject(uuid.NewString(), namespaceGUID, source2, env, services, reconcilerName, buildpacks) Expect(adminClient.Create(ctx, buildWorkload2)).To(Succeed()) Eventually(func(g Gomega) { g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(buildWorkload2), buildWorkload2)).To(Succeed()) @@ -906,7 +905,7 @@ var _ = Describe("BuildWorkloadReconciler", func() { BeforeEach(func() { Expect(adminClient.Create(ctx, &buildv1alpha2.Build{ ObjectMeta: metav1.ObjectMeta{ - Name: testutils.GenerateGUID(), + Name: uuid.NewString(), Namespace: namespaceGUID, Labels: map[string]string{ buildv1alpha2.ImageLabel: appGUID, @@ -954,7 +953,7 @@ var _ = Describe("BuildWorkloadReconciler", func() { BeforeEach(func() { source3 := source source3.Registry.Image += "3" - buildWorkload3 = buildWorkloadObject(testutils.GenerateGUID(), namespaceGUID, source3, env, services, reconcilerName, buildpacks) + buildWorkload3 = buildWorkloadObject(uuid.NewString(), namespaceGUID, source3, env, services, reconcilerName, buildpacks) Expect(adminClient.Create(ctx, buildWorkload3)).To(Succeed()) Eventually(func(g Gomega) { g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(buildWorkload3), buildWorkload3)).To(Succeed()) @@ -988,7 +987,7 @@ var _ = Describe("BuildWorkloadReconciler", func() { g.Expect(buildWorkload.Labels).To(HaveKey(controllers.ImageGenerationKey)) }).Should(Succeed()) - buildWorkload2 = buildWorkloadObject(testutils.GenerateGUID(), namespaceGUID, source, env, services, reconcilerName, buildpacks) + buildWorkload2 = buildWorkloadObject(uuid.NewString(), namespaceGUID, source, env, services, reconcilerName, buildpacks) Expect(adminClient.Create(ctx, buildWorkload2)).To(Succeed()) Eventually(func(g Gomega) { g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(buildWorkload2), buildWorkload2)).To(Succeed()) @@ -1132,7 +1131,7 @@ var _ = Describe("BuildWorkloadReconciler", func() { When("there is another BuildWorkload referring to the kpack.Build", func() { BeforeEach(func() { - otherBuildWorkload := buildWorkloadObject(testutils.GenerateGUID(), namespaceGUID, source, env, services, reconcilerName, buildpacks) + otherBuildWorkload := buildWorkloadObject(uuid.NewString(), namespaceGUID, source, env, services, reconcilerName, buildpacks) otherBuildWorkload.Labels[controllers.ImageGenerationKey] = "1" Expect(adminClient.Create(ctx, otherBuildWorkload)).To(Succeed()) }) diff --git a/statefulset-runner/api/v1/pod_webhook_test.go b/statefulset-runner/api/v1/pod_webhook_test.go index 9bc41ddc6..a4e4c973b 100644 --- a/statefulset-runner/api/v1/pod_webhook_test.go +++ b/statefulset-runner/api/v1/pod_webhook_test.go @@ -1,8 +1,7 @@ package v1_test import ( - "code.cloudfoundry.org/korifi/controllers/controllers/workloads/testutils" - + "github.com/google/uuid" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" @@ -17,7 +16,7 @@ var _ = Describe("StatefulSet Runner Pod Mutating Webhook", func() { ) BeforeEach(func() { - namespace = testutils.PrefixedGUID("ns") + namespace = uuid.NewString() err := adminClient.Create(ctx, &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: namespace, @@ -27,7 +26,7 @@ var _ = Describe("StatefulSet Runner Pod Mutating Webhook", func() { stsPod = &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ - Name: testutils.PrefixedGUID("pod") + "-1", + Name: uuid.NewString() + "-1", Namespace: namespace, }, Spec: corev1.PodSpec{ diff --git a/tests/matchers/validation_error.go b/tests/matchers/validation_error.go index 6dbf63ee5..cf109d636 100644 --- a/tests/matchers/validation_error.go +++ b/tests/matchers/validation_error.go @@ -4,7 +4,7 @@ import ( "encoding/json" "fmt" - "code.cloudfoundry.org/korifi/controllers/webhooks" + "code.cloudfoundry.org/korifi/controllers/webhooks/validation" k8serrors "k8s.io/apimachinery/pkg/api/errors" //. "github.com/onsi/gomega" @@ -23,7 +23,7 @@ func BeValidationError(expectedErrorType string, messageMatcher types.GomegaMatc return &beValidationErrorMatcher{ matcher: &matchers.AndMatcher{ Matchers: []types.GomegaMatcher{ - gomega.BeAssignableToTypeOf(webhooks.ValidationError{}), + gomega.BeAssignableToTypeOf(validation.ValidationError{}), gstruct.MatchAllFields(gstruct.Fields{ "Type": gomega.Equal(expectedErrorType), "Message": messageMatcher, @@ -57,16 +57,16 @@ func (m *beValidationErrorMatcher) NegatedFailureMessage(actual interface{}) (me return m.matcher.NegatedFailureMessage(validationErr) } -func toValidationError(actual interface{}) (webhooks.ValidationError, error) { +func toValidationError(actual interface{}) (validation.ValidationError, error) { actualErr, ok := actual.(*k8serrors.StatusError) if !ok { - return webhooks.ValidationError{}, fmt.Errorf("%v is not a status error", actual) + return validation.ValidationError{}, fmt.Errorf("%v is not a status error", actual) } - var validationErr webhooks.ValidationError + var validationErr validation.ValidationError err := json.Unmarshal([]byte(actualErr.Status().Reason), &validationErr) if err != nil { - return webhooks.ValidationError{}, fmt.Errorf("%v is not a validation error: %w", actualErr.Error(), err) + return validation.ValidationError{}, fmt.Errorf("%v is not a validation error: %w", actualErr.Error(), err) } return validationErr, nil diff --git a/tools/image/client_test.go b/tools/image/client_test.go index ee4f1e41d..42e66c1a0 100644 --- a/tools/image/client_test.go +++ b/tools/image/client_test.go @@ -3,10 +3,10 @@ package image_test import ( "os" - "code.cloudfoundry.org/korifi/controllers/controllers/workloads/testutils" "code.cloudfoundry.org/korifi/tests/helpers/oci" "code.cloudfoundry.org/korifi/tools/image" v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/uuid" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) @@ -317,7 +317,7 @@ var _ = Describe("Client", func() { tagsToDelete = []string{"jim", "bob"} pushRef = reg.RepoName if pushRef == "" { - repoName = testutils.GenerateGUID() + repoName = uuid.NewString() pushRef = reg.PathPrefix + "/" + repoName } var err error diff --git a/tools/image/image_suite_test.go b/tools/image/image_suite_test.go index 95ce75524..3daf58c4b 100644 --- a/tools/image/image_suite_test.go +++ b/tools/image/image_suite_test.go @@ -5,10 +5,10 @@ import ( "os" "testing" - "code.cloudfoundry.org/korifi/controllers/controllers/workloads/testutils" "code.cloudfoundry.org/korifi/tests/helpers/oci" "code.cloudfoundry.org/korifi/tools/dockercfg" "code.cloudfoundry.org/korifi/tools/image" + "github.com/google/uuid" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "gopkg.in/yaml.v2" @@ -75,7 +75,7 @@ var _ = BeforeSuite(func() { containerRegistry = oci.NewContainerRegistry("user", "password") - secretName = testutils.GenerateGUID() + secretName = uuid.NewString() dockerConfigs := []dockercfg.DockerServerConfig{} for _, reg := range registries { @@ -100,7 +100,7 @@ var _ = BeforeSuite(func() { g.Expect(getErr).NotTo(HaveOccurred()) }).Should(Succeed()) - serviceAccountName = testutils.GenerateGUID() + serviceAccountName = uuid.NewString() Expect(k8sClient.Create(ctx, &corev1.ServiceAccount{ ObjectMeta: metav1.ObjectMeta{ Namespace: "default",