From 6a52d81f561efed033109f9a812c4c90d8ccff58 Mon Sep 17 00:00:00 2001 From: Danail Branekov Date: Tue, 9 Apr 2024 14:10:30 +0000 Subject: [PATCH] App restart is synchronous * Introduce `CFApp.Status.ActualState` to reflect the actual state of the app. The start/stop/restart operations waits for that field to become equal to the desired app state * Introduce `CFProcess.Status.ActualInstances` to reflect the actual app workload instances * Introduce `AppWorkload.Status.ActualInstances` to reflect the actual statefulset replicas * The state chain is tested by a test in the `crds` suite * The existing crd test is deleted as it has become obsolete by the test above fixes #3036 Co-authored-by: Georgi Sabev --- api/main.go | 14 +- api/repositories/app_repository.go | 22 +- api/repositories/app_repository_test.go | 40 ++-- api/repositories/conditions/await.go | 21 +- api/repositories/conditions/await_test.go | 105 ++++++--- .../conditions/conditions_suite_test.go | 6 +- .../deployment_repository_test.go | 2 +- api/repositories/fakeawaiter/await.go | 68 ++++++ api/repositories/org_repository.go | 4 +- api/repositories/org_repository_test.go | 5 +- api/repositories/package_repository.go | 4 +- api/repositories/package_repository_test.go | 5 +- api/repositories/repositories_suite_test.go | 43 +--- api/repositories/role_repository_test.go | 5 +- .../service_binding_repository.go | 4 +- .../service_binding_repository_test.go | 7 +- .../service_instance_repository.go | 4 +- .../service_instance_repository_test.go | 5 +- api/repositories/shared.go | 5 +- api/repositories/space_repository.go | 4 +- api/repositories/space_repository_test.go | 7 +- api/repositories/task_repository.go | 4 +- api/repositories/task_repository_test.go | 5 +- controllers/api/v1alpha1/appworkload_types.go | 3 + controllers/api/v1alpha1/cfapp_types.go | 11 +- controllers/api/v1alpha1/cfprocess_types.go | 3 + controllers/api/v1alpha1/constants.go | 4 +- .../controllers/workloads/cfapp_controller.go | 45 +++- .../workloads/cfapp_controller_test.go | 2 +- .../workloads/cfprocess_controller.go | 19 +- .../workloads/cfprocess_controller_test.go | 17 ++ .../controllers/workloads/suite_test.go | 2 +- .../korifi.cloudfoundry.org_appworkloads.yaml | 3 + .../crds/korifi.cloudfoundry.org_cfapps.yaml | 3 + .../korifi.cloudfoundry.org_cfprocesses.yaml | 3 + scripts/run-tests.sh | 1 - .../controllers/appworkload_controller.go | 2 + .../appworkload_controller_test.go | 17 ++ tests/crds/apps_test.go | 208 ++++++++++++++++++ tests/crds/crds_suite_test.go | 136 ++++++++++-- tests/crds/crds_test.go | 189 ---------------- tests/e2e/e2e_suite_test.go | 150 ++----------- tests/helpers/cf.go | 10 +- tests/helpers/fail_handler/handler.go | 68 ++++-- tests/helpers/fail_handler/pods.go | 35 +++ tests/helpers/k8s.go | 33 +-- tests/helpers/zip.go | 64 ++++++ tests/smoke/smoke_suite_test.go | 33 +-- 48 files changed, 877 insertions(+), 573 deletions(-) create mode 100644 api/repositories/fakeawaiter/await.go create mode 100644 tests/crds/apps_test.go delete mode 100644 tests/crds/crds_test.go create mode 100644 tests/helpers/zip.go diff --git a/api/main.go b/api/main.go index b8a413c0f..867959ebc 100644 --- a/api/main.go +++ b/api/main.go @@ -127,14 +127,14 @@ func main() { privilegedCRClient, userClientFactory, nsPermissions, - conditions.NewConditionAwaiter[*korifiv1alpha1.CFOrg, korifiv1alpha1.CFOrgList](conditionTimeout), + conditions.NewStateAwaiter[*korifiv1alpha1.CFOrg, korifiv1alpha1.CFOrgList](conditionTimeout), ) spaceRepo := repositories.NewSpaceRepo( namespaceRetriever, orgRepo, userClientFactory, nsPermissions, - conditions.NewConditionAwaiter[*korifiv1alpha1.CFSpace, korifiv1alpha1.CFSpaceList](conditionTimeout), + conditions.NewStateAwaiter[*korifiv1alpha1.CFSpace, korifiv1alpha1.CFSpaceList](conditionTimeout), ) processRepo := repositories.NewProcessRepo( namespaceRetriever, @@ -148,7 +148,7 @@ func main() { namespaceRetriever, userClientFactory, nsPermissions, - conditions.NewConditionAwaiter[*korifiv1alpha1.CFApp, korifiv1alpha1.CFAppList](conditionTimeout), + conditions.NewStateAwaiter[*korifiv1alpha1.CFApp, korifiv1alpha1.CFAppList](conditionTimeout), ) dropletRepo := repositories.NewDropletRepo( userClientFactory, @@ -184,19 +184,19 @@ func main() { nsPermissions, toolsregistry.NewRepositoryCreator(cfg.ContainerRegistryType), cfg.ContainerRepositoryPrefix, - conditions.NewConditionAwaiter[*korifiv1alpha1.CFPackage, korifiv1alpha1.CFPackageList](conditionTimeout), + conditions.NewStateAwaiter[*korifiv1alpha1.CFPackage, korifiv1alpha1.CFPackageList](conditionTimeout), ) serviceInstanceRepo := repositories.NewServiceInstanceRepo( namespaceRetriever, userClientFactory, nsPermissions, - conditions.NewConditionAwaiter[*korifiv1alpha1.CFServiceInstance, korifiv1alpha1.CFServiceInstanceList](conditionTimeout), + conditions.NewStateAwaiter[*korifiv1alpha1.CFServiceInstance, korifiv1alpha1.CFServiceInstanceList](conditionTimeout), ) serviceBindingRepo := repositories.NewServiceBindingRepo( namespaceRetriever, userClientFactory, nsPermissions, - conditions.NewConditionAwaiter[*korifiv1alpha1.CFServiceBinding, korifiv1alpha1.CFServiceBindingList](conditionTimeout), + conditions.NewStateAwaiter[*korifiv1alpha1.CFServiceBinding, korifiv1alpha1.CFServiceBindingList](conditionTimeout), ) buildpackRepo := repositories.NewBuildpackRepository(cfg.BuilderName, userClientFactory, @@ -223,7 +223,7 @@ func main() { userClientFactory, namespaceRetriever, nsPermissions, - conditions.NewConditionAwaiter[*korifiv1alpha1.CFTask, korifiv1alpha1.CFTaskList](conditionTimeout), + conditions.NewStateAwaiter[*korifiv1alpha1.CFTask, korifiv1alpha1.CFTaskList](conditionTimeout), ) metricsRepo := repositories.NewMetricsRepo(userClientFactory) diff --git a/api/repositories/app_repository.go b/api/repositories/app_repository.go index 197a10c8a..7b5cc0c49 100644 --- a/api/repositories/app_repository.go +++ b/api/repositories/app_repository.go @@ -42,20 +42,20 @@ type AppRepo struct { namespaceRetriever NamespaceRetriever userClientFactory authorization.UserK8sClientFactory namespacePermissions *authorization.NamespacePermissions - appConditionAwaiter ConditionAwaiter[*korifiv1alpha1.CFApp] + appAwaiter Awaiter[*korifiv1alpha1.CFApp] } func NewAppRepo( namespaceRetriever NamespaceRetriever, userClientFactory authorization.UserK8sClientFactory, authPerms *authorization.NamespacePermissions, - appConditionAwaiter ConditionAwaiter[*korifiv1alpha1.CFApp], + appAwaiter Awaiter[*korifiv1alpha1.CFApp], ) *AppRepo { return &AppRepo{ namespaceRetriever: namespaceRetriever, userClientFactory: userClientFactory, namespacePermissions: authPerms, - appConditionAwaiter: appConditionAwaiter, + appAwaiter: appAwaiter, } } @@ -431,7 +431,7 @@ func (f *AppRepo) SetCurrentDroplet(ctx context.Context, authInfo authorization. return CurrentDropletRecord{}, fmt.Errorf("failed to set app droplet: %w", apierrors.FromK8sError(err, AppResourceType)) } - _, err = f.appConditionAwaiter.AwaitCondition(ctx, userClient, cfApp, shared.StatusConditionReady) + _, err = f.appAwaiter.AwaitCondition(ctx, userClient, cfApp, shared.StatusConditionReady) if err != nil { return CurrentDropletRecord{}, fmt.Errorf("failed to await the app staged condition: %w", apierrors.FromK8sError(err, AppResourceType)) } @@ -456,12 +456,22 @@ func (f *AppRepo) SetAppDesiredState(ctx context.Context, authInfo authorization } err = k8s.PatchResource(ctx, userClient, cfApp, func() { - cfApp.Spec.DesiredState = korifiv1alpha1.DesiredState(message.DesiredState) + cfApp.Spec.DesiredState = korifiv1alpha1.AppState(message.DesiredState) }) if err != nil { return AppRecord{}, fmt.Errorf("failed to set app desired state: %w", apierrors.FromK8sError(err, AppResourceType)) } + if _, err := f.appAwaiter.AwaitState(ctx, userClient, cfApp, func(actualApp *korifiv1alpha1.CFApp) error { + desiredState := korifiv1alpha1.AppState(message.DesiredState) + if (actualApp.Spec.DesiredState == desiredState) && (actualApp.Status.ActualState == desiredState) { + return nil + } + return fmt.Errorf("expected actual state to be %s; it is currently %s", message.DesiredState, actualApp.Status.ActualState) + }); err != nil { + return AppRecord{}, apierrors.FromK8sError(err, AppResourceType) + } + return cfAppToAppRecord(*cfApp), nil } @@ -608,7 +618,7 @@ func (m *CreateAppMessage) toCFApp() korifiv1alpha1.CFApp { }, Spec: korifiv1alpha1.CFAppSpec{ DisplayName: m.Name, - DesiredState: korifiv1alpha1.DesiredState(m.State), + DesiredState: korifiv1alpha1.AppState(m.State), EnvSecretName: GenerateEnvSecretName(guid), Lifecycle: korifiv1alpha1.Lifecycle{ Type: korifiv1alpha1.LifecycleType(m.Lifecycle.Type), diff --git a/api/repositories/app_repository_test.go b/api/repositories/app_repository_test.go index 58c037153..d9b990b31 100644 --- a/api/repositories/app_repository_test.go +++ b/api/repositories/app_repository_test.go @@ -11,6 +11,7 @@ import ( "code.cloudfoundry.org/korifi/api/authorization" apierrors "code.cloudfoundry.org/korifi/api/errors" . "code.cloudfoundry.org/korifi/api/repositories" + "code.cloudfoundry.org/korifi/api/repositories/fakeawaiter" korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" "code.cloudfoundry.org/korifi/controllers/controllers/shared" "code.cloudfoundry.org/korifi/controllers/controllers/workloads/env" @@ -37,7 +38,7 @@ const ( var _ = Describe("AppRepository", func() { var ( - conditionAwaiter *FakeAwaiter[ + appAwaiter *fakeawaiter.FakeAwaiter[ *korifiv1alpha1.CFApp, korifiv1alpha1.CFAppList, *korifiv1alpha1.CFAppList, @@ -49,12 +50,12 @@ var _ = Describe("AppRepository", func() { ) BeforeEach(func() { - conditionAwaiter = &FakeAwaiter[ + appAwaiter = &fakeawaiter.FakeAwaiter[ *korifiv1alpha1.CFApp, korifiv1alpha1.CFAppList, *korifiv1alpha1.CFAppList, ]{} - appRepo = NewAppRepo(namespaceRetriever, userClientFactory, nsPerms, conditionAwaiter) + appRepo = NewAppRepo(namespaceRetriever, userClientFactory, nsPerms, appAwaiter) cfOrg = createOrgWithCleanup(ctx, prefixedGUID("org")) cfSpace = createSpaceWithCleanup(ctx, cfOrg.Name, prefixedGUID("space1")) @@ -1115,8 +1116,8 @@ var _ = Describe("AppRepository", func() { }) It("awaits the ready condition", func() { - Expect(conditionAwaiter.AwaitConditionCallCount()).To(Equal(1)) - obj, conditionType := conditionAwaiter.AwaitConditionArgsForCall(0) + Expect(appAwaiter.AwaitConditionCallCount()).To(Equal(1)) + obj, conditionType := appAwaiter.AwaitConditionArgsForCall(0) Expect(obj.GetName()).To(Equal(appGUID)) Expect(obj.GetNamespace()).To(Equal(cfSpace.Name)) Expect(conditionType).To(Equal(shared.StatusConditionReady)) @@ -1139,7 +1140,7 @@ var _ = Describe("AppRepository", func() { When("the app never becomes ready", func() { BeforeEach(func() { - conditionAwaiter.AwaitConditionReturns(&korifiv1alpha1.CFApp{}, errors.New("time-out-err")) + appAwaiter.AwaitConditionReturns(&korifiv1alpha1.CFApp{}, errors.New("time-out-err")) }) It("returns an error", func() { @@ -1167,7 +1168,6 @@ var _ = Describe("AppRepository", func() { Describe("SetDesiredState", func() { const ( - appName = "some-app" appStartedValue = "STARTED" appStoppedValue = "STOPPED" ) @@ -1186,8 +1186,10 @@ var _ = Describe("AppRepository", func() { }) JustBeforeEach(func() { - appGUID = uuid.NewString() - _ = createAppCR(ctx, k8sClient, appName, appGUID, cfSpace.Name, initialAppState) + appGUID = cfApp.Name + Expect(k8s.PatchResource(ctx, k8sClient, cfApp, func() { + cfApp.Spec.DesiredState = korifiv1alpha1.AppState(initialAppState) + })).To(Succeed()) appRecord, err := appRepo.SetAppDesiredState(ctx, authInfo, SetAppDesiredStateMessage{ AppGUID: appGUID, SpaceGUID: cfSpace.Name, @@ -1213,9 +1215,15 @@ var _ = Describe("AppRepository", func() { It("returns the updated app record", func() { Expect(returnedAppRecord.GUID).To(Equal(appGUID)) - Expect(returnedAppRecord.Name).To(Equal(appName)) + Expect(returnedAppRecord.Name).To(Equal(cfApp.Spec.DisplayName)) Expect(returnedAppRecord.SpaceGUID).To(Equal(cfSpace.Name)) - Expect(returnedAppRecord.State).To(Equal(DesiredState("STARTED"))) + }) + + It("waits for the desired state", func() { + Expect(appAwaiter.AwaitStateCallCount()).To(Equal(1)) + actualCFApp := appAwaiter.AwaitStateArgsForCall(0) + Expect(actualCFApp.GetName()).To(Equal(cfApp.Name)) + Expect(actualCFApp.GetNamespace()).To(Equal(cfApp.Namespace)) }) It("changes the desired state of the App", func() { @@ -1235,11 +1243,11 @@ var _ = Describe("AppRepository", func() { Expect(returnedErr).ToNot(HaveOccurred()) }) - It("returns the updated app record", func() { - Expect(returnedAppRecord.GUID).To(Equal(appGUID)) - Expect(returnedAppRecord.Name).To(Equal(appName)) - Expect(returnedAppRecord.SpaceGUID).To(Equal(cfSpace.Name)) - Expect(returnedAppRecord.State).To(Equal(DesiredState("STOPPED"))) + It("waits for the desired state", func() { + Expect(appAwaiter.AwaitStateCallCount()).To(Equal(1)) + actualCFApp := appAwaiter.AwaitStateArgsForCall(0) + Expect(actualCFApp.GetName()).To(Equal(cfApp.Name)) + Expect(actualCFApp.GetNamespace()).To(Equal(cfApp.Namespace)) }) It("changes the desired state of the App", func() { diff --git a/api/repositories/conditions/await.go b/api/repositories/conditions/await.go index 0427215da..710c9eb9e 100644 --- a/api/repositories/conditions/await.go +++ b/api/repositories/conditions/await.go @@ -24,13 +24,13 @@ type Awaiter[T RuntimeObjectWithStatusConditions, L any, PL ObjectList[L]] struc timeout time.Duration } -func NewConditionAwaiter[T RuntimeObjectWithStatusConditions, L any, PL ObjectList[L]](timeout time.Duration) *Awaiter[T, L, PL] { +func NewStateAwaiter[T RuntimeObjectWithStatusConditions, L any, PL ObjectList[L]](timeout time.Duration) *Awaiter[T, L, PL] { return &Awaiter[T, L, PL]{ timeout: timeout, } } -func (a *Awaiter[T, L, PL]) AwaitCondition(ctx context.Context, k8sClient client.WithWatch, object client.Object, conditionType string) (T, error) { +func (a *Awaiter[T, L, PL]) AwaitState(ctx context.Context, k8sClient client.WithWatch, object client.Object, checkState func(T) error) (T, error) { var empty T objList := PL(new(L)) @@ -47,18 +47,29 @@ func (a *Awaiter[T, L, PL]) AwaitCondition(ctx context.Context, k8sClient client } defer watch.Stop() + var stateCheckErr error for e := range watch.ResultChan() { obj, ok := e.Object.(T) if !ok { continue } - if meta.IsStatusConditionTrue(obj.StatusConditions(), conditionType) { + stateCheckErr = checkState(obj) + if stateCheckErr == nil { return obj, nil } } - return empty, fmt.Errorf("object %s:%s did not get the %s condition within timeout period %d ms", - object.GetNamespace(), object.GetName(), conditionType, a.timeout.Milliseconds(), + return empty, fmt.Errorf("object %s/%s did not match desired state within %d ms: %s", + object.GetNamespace(), object.GetName(), a.timeout.Milliseconds(), stateCheckErr.Error(), ) } + +func (a *Awaiter[T, L, PL]) AwaitCondition(ctx context.Context, k8sClient client.WithWatch, object client.Object, conditionType string) (T, error) { + return a.AwaitState(ctx, k8sClient, object, func(obj T) error { + if meta.IsStatusConditionTrue(obj.StatusConditions(), conditionType) { + return nil + } + return fmt.Errorf("expected the %s condition to be true", conditionType) + }) +} diff --git a/api/repositories/conditions/await_test.go b/api/repositories/conditions/await_test.go index 14bb74052..71974612a 100644 --- a/api/repositories/conditions/await_test.go +++ b/api/repositories/conditions/await_test.go @@ -1,20 +1,20 @@ package conditions_test import ( - "context" + "errors" "sync" "time" "code.cloudfoundry.org/korifi/api/repositories/conditions" korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" + "code.cloudfoundry.org/korifi/tools/k8s" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/controller-runtime/pkg/client" ) -var _ = Describe("Await", func() { +var _ = Describe("StateAwaiter", func() { var ( awaiter *conditions.Awaiter[*korifiv1alpha1.CFTask, korifiv1alpha1.CFTaskList, *korifiv1alpha1.CFTaskList] task *korifiv1alpha1.CFTask @@ -22,8 +22,27 @@ var _ = Describe("Await", func() { awaitErr error ) + asyncPatchTask := func(patchTask func(*korifiv1alpha1.CFTask)) { + wg := &sync.WaitGroup{} + wg.Add(1) + + go func() { + defer GinkgoRecover() + defer wg.Done() + + patchedTask := task.DeepCopy() + Expect(k8s.Patch(ctx, k8sClient, patchedTask, func() { + patchTask(patchedTask) + })).To(Succeed()) + }() + + DeferCleanup(func() { + wg.Wait() + }) + } + BeforeEach(func() { - awaiter = conditions.NewConditionAwaiter[*korifiv1alpha1.CFTask, korifiv1alpha1.CFTaskList](time.Second) + awaiter = conditions.NewStateAwaiter[*korifiv1alpha1.CFTask, korifiv1alpha1.CFTaskList](time.Second) awaitedTask = nil awaitErr = nil @@ -34,48 +53,68 @@ var _ = Describe("Await", func() { }, } - Expect(k8sClient.Create(context.Background(), task)).To(Succeed()) + Expect(k8sClient.Create(ctx, task)).To(Succeed()) }) - JustBeforeEach(func() { - awaitedTask, awaitErr = awaiter.AwaitCondition(context.Background(), k8sClient, task, korifiv1alpha1.TaskInitializedConditionType) - }) + Describe("AwaitState", func() { + JustBeforeEach(func() { + awaitedTask, awaitErr = awaiter.AwaitState(ctx, k8sClient, task, func(actualTask *korifiv1alpha1.CFTask) error { + if actualTask.Status.DropletRef.Name == "" { + return errors.New("droplet ref not set") + } - It("returns an error as the condition never becomes true", func() { - Expect(awaitErr).To(MatchError(ContainSubstring("did not get the Initialized condition"))) - }) + return nil + }) + }) - When("the condition becomes true", func() { - var wg sync.WaitGroup + It("returns an error as the desired state is never reached", func() { + Expect(awaitErr).To(MatchError(ContainSubstring("droplet ref not set"))) + }) - BeforeEach(func() { - wg.Add(1) + When("the desired state is reached", func() { + BeforeEach(func() { + asyncPatchTask(func(cfTask *korifiv1alpha1.CFTask) { + cfTask.Status.DropletRef.Name = "some-droplet" + }) + }) - go func() { - defer GinkgoRecover() - defer wg.Done() + It("succeeds and returns the updated object", func() { + Expect(awaitErr).NotTo(HaveOccurred()) + Expect(awaitedTask).NotTo(BeNil()) - taskCopy := task.DeepCopy() - meta.SetStatusCondition(&taskCopy.Status.Conditions, metav1.Condition{ - Type: korifiv1alpha1.TaskInitializedConditionType, - Status: metav1.ConditionTrue, - Reason: "initialized", - }) + Expect(awaitedTask.Name).To(Equal(task.Name)) + Expect(awaitedTask.Status.DropletRef.Name).To(Equal("some-droplet")) + }) + }) + }) - Expect(k8sClient.Status().Patch(context.Background(), taskCopy, client.MergeFrom(task))).To(Succeed()) - }() + Describe("AwaitCondition", func() { + JustBeforeEach(func() { + awaitedTask, awaitErr = awaiter.AwaitCondition(ctx, k8sClient, task, korifiv1alpha1.TaskInitializedConditionType) }) - AfterEach(func() { - wg.Wait() + It("returns an error as the condition never becomes true", func() { + Expect(awaitErr).To(MatchError(ContainSubstring("expected the Initialized condition to be true"))) }) - It("succeeds and returns the updated object", func() { - Expect(awaitErr).NotTo(HaveOccurred()) - Expect(awaitedTask).NotTo(BeNil()) + When("the condition becomes true", func() { + BeforeEach(func() { + asyncPatchTask(func(cfTask *korifiv1alpha1.CFTask) { + meta.SetStatusCondition(&cfTask.Status.Conditions, metav1.Condition{ + Type: korifiv1alpha1.TaskInitializedConditionType, + Status: metav1.ConditionTrue, + Reason: "initialized", + }) + }) + }) + + It("succeeds and returns the updated object", func() { + Expect(awaitErr).NotTo(HaveOccurred()) + Expect(awaitedTask).NotTo(BeNil()) - Expect(awaitedTask.Name).To(Equal(task.Name)) - Expect(meta.IsStatusConditionTrue(awaitedTask.Status.Conditions, korifiv1alpha1.TaskInitializedConditionType)).To(BeTrue()) + Expect(awaitedTask.Name).To(Equal(task.Name)) + Expect(meta.IsStatusConditionTrue(awaitedTask.Status.Conditions, korifiv1alpha1.TaskInitializedConditionType)).To(BeTrue()) + }) }) }) }) diff --git a/api/repositories/conditions/conditions_suite_test.go b/api/repositories/conditions/conditions_suite_test.go index caca4a8f3..c91784369 100644 --- a/api/repositories/conditions/conditions_suite_test.go +++ b/api/repositories/conditions/conditions_suite_test.go @@ -25,12 +25,14 @@ func TestConditions(t *testing.T) { } var ( + ctx context.Context testEnv *envtest.Environment k8sClient client.WithWatch namespace string ) var _ = BeforeSuite(func() { + ctx = context.Background() logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) testEnv = &envtest.Environment{ @@ -58,9 +60,9 @@ var _ = AfterSuite(func() { var _ = BeforeEach(func() { namespace = "test-ns-" + uuid.NewString()[:8] - Expect(k8sClient.Create(context.Background(), &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}})).To(Succeed()) + Expect(k8sClient.Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}})).To(Succeed()) }) var _ = AfterEach(func() { - Expect(k8sClient.Delete(context.Background(), &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}})).To(Succeed()) + Expect(k8sClient.Delete(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}})).To(Succeed()) }) diff --git a/api/repositories/deployment_repository_test.go b/api/repositories/deployment_repository_test.go index 1c66cb051..d0f20c149 100644 --- a/api/repositories/deployment_repository_test.go +++ b/api/repositories/deployment_repository_test.go @@ -164,7 +164,7 @@ var _ = Describe("DeploymentRepository", func() { Expect(createErr).NotTo(HaveOccurred()) Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(cfApp), cfApp)).To(Succeed()) - Expect(cfApp.Spec.DesiredState).To(Equal(korifiv1alpha1.DesiredState("STARTED"))) + Expect(cfApp.Spec.DesiredState).To(Equal(korifiv1alpha1.AppState("STARTED"))) }) It("does not change the app droplet", func() { diff --git a/api/repositories/fakeawaiter/await.go b/api/repositories/fakeawaiter/await.go new file mode 100644 index 000000000..4fda21020 --- /dev/null +++ b/api/repositories/fakeawaiter/await.go @@ -0,0 +1,68 @@ +package fakeawaiter + +import ( + "context" + + "code.cloudfoundry.org/korifi/api/repositories/conditions" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type FakeAwaiter[T conditions.RuntimeObjectWithStatusConditions, L any, PL conditions.ObjectList[L]] struct { + awaitStateCalls []struct { + obj client.Object + } + awaitConditionCalls []struct { + obj client.Object + conditionType string + } + AwaitStateStub func(context.Context, client.WithWatch, client.Object, func(T) error) (T, error) + AwaitConditionStub func(context.Context, client.WithWatch, client.Object, string) (T, error) +} + +func (a *FakeAwaiter[T, L, PL]) AwaitState(ctx context.Context, k8sClient client.WithWatch, object client.Object, checkState func(T) error) (T, error) { + a.awaitStateCalls = append(a.awaitStateCalls, struct { + obj client.Object + }{ + object, + }) + + return object.(T), nil +} + +func (a *FakeAwaiter[T, L, PL]) AwaitStateCallCount() int { + return len(a.awaitStateCalls) +} + +func (a *FakeAwaiter[T, L, PL]) AwaitStateArgsForCall(i int) client.Object { + return a.awaitStateCalls[i].obj +} + +func (a *FakeAwaiter[T, L, PL]) AwaitCondition(ctx context.Context, k8sClient client.WithWatch, object client.Object, conditionType string) (T, error) { + a.awaitConditionCalls = append(a.awaitConditionCalls, struct { + obj client.Object + conditionType string + }{ + object, + conditionType, + }) + + if a.AwaitConditionStub == nil { + return object.(T), nil + } + + return a.AwaitConditionStub(ctx, k8sClient, object, conditionType) +} + +func (a *FakeAwaiter[T, L, PL]) AwaitConditionReturns(object T, err error) { + a.AwaitConditionStub = func(ctx context.Context, k8sClient client.WithWatch, object client.Object, conditionType string) (T, error) { + return object.(T), err + } +} + +func (a *FakeAwaiter[T, L, PL]) AwaitConditionCallCount() int { + return len(a.awaitConditionCalls) +} + +func (a *FakeAwaiter[T, L, PL]) AwaitConditionArgsForCall(i int) (client.Object, string) { + return a.awaitConditionCalls[i].obj, a.awaitConditionCalls[i].conditionType +} diff --git a/api/repositories/org_repository.go b/api/repositories/org_repository.go index f99dc4b88..449c9422a 100644 --- a/api/repositories/org_repository.go +++ b/api/repositories/org_repository.go @@ -58,7 +58,7 @@ type OrgRepo struct { privilegedClient client.WithWatch userClientFactory authorization.UserK8sClientFactory nsPerms *authorization.NamespacePermissions - conditionAwaiter ConditionAwaiter[*korifiv1alpha1.CFOrg] + conditionAwaiter Awaiter[*korifiv1alpha1.CFOrg] } func NewOrgRepo( @@ -66,7 +66,7 @@ func NewOrgRepo( privilegedClient client.WithWatch, userClientFactory authorization.UserK8sClientFactory, nsPerms *authorization.NamespacePermissions, - conditionAwaiter ConditionAwaiter[*korifiv1alpha1.CFOrg], + conditionAwaiter Awaiter[*korifiv1alpha1.CFOrg], ) *OrgRepo { return &OrgRepo{ rootNamespace: rootNamespace, diff --git a/api/repositories/org_repository_test.go b/api/repositories/org_repository_test.go index 07a081614..a3fd81202 100644 --- a/api/repositories/org_repository_test.go +++ b/api/repositories/org_repository_test.go @@ -8,6 +8,7 @@ import ( "code.cloudfoundry.org/korifi/api/authorization" apierrors "code.cloudfoundry.org/korifi/api/errors" "code.cloudfoundry.org/korifi/api/repositories" + "code.cloudfoundry.org/korifi/api/repositories/fakeawaiter" korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" "code.cloudfoundry.org/korifi/controllers/controllers/shared" "code.cloudfoundry.org/korifi/tests/matchers" @@ -25,7 +26,7 @@ import ( var _ = Describe("OrgRepository", func() { var ( - conditionAwaiter *FakeAwaiter[ + conditionAwaiter *fakeawaiter.FakeAwaiter[ *korifiv1alpha1.CFOrg, korifiv1alpha1.CFOrgList, *korifiv1alpha1.CFOrgList, @@ -34,7 +35,7 @@ var _ = Describe("OrgRepository", func() { ) BeforeEach(func() { - conditionAwaiter = &FakeAwaiter[ + conditionAwaiter = &fakeawaiter.FakeAwaiter[ *korifiv1alpha1.CFOrg, korifiv1alpha1.CFOrgList, *korifiv1alpha1.CFOrgList, diff --git a/api/repositories/package_repository.go b/api/repositories/package_repository.go index b44bfd2f5..e9af01b5a 100644 --- a/api/repositories/package_repository.go +++ b/api/repositories/package_repository.go @@ -45,7 +45,7 @@ type PackageRepo struct { namespacePermissions *authorization.NamespacePermissions repositoryCreator RepositoryCreator repositoryPrefix string - awaiter ConditionAwaiter[*korifiv1alpha1.CFPackage] + awaiter Awaiter[*korifiv1alpha1.CFPackage] } func NewPackageRepo( @@ -54,7 +54,7 @@ func NewPackageRepo( authPerms *authorization.NamespacePermissions, repositoryCreator RepositoryCreator, repositoryPrefix string, - awaiter ConditionAwaiter[*korifiv1alpha1.CFPackage], + awaiter Awaiter[*korifiv1alpha1.CFPackage], ) *PackageRepo { return &PackageRepo{ userClientFactory: userClientFactory, diff --git a/api/repositories/package_repository_test.go b/api/repositories/package_repository_test.go index efeb07bbf..61279f39a 100644 --- a/api/repositories/package_repository_test.go +++ b/api/repositories/package_repository_test.go @@ -9,6 +9,7 @@ import ( apierrors "code.cloudfoundry.org/korifi/api/errors" "code.cloudfoundry.org/korifi/api/repositories" "code.cloudfoundry.org/korifi/api/repositories/fake" + "code.cloudfoundry.org/korifi/api/repositories/fakeawaiter" korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" "code.cloudfoundry.org/korifi/tests/matchers" "code.cloudfoundry.org/korifi/tools" @@ -28,7 +29,7 @@ import ( var _ = Describe("PackageRepository", func() { var ( repoCreator *fake.RepositoryCreator - conditionAwaiter *FakeAwaiter[ + conditionAwaiter *fakeawaiter.FakeAwaiter[ *korifiv1alpha1.CFPackage, korifiv1alpha1.CFPackageList, *korifiv1alpha1.CFPackageList, @@ -42,7 +43,7 @@ var _ = Describe("PackageRepository", func() { BeforeEach(func() { repoCreator = new(fake.RepositoryCreator) - conditionAwaiter = &FakeAwaiter[ + conditionAwaiter = &fakeawaiter.FakeAwaiter[ *korifiv1alpha1.CFPackage, korifiv1alpha1.CFPackageList, *korifiv1alpha1.CFPackageList, diff --git a/api/repositories/repositories_suite_test.go b/api/repositories/repositories_suite_test.go index 011d1c10f..7bc922a3e 100644 --- a/api/repositories/repositories_suite_test.go +++ b/api/repositories/repositories_suite_test.go @@ -10,7 +10,6 @@ import ( "code.cloudfoundry.org/korifi/api/authorization" "code.cloudfoundry.org/korifi/api/authorization/testhelpers" "code.cloudfoundry.org/korifi/api/repositories" - "code.cloudfoundry.org/korifi/api/repositories/conditions" korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" "code.cloudfoundry.org/korifi/tools" "code.cloudfoundry.org/korifi/tools/k8s" @@ -138,44 +137,6 @@ var _ = AfterEach(func() { Expect(k8sClient.Delete(context.Background(), &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: rootNamespace}})).To(Succeed()) }) -type FakeAwaiter[T conditions.RuntimeObjectWithStatusConditions, L any, PL conditions.ObjectList[L]] struct { - invocations []struct { - obj client.Object - conditionType string - } - AwaitConditionStub func(context.Context, client.WithWatch, client.Object, string) (T, error) -} - -func (a *FakeAwaiter[T, L, PL]) AwaitCondition(ctx context.Context, k8sClient client.WithWatch, object client.Object, conditionType string) (T, error) { - a.invocations = append(a.invocations, struct { - obj client.Object - conditionType string - }{ - object, - conditionType, - }) - - if a.AwaitConditionStub == nil { - return object.(T), nil - } - - return a.AwaitConditionStub(ctx, k8sClient, object, conditionType) -} - -func (a *FakeAwaiter[T, L, PL]) AwaitConditionReturns(object T, err error) { - a.AwaitConditionStub = func(ctx context.Context, k8sClient client.WithWatch, object client.Object, conditionType string) (T, error) { - return object.(T), err - } -} - -func (a *FakeAwaiter[T, L, PL]) AwaitConditionCallCount() int { - return len(a.invocations) -} - -func (a *FakeAwaiter[T, L, PL]) AwaitConditionArgsForCall(i int) (client.Object, string) { - return a.invocations[i].obj, a.invocations[i].conditionType -} - func createOrgWithCleanup(ctx context.Context, displayName string) *korifiv1alpha1.CFOrg { guid := uuid.NewString() cfOrg := &korifiv1alpha1.CFOrg{ @@ -301,6 +262,8 @@ func prefixedGUID(prefix string) string { } func createAppCR(ctx context.Context, k8sClient client.Client, appName, appGUID, spaceGUID, desiredState string) *korifiv1alpha1.CFApp { + GinkgoHelper() + toReturn := &korifiv1alpha1.CFApp{ ObjectMeta: metav1.ObjectMeta{ Name: appGUID, @@ -308,7 +271,7 @@ func createAppCR(ctx context.Context, k8sClient client.Client, appName, appGUID, }, Spec: korifiv1alpha1.CFAppSpec{ DisplayName: appName, - DesiredState: korifiv1alpha1.DesiredState(desiredState), + DesiredState: korifiv1alpha1.AppState(desiredState), Lifecycle: korifiv1alpha1.Lifecycle{ Type: "buildpack", Data: korifiv1alpha1.LifecycleData{ diff --git a/api/repositories/role_repository_test.go b/api/repositories/role_repository_test.go index d61892038..7b6af9c35 100644 --- a/api/repositories/role_repository_test.go +++ b/api/repositories/role_repository_test.go @@ -9,6 +9,7 @@ import ( apierrors "code.cloudfoundry.org/korifi/api/errors" "code.cloudfoundry.org/korifi/api/repositories" "code.cloudfoundry.org/korifi/api/repositories/fake" + "code.cloudfoundry.org/korifi/api/repositories/fakeawaiter" korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" "code.cloudfoundry.org/korifi/tests/matchers" "code.cloudfoundry.org/korifi/tools" @@ -43,12 +44,12 @@ var _ = Describe("RoleRepository", func() { "cf_user": {Name: rootNamespaceUserRole.Name}, "admin": {Name: adminRole.Name, Propagate: true}, } - orgRepo := repositories.NewOrgRepo(rootNamespace, k8sClient, userClientFactory, nsPerms, &FakeAwaiter[ + orgRepo := repositories.NewOrgRepo(rootNamespace, k8sClient, userClientFactory, nsPerms, &fakeawaiter.FakeAwaiter[ *korifiv1alpha1.CFOrg, korifiv1alpha1.CFOrgList, *korifiv1alpha1.CFOrgList, ]{}) - spaceRepo := repositories.NewSpaceRepo(namespaceRetriever, orgRepo, userClientFactory, nsPerms, &FakeAwaiter[ + spaceRepo := repositories.NewSpaceRepo(namespaceRetriever, orgRepo, userClientFactory, nsPerms, &fakeawaiter.FakeAwaiter[ *korifiv1alpha1.CFSpace, korifiv1alpha1.CFSpaceList, *korifiv1alpha1.CFSpaceList, diff --git a/api/repositories/service_binding_repository.go b/api/repositories/service_binding_repository.go index e364edca2..3443bb865 100644 --- a/api/repositories/service_binding_repository.go +++ b/api/repositories/service_binding_repository.go @@ -32,14 +32,14 @@ type ServiceBindingRepo struct { userClientFactory authorization.UserK8sClientFactory namespacePermissions *authorization.NamespacePermissions namespaceRetriever NamespaceRetriever - bindingConditionAwaiter ConditionAwaiter[*korifiv1alpha1.CFServiceBinding] + bindingConditionAwaiter Awaiter[*korifiv1alpha1.CFServiceBinding] } func NewServiceBindingRepo( namespaceRetriever NamespaceRetriever, userClientFactory authorization.UserK8sClientFactory, namespacePermissions *authorization.NamespacePermissions, - bindingConditionAwaiter ConditionAwaiter[*korifiv1alpha1.CFServiceBinding], + bindingConditionAwaiter Awaiter[*korifiv1alpha1.CFServiceBinding], ) *ServiceBindingRepo { return &ServiceBindingRepo{ userClientFactory: userClientFactory, diff --git a/api/repositories/service_binding_repository_test.go b/api/repositories/service_binding_repository_test.go index 22646ce1c..b24348842 100644 --- a/api/repositories/service_binding_repository_test.go +++ b/api/repositories/service_binding_repository_test.go @@ -7,6 +7,7 @@ import ( "code.cloudfoundry.org/korifi/api/authorization" apierrors "code.cloudfoundry.org/korifi/api/errors" "code.cloudfoundry.org/korifi/api/repositories" + "code.cloudfoundry.org/korifi/api/repositories/fakeawaiter" korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" "code.cloudfoundry.org/korifi/tests/matchers" "code.cloudfoundry.org/korifi/tools" @@ -31,7 +32,7 @@ var _ = Describe("ServiceBindingRepo", func() { appGUID string serviceInstanceGUID string bindingName *string - conditionAwaiter *FakeAwaiter[ + conditionAwaiter *fakeawaiter.FakeAwaiter[ *korifiv1alpha1.CFServiceBinding, korifiv1alpha1.CFServiceBindingList, *korifiv1alpha1.CFServiceBindingList, @@ -40,7 +41,7 @@ var _ = Describe("ServiceBindingRepo", func() { BeforeEach(func() { testCtx = context.Background() - conditionAwaiter = &FakeAwaiter[ + conditionAwaiter = &fakeawaiter.FakeAwaiter[ *korifiv1alpha1.CFServiceBinding, korifiv1alpha1.CFServiceBindingList, *korifiv1alpha1.CFServiceBindingList, @@ -60,7 +61,7 @@ var _ = Describe("ServiceBindingRepo", func() { }, Spec: korifiv1alpha1.CFAppSpec{ DisplayName: "some-app", - DesiredState: korifiv1alpha1.DesiredState(repositories.StoppedState), + DesiredState: korifiv1alpha1.AppState(repositories.StoppedState), Lifecycle: korifiv1alpha1.Lifecycle{ Type: "buildpack", Data: korifiv1alpha1.LifecycleData{ diff --git a/api/repositories/service_instance_repository.go b/api/repositories/service_instance_repository.go index 04fcd101f..d115fcdc7 100644 --- a/api/repositories/service_instance_repository.go +++ b/api/repositories/service_instance_repository.go @@ -40,14 +40,14 @@ type ServiceInstanceRepo struct { namespaceRetriever NamespaceRetriever userClientFactory authorization.UserK8sClientFactory namespacePermissions *authorization.NamespacePermissions - awaiter ConditionAwaiter[*korifiv1alpha1.CFServiceInstance] + awaiter Awaiter[*korifiv1alpha1.CFServiceInstance] } func NewServiceInstanceRepo( namespaceRetriever NamespaceRetriever, userClientFactory authorization.UserK8sClientFactory, namespacePermissions *authorization.NamespacePermissions, - awaiter ConditionAwaiter[*korifiv1alpha1.CFServiceInstance], + awaiter Awaiter[*korifiv1alpha1.CFServiceInstance], ) *ServiceInstanceRepo { return &ServiceInstanceRepo{ namespaceRetriever: namespaceRetriever, diff --git a/api/repositories/service_instance_repository_test.go b/api/repositories/service_instance_repository_test.go index 3b7bdd70b..49b13c043 100644 --- a/api/repositories/service_instance_repository_test.go +++ b/api/repositories/service_instance_repository_test.go @@ -9,6 +9,7 @@ import ( apierrors "code.cloudfoundry.org/korifi/api/errors" "code.cloudfoundry.org/korifi/api/repositories" + "code.cloudfoundry.org/korifi/api/repositories/fakeawaiter" korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" "code.cloudfoundry.org/korifi/tests/matchers" "code.cloudfoundry.org/korifi/tools" @@ -29,7 +30,7 @@ var _ = Describe("ServiceInstanceRepository", func() { var ( testCtx context.Context serviceInstanceRepo *repositories.ServiceInstanceRepo - conditionAwaiter *FakeAwaiter[ + conditionAwaiter *fakeawaiter.FakeAwaiter[ *korifiv1alpha1.CFServiceInstance, korifiv1alpha1.CFServiceInstanceList, *korifiv1alpha1.CFServiceInstanceList, @@ -42,7 +43,7 @@ var _ = Describe("ServiceInstanceRepository", func() { BeforeEach(func() { testCtx = context.Background() - conditionAwaiter = &FakeAwaiter[ + conditionAwaiter = &fakeawaiter.FakeAwaiter[ *korifiv1alpha1.CFServiceInstance, korifiv1alpha1.CFServiceInstanceList, *korifiv1alpha1.CFServiceInstanceList, diff --git a/api/repositories/shared.go b/api/repositories/shared.go index ae347271b..95e5ba9a1 100644 --- a/api/repositories/shared.go +++ b/api/repositories/shared.go @@ -22,8 +22,9 @@ type RepositoryCreator interface { CreateRepository(ctx context.Context, name string) error } -type ConditionAwaiter[T runtime.Object] interface { - AwaitCondition(ctx context.Context, userClient client.WithWatch, object client.Object, conditionType string) (T, error) +type Awaiter[T runtime.Object] interface { + AwaitState(context.Context, client.WithWatch, client.Object, func(T) error) (T, error) + AwaitCondition(context.Context, client.WithWatch, client.Object, string) (T, error) } func getLastUpdatedTime(obj client.Object) *time.Time { diff --git a/api/repositories/space_repository.go b/api/repositories/space_repository.go index 6e4a3cf2e..6f21699b4 100644 --- a/api/repositories/space_repository.go +++ b/api/repositories/space_repository.go @@ -60,7 +60,7 @@ type SpaceRepo struct { namespaceRetriever NamespaceRetriever userClientFactory authorization.UserK8sClientFactory nsPerms *authorization.NamespacePermissions - conditionAwaiter ConditionAwaiter[*korifiv1alpha1.CFSpace] + conditionAwaiter Awaiter[*korifiv1alpha1.CFSpace] } func NewSpaceRepo( @@ -68,7 +68,7 @@ func NewSpaceRepo( orgRepo *OrgRepo, userClientFactory authorization.UserK8sClientFactory, nsPerms *authorization.NamespacePermissions, - conditionAwaiter ConditionAwaiter[*korifiv1alpha1.CFSpace], + conditionAwaiter Awaiter[*korifiv1alpha1.CFSpace], ) *SpaceRepo { return &SpaceRepo{ orgRepo: orgRepo, diff --git a/api/repositories/space_repository_test.go b/api/repositories/space_repository_test.go index f80af0027..db64ad119 100644 --- a/api/repositories/space_repository_test.go +++ b/api/repositories/space_repository_test.go @@ -7,6 +7,7 @@ import ( apierrors "code.cloudfoundry.org/korifi/api/errors" "code.cloudfoundry.org/korifi/api/repositories" + "code.cloudfoundry.org/korifi/api/repositories/fakeawaiter" korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" "code.cloudfoundry.org/korifi/controllers/controllers/shared" "code.cloudfoundry.org/korifi/tests/matchers" @@ -25,7 +26,7 @@ import ( var _ = Describe("SpaceRepository", func() { var ( orgRepo *repositories.OrgRepo - conditionAwaiter *FakeAwaiter[ + conditionAwaiter *fakeawaiter.FakeAwaiter[ *korifiv1alpha1.CFSpace, korifiv1alpha1.CFSpaceList, *korifiv1alpha1.CFSpaceList, @@ -34,13 +35,13 @@ var _ = Describe("SpaceRepository", func() { ) BeforeEach(func() { - orgRepo = repositories.NewOrgRepo(rootNamespace, k8sClient, userClientFactory, nsPerms, &FakeAwaiter[ + orgRepo = repositories.NewOrgRepo(rootNamespace, k8sClient, userClientFactory, nsPerms, &fakeawaiter.FakeAwaiter[ *korifiv1alpha1.CFOrg, korifiv1alpha1.CFOrgList, *korifiv1alpha1.CFOrgList, ]{}) - conditionAwaiter = &FakeAwaiter[ + conditionAwaiter = &fakeawaiter.FakeAwaiter[ *korifiv1alpha1.CFSpace, korifiv1alpha1.CFSpaceList, *korifiv1alpha1.CFSpaceList, diff --git a/api/repositories/task_repository.go b/api/repositories/task_repository.go index b0646a3aa..e0fa0454b 100644 --- a/api/repositories/task_repository.go +++ b/api/repositories/task_repository.go @@ -88,14 +88,14 @@ type TaskRepo struct { userClientFactory authorization.UserK8sClientFactory namespaceRetriever NamespaceRetriever namespacePermissions *authorization.NamespacePermissions - taskConditionAwaiter ConditionAwaiter[*korifiv1alpha1.CFTask] + taskConditionAwaiter Awaiter[*korifiv1alpha1.CFTask] } func NewTaskRepo( userClientFactory authorization.UserK8sClientFactory, nsRetriever NamespaceRetriever, namespacePermissions *authorization.NamespacePermissions, - taskConditionAwaiter ConditionAwaiter[*korifiv1alpha1.CFTask], + taskConditionAwaiter Awaiter[*korifiv1alpha1.CFTask], ) *TaskRepo { return &TaskRepo{ userClientFactory: userClientFactory, diff --git a/api/repositories/task_repository_test.go b/api/repositories/task_repository_test.go index 53e22eac1..09b36469f 100644 --- a/api/repositories/task_repository_test.go +++ b/api/repositories/task_repository_test.go @@ -8,6 +8,7 @@ import ( "code.cloudfoundry.org/korifi/api/authorization" apierrors "code.cloudfoundry.org/korifi/api/errors" "code.cloudfoundry.org/korifi/api/repositories" + "code.cloudfoundry.org/korifi/api/repositories/fakeawaiter" korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" "code.cloudfoundry.org/korifi/tests/matchers" "code.cloudfoundry.org/korifi/tools" @@ -25,7 +26,7 @@ import ( var _ = Describe("TaskRepository", func() { var ( - conditionAwaiter *FakeAwaiter[ + conditionAwaiter *fakeawaiter.FakeAwaiter[ *korifiv1alpha1.CFTask, korifiv1alpha1.CFTaskList, *korifiv1alpha1.CFTaskList, @@ -37,7 +38,7 @@ var _ = Describe("TaskRepository", func() { ) BeforeEach(func() { - conditionAwaiter = &FakeAwaiter[ + conditionAwaiter = &fakeawaiter.FakeAwaiter[ *korifiv1alpha1.CFTask, korifiv1alpha1.CFTaskList, *korifiv1alpha1.CFTaskList, diff --git a/controllers/api/v1alpha1/appworkload_types.go b/controllers/api/v1alpha1/appworkload_types.go index 12df7ec16..2fe0bcfd8 100644 --- a/controllers/api/v1alpha1/appworkload_types.go +++ b/controllers/api/v1alpha1/appworkload_types.go @@ -61,6 +61,9 @@ type AppWorkloadStatus struct { // ObservedGeneration captures the latest generation of the AppWorkload that has been reconciled ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + //+kubebuilder:validation:Optional + ActualInstances int32 `json:"actualInstances"` } //+kubebuilder:object:root=true diff --git a/controllers/api/v1alpha1/cfapp_types.go b/controllers/api/v1alpha1/cfapp_types.go index 85c36d191..1c3ea88ba 100644 --- a/controllers/api/v1alpha1/cfapp_types.go +++ b/controllers/api/v1alpha1/cfapp_types.go @@ -34,7 +34,7 @@ type CFAppSpec struct { // The user-requested state of the CFApp. The currently-applied state of the CFApp is in status.ObservedDesiredState. // Allowed values are "STARTED", and "STOPPED". // +kubebuilder:validation:Enum=STOPPED;STARTED - DesiredState DesiredState `json:"desiredState"` + DesiredState AppState `json:"desiredState"` // Specifies how to build images for the app Lifecycle Lifecycle `json:"lifecycle"` @@ -46,8 +46,8 @@ type CFAppSpec struct { CurrentDropletRef v1.LocalObjectReference `json:"currentDropletRef,omitempty"` } -// DesiredState defines the desired state of CFApp. -type DesiredState string +// AppState defines the desired state of CFApp. +type AppState string // CFAppStatus defines the observed state of CFApp type CFAppStatus struct { @@ -56,7 +56,7 @@ type CFAppStatus struct { // Deprecated: No longer used //+kubebuilder:validation:Optional - ObservedDesiredState DesiredState `json:"observedDesiredState"` + ObservedDesiredState AppState `json:"observedDesiredState"` // VCAPServicesSecretName contains the name of the CFApp's VCAP_SERVICES Secret, which should exist in the same namespace //+kubebuilder:validation:Optional @@ -68,6 +68,9 @@ type CFAppStatus struct { // ObservedGeneration captures the latest generation of the CFApp that has been reconciled ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + //+kubebuilder:validation:Optional + ActualState AppState `json:"actualState"` } //+kubebuilder:object:root=true diff --git a/controllers/api/v1alpha1/cfprocess_types.go b/controllers/api/v1alpha1/cfprocess_types.go index b01fa2211..0b845ff19 100644 --- a/controllers/api/v1alpha1/cfprocess_types.go +++ b/controllers/api/v1alpha1/cfprocess_types.go @@ -90,6 +90,9 @@ type CFProcessStatus struct { // ObservedGeneration captures the latest generation of the CFProcess that has been reconciled ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + //+kubebuilder:validation:Optional + ActualInstances int32 `json:"actualInstances"` } //+kubebuilder:object:root=true diff --git a/controllers/api/v1alpha1/constants.go b/controllers/api/v1alpha1/constants.go index a8da3ddf7..14b69a293 100644 --- a/controllers/api/v1alpha1/constants.go +++ b/controllers/api/v1alpha1/constants.go @@ -4,8 +4,8 @@ const ( BuildpackLifecycle LifecycleType = "buildpack" DockerPackage PackageType = "docker" - StartedState DesiredState = "STARTED" - StoppedState DesiredState = "STOPPED" + StartedState AppState = "STARTED" + StoppedState AppState = "STOPPED" HTTPHealthCheckType HealthCheckType = "http" PortHealthCheckType HealthCheckType = "port" diff --git a/controllers/controllers/workloads/cfapp_controller.go b/controllers/controllers/workloads/cfapp_controller.go index 1fb61a5d8..903104717 100644 --- a/controllers/controllers/workloads/cfapp_controller.go +++ b/controllers/controllers/workloads/cfapp_controller.go @@ -52,6 +52,7 @@ func NewCFAppReconciler(k8sClient client.Client, scheme *runtime.Scheme, log log func (r *CFAppReconciler) SetupWithManager(mgr ctrl.Manager) *builder.Builder { return ctrl.NewControllerManagedBy(mgr). For(&korifiv1alpha1.CFApp{}). + Owns(&korifiv1alpha1.CFProcess{}). Watches( &korifiv1alpha1.CFBuild{}, handler.EnqueueRequestsFromMapFunc(buildToApp), @@ -132,6 +133,7 @@ func (r *CFAppReconciler) ReconcileResource(ctx context.Context, cfApp *korifiv1 cfApp.Status.VCAPServicesSecretName = secretName + cfApp.Status.ActualState = korifiv1alpha1.StoppedState if cfApp.Spec.CurrentDropletRef.Name == "" { meta.SetStatusCondition(&cfApp.Status.Conditions, metav1.Condition{ Type: shared.StatusConditionReady, @@ -162,14 +164,28 @@ func (r *CFAppReconciler) ReconcileResource(ctx context.Context, cfApp *korifiv1 ObservedGeneration: cfApp.Generation, }) - err = r.startApp(ctx, cfApp, droplet) + reconciledProcesses, err := r.reconcileProcesses(ctx, cfApp, droplet) if err != nil { return ctrl.Result{}, err } + cfApp.Status.ActualState = getActualState(reconciledProcesses) + return ctrl.Result{}, nil } +func getActualState(processes []*korifiv1alpha1.CFProcess) korifiv1alpha1.AppState { + processInstances := int32(0) + for _, p := range processes { + processInstances += p.Status.ActualInstances + } + + if processInstances == 0 { + return korifiv1alpha1.StoppedState + } + return korifiv1alpha1.StartedState +} + func (r *CFAppReconciler) getDroplet(ctx context.Context, cfApp *korifiv1alpha1.CFApp) (*korifiv1alpha1.BuildDropletStatus, error) { log := logr.FromContextOrDiscard(ctx).WithName("getDroplet").WithValues("dropletName", cfApp.Spec.CurrentDropletRef.Name) @@ -189,9 +205,11 @@ func (r *CFAppReconciler) getDroplet(ctx context.Context, cfApp *korifiv1alpha1. return cfBuild.Status.Droplet, nil } -func (r *CFAppReconciler) startApp(ctx context.Context, cfApp *korifiv1alpha1.CFApp, droplet *korifiv1alpha1.BuildDropletStatus) error { +func (r *CFAppReconciler) reconcileProcesses(ctx context.Context, cfApp *korifiv1alpha1.CFApp, droplet *korifiv1alpha1.BuildDropletStatus) ([]*korifiv1alpha1.CFProcess, error) { log := logr.FromContextOrDiscard(ctx).WithName("startApp") + reconciledProcess := []*korifiv1alpha1.CFProcess{} + for _, dropletProcess := range addWebIfMissing(droplet.ProcessTypes) { loopLog := log.WithValues("processType", dropletProcess.Type) ctx = logr.NewContext(ctx, loopLog) @@ -199,25 +217,27 @@ func (r *CFAppReconciler) startApp(ctx context.Context, cfApp *korifiv1alpha1.CF existingProcess, err := r.fetchProcessByType(ctx, cfApp.Name, cfApp.Namespace, dropletProcess.Type) if err != nil { loopLog.Info("error when fetching CFProcess by type", "reason", err) - return err + return nil, err } if existingProcess != nil { err = r.updateCFProcess(ctx, existingProcess, dropletProcess.Command) if err != nil { loopLog.Info("error updating CFProcess", "reason", err) - return err + return nil, err } + reconciledProcess = append(reconciledProcess, existingProcess) } else { - err = r.createCFProcess(ctx, dropletProcess, cfApp) + createdProcess, err := r.createCFProcess(ctx, dropletProcess, cfApp) if err != nil { loopLog.Info("error creating CFProcess", "reason", err) - return err + return nil, err } + reconciledProcess = append(reconciledProcess, createdProcess) } } - return nil + return reconciledProcess, nil } func addWebIfMissing(processTypes []korifiv1alpha1.ProcessType) []korifiv1alpha1.ProcessType { @@ -236,7 +256,7 @@ func (r *CFAppReconciler) updateCFProcess(ctx context.Context, process *korifiv1 }) } -func (r *CFAppReconciler) createCFProcess(ctx context.Context, process korifiv1alpha1.ProcessType, cfApp *korifiv1alpha1.CFApp) error { +func (r *CFAppReconciler) createCFProcess(ctx context.Context, process korifiv1alpha1.ProcessType, cfApp *korifiv1alpha1.CFApp) (*korifiv1alpha1.CFProcess, error) { desiredCFProcess := &korifiv1alpha1.CFProcess{ ObjectMeta: metav1.ObjectMeta{ Namespace: cfApp.Namespace, @@ -255,10 +275,15 @@ func (r *CFAppReconciler) createCFProcess(ctx context.Context, process korifiv1a if err := controllerutil.SetControllerReference(cfApp, desiredCFProcess, r.scheme); err != nil { err = fmt.Errorf("failed to set OwnerRef on CFProcess: %w", err) - return err + return nil, err + } + + err := r.k8sClient.Create(ctx, desiredCFProcess) + if err != nil { + return nil, err } - return r.k8sClient.Create(ctx, desiredCFProcess) + return desiredCFProcess, nil } func (r *CFAppReconciler) fetchProcessByType(ctx context.Context, appGUID, appNamespace, processType string) (*korifiv1alpha1.CFProcess, error) { diff --git a/controllers/controllers/workloads/cfapp_controller_test.go b/controllers/controllers/workloads/cfapp_controller_test.go index c65d3ce80..b2d937742 100644 --- a/controllers/controllers/workloads/cfapp_controller_test.go +++ b/controllers/controllers/workloads/cfapp_controller_test.go @@ -363,7 +363,7 @@ var _ = Describe("CFAppReconciler Integration Tests", func() { When("the command on the web process is not empty", func() { BeforeEach(func() { - Expect(k8s.Patch(context.Background(), adminClient, cfProcessForTypeWeb, func() { + Expect(k8s.PatchResource(context.Background(), adminClient, cfProcessForTypeWeb, func() { cfProcessForTypeWeb.Spec.Command = "something else" })).To(Succeed()) }) diff --git a/controllers/controllers/workloads/cfprocess_controller.go b/controllers/controllers/workloads/cfprocess_controller.go index b78be91b9..3e0da9ad0 100644 --- a/controllers/controllers/workloads/cfprocess_controller.go +++ b/controllers/controllers/workloads/cfprocess_controller.go @@ -71,6 +71,7 @@ func NewCFProcessReconciler( func (r *CFProcessReconciler) SetupWithManager(mgr ctrl.Manager) *builder.Builder { return ctrl.NewControllerManagedBy(mgr). For(&korifiv1alpha1.CFProcess{}). + Owns(&korifiv1alpha1.AppWorkload{}). Watches( &korifiv1alpha1.CFApp{}, handler.EnqueueRequestsFromMapFunc(r.enqueueCFProcessRequestsForApp), @@ -173,9 +174,23 @@ func (r *CFProcessReconciler) ReconcileResource(ctx context.Context, cfProcess * ObservedGeneration: cfProcess.Generation, }) + appWorkloads, err := r.fetchAppWorkloadsForProcess(ctx, cfProcess) + if err != nil { + return ctrl.Result{}, err + } + + cfProcess.Status.ActualInstances = getActualInstances(appWorkloads) return ctrl.Result{}, nil } +func getActualInstances(appWorkloads []korifiv1alpha1.AppWorkload) int32 { + actualInstances := int32(0) + for _, w := range appWorkloads { + actualInstances += w.Status.ActualInstances + } + return actualInstances +} + func needsAppWorkload(cfApp *korifiv1alpha1.CFApp, cfProcess *korifiv1alpha1.CFProcess) bool { if cfApp.Spec.DesiredState != korifiv1alpha1.StartedState { return false @@ -239,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.DesiredState, cfLastStopAppRev string) error { +func (r *CFProcessReconciler) 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) @@ -261,7 +276,7 @@ func (r *CFProcessReconciler) cleanUpAppWorkloads(ctx context.Context, cfProcess } func needsToDeleteAppWorkload( - desiredState korifiv1alpha1.DesiredState, + desiredState korifiv1alpha1.AppState, cfProcess *korifiv1alpha1.CFProcess, appWorkload korifiv1alpha1.AppWorkload, cfLastStopAppRev string, diff --git a/controllers/controllers/workloads/cfprocess_controller_test.go b/controllers/controllers/workloads/cfprocess_controller_test.go index ef786af94..d981aaa90 100644 --- a/controllers/controllers/workloads/cfprocess_controller_test.go +++ b/controllers/controllers/workloads/cfprocess_controller_test.go @@ -185,6 +185,23 @@ var _ = Describe("CFProcessReconciler Integration Tests", func() { }) }) + When("the app workload instances is set", func() { + BeforeEach(func() { + eventuallyCreatedAppWorkloadShould(cfProcess.Name, cfSpace.Status.GUID, func(g Gomega, appWorkload korifiv1alpha1.AppWorkload) { + g.Expect(k8s.Patch(ctx, adminClient, &appWorkload, func() { + appWorkload.Status.ActualInstances = 3 + })).To(Succeed()) + }) + }) + + It("updates the actual process instances", func() { + Eventually(func(g Gomega) { + g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(cfProcess), cfProcess)).To(Succeed()) + g.Expect(cfProcess.Status.ActualInstances).To(BeEquivalentTo(3)) + }).Should(Succeed()) + }) + }) + When("The process command field isn't set", func() { BeforeEach(func() { Expect(k8s.PatchResource(ctx, adminClient, cfProcess, func() { diff --git a/controllers/controllers/workloads/suite_test.go b/controllers/controllers/workloads/suite_test.go index 568390caa..7713f8d46 100644 --- a/controllers/controllers/workloads/suite_test.go +++ b/controllers/controllers/workloads/suite_test.go @@ -338,7 +338,7 @@ func patchAppWithDroplet(ctx context.Context, k8sClient client.Client, appGUID, Namespace: spaceGUID, }, } - Expect(k8s.Patch(ctx, k8sClient, cfApp, func() { + Expect(k8s.PatchResource(ctx, k8sClient, cfApp, func() { cfApp.Spec.CurrentDropletRef = corev1.LocalObjectReference{Name: buildGUID} })).To(Succeed()) return cfApp diff --git a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_appworkloads.yaml b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_appworkloads.yaml index 179df9980..51b933105 100644 --- a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_appworkloads.yaml +++ b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_appworkloads.yaml @@ -716,6 +716,9 @@ spec: status: description: AppWorkloadStatus defines the observed state of AppWorkload properties: + actualInstances: + format: int32 + type: integer conditions: items: description: "Condition contains details for one aspect of the current diff --git a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfapps.yaml b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfapps.yaml index 869535af4..aeeb0288f 100644 --- a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfapps.yaml +++ b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfapps.yaml @@ -116,6 +116,9 @@ spec: status: description: CFAppStatus defines the observed state of CFApp properties: + actualState: + description: AppState defines the desired state of CFApp. + type: string conditions: items: description: "Condition contains details for one aspect of the current diff --git a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfprocesses.yaml b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfprocesses.yaml index c679da30a..35112e74d 100644 --- a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfprocesses.yaml +++ b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfprocesses.yaml @@ -127,6 +127,9 @@ spec: status: description: CFProcessStatus defines the observed state of CFProcess properties: + actualInstances: + format: int32 + type: integer conditions: items: description: "Condition contains details for one aspect of the current diff --git a/scripts/run-tests.sh b/scripts/run-tests.sh index f174f0939..580a8f87f 100755 --- a/scripts/run-tests.sh +++ b/scripts/run-tests.sh @@ -38,7 +38,6 @@ function configure_e2e_tests() { function configure_crd_tests() { export API_SERVER_ROOT="${API_SERVER_ROOT:-https://localhost}" - export NO_PARALLEL=true deploy_korifi } diff --git a/statefulset-runner/controllers/appworkload_controller.go b/statefulset-runner/controllers/appworkload_controller.go index c5ceb17c6..adbc2cd95 100644 --- a/statefulset-runner/controllers/appworkload_controller.go +++ b/statefulset-runner/controllers/appworkload_controller.go @@ -109,6 +109,7 @@ func NewAppWorkloadReconciler( func (r *AppWorkloadReconciler) SetupWithManager(mgr ctrl.Manager) *builder.Builder { return ctrl.NewControllerManagedBy(mgr). For(&korifiv1alpha1.AppWorkload{}). + Owns(&appsv1.StatefulSet{}). Watches( new(appsv1.StatefulSet), handler.EnqueueRequestsFromMapFunc(r.enqueueAppWorkloadRequests), @@ -203,6 +204,7 @@ func (r *AppWorkloadReconciler) ReconcileResource(ctx context.Context, appWorklo return ctrl.Result{}, err } + appWorkload.Status.ActualInstances = updatedStatefulSet.Status.Replicas meta.SetStatusCondition(&appWorkload.Status.Conditions, metav1.Condition{ Type: shared.StatusConditionReady, Status: metav1.ConditionTrue, diff --git a/statefulset-runner/controllers/integration/appworkload_controller_test.go b/statefulset-runner/controllers/integration/appworkload_controller_test.go index efe14e8cc..9883c5446 100644 --- a/statefulset-runner/controllers/integration/appworkload_controller_test.go +++ b/statefulset-runner/controllers/integration/appworkload_controller_test.go @@ -107,6 +107,23 @@ var _ = Describe("AppWorkloadsController", func() { }).Should(Succeed()) Expect(*pdb.Spec.MinAvailable).To(Equal(intstr.FromString("50%"))) }) + + When("the statefulset replicas is set", func() { + JustBeforeEach(func() { + statefulset := getStatefulsetForAppWorkload(Default) + updatedStatefulset := statefulset.DeepCopy() + updatedStatefulset.Status.Replicas = 1 + + Expect(k8sClient.Status().Patch(ctx, updatedStatefulset, client.MergeFrom(&statefulset))).To(Succeed()) + }) + + It("updates workload actual instances", func() { + Eventually(func(g Gomega) { + g.Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(appWorkload), appWorkload)).To(Succeed()) + g.Expect(appWorkload.Status.ActualInstances).To(BeEquivalentTo(1)) + }).Should(Succeed()) + }) + }) }) When("AppWorkload update", func() { diff --git a/tests/crds/apps_test.go b/tests/crds/apps_test.go new file mode 100644 index 000000000..ae3348743 --- /dev/null +++ b/tests/crds/apps_test.go @@ -0,0 +1,208 @@ +package crds_test + +import ( + "crypto/tls" + "fmt" + "net/http" + "time" + + korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" + "code.cloudfoundry.org/korifi/controllers/config" + "code.cloudfoundry.org/korifi/tests/helpers" + "code.cloudfoundry.org/korifi/tests/helpers/fail_handler" + "code.cloudfoundry.org/korifi/tools/k8s" + "code.cloudfoundry.org/korifi/tools/registry" + "github.com/google/uuid" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "gopkg.in/yaml.v3" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var _ = Describe("CFApp", func() { + var ( + app *korifiv1alpha1.CFApp + process *korifiv1alpha1.CFProcess + ) + + BeforeEach(func() { + app = pushApp() + + Eventually(func(g Gomega) { + processList := &korifiv1alpha1.CFProcessList{} + g.Expect(k8sClient.List(ctx, processList, client.InNamespace(testSpace.Status.GUID))).To(Succeed()) + g.Expect(processList.Items).To(HaveLen(1)) + process = &processList.Items[0] + }).Should(Succeed()) + }) + + It("is in STOPPED state and has 0 processes", func() { + Eventually(func(g Gomega) { + g.Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(app), app)).To(Succeed()) + g.Expect(app.Status.ActualState).To(Equal(korifiv1alpha1.StoppedState)) + }).Should(Succeed()) + + Eventually(func(g Gomega) { + g.Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(process), process)).To(Succeed()) + g.Expect(process.Status.ActualInstances).To(BeEquivalentTo(0)) + }).Should(Succeed()) + }) + + When("the app is started", func() { + BeforeEach(func() { + Expect(k8s.PatchResource(ctx, k8sClient, app, func() { + app.Spec.DesiredState = korifiv1alpha1.StartedState + })).To(Succeed()) + }) + + It("is in STARTED state and has 1 process", func() { + Eventually(func(g Gomega) { + g.Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(app), app)).To(Succeed()) + g.Expect(app.Status.ActualState).To(Equal(korifiv1alpha1.StartedState)) + }).Should(Succeed()) + + Eventually(func(g Gomega) { + g.Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(process), process)).To(Succeed()) + g.Expect(process.Status.ActualInstances).To(BeEquivalentTo(1)) + }).Should(Succeed()) + }) + }) +}) + +func pushApp() *korifiv1alpha1.CFApp { + GinkgoHelper() + + appEnvSecret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testSpace.Status.GUID, + Name: uuid.NewString(), + }, + StringData: map[string]string{}, + } + Expect(k8sClient.Create(ctx, &appEnvSecret)).To(Succeed()) + + app := &korifiv1alpha1.CFApp{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testSpace.Status.GUID, + Name: uuid.NewString(), + }, + Spec: korifiv1alpha1.CFAppSpec{ + DisplayName: fmt.Sprintf("app-%d", time.Now().UnixMicro()), + DesiredState: korifiv1alpha1.StoppedState, + Lifecycle: korifiv1alpha1.Lifecycle{ + Type: "buildpack", + }, + EnvSecretName: appEnvSecret.Name, + }, + } + Expect(k8sClient.Create(ctx, app)).To(Succeed()) + + appPackage := &korifiv1alpha1.CFPackage{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testSpace.Status.GUID, + Name: uuid.NewString(), + }, + Spec: korifiv1alpha1.CFPackageSpec{ + Type: "bits", + AppRef: corev1.LocalObjectReference{ + Name: app.Name, + }, + }, + } + Expect(k8sClient.Create(ctx, appPackage)).To(Succeed()) + + uploadAppBits(app.Name, appPackage.Name) + + build := &korifiv1alpha1.CFBuild{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testSpace.Status.GUID, + Name: uuid.NewString(), + }, + Spec: korifiv1alpha1.CFBuildSpec{ + PackageRef: corev1.LocalObjectReference{ + Name: appPackage.Name, + }, + AppRef: corev1.LocalObjectReference{ + Name: app.Name, + }, + Lifecycle: korifiv1alpha1.Lifecycle{ + Type: "buildpack", + }, + }, + } + Expect(k8sClient.Create(ctx, build)).To(Succeed()) + + failHandler.RegisterFailHandler(fail_handler.Hook{ + Matcher: fail_handler.Always, + Hook: func(config *rest.Config, message string) { + fail_handler.PrintBuildLogs(config, build.Name) + }, + }) + + Eventually(func(g Gomega) { + g.Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(build), build)).To(Succeed()) + g.Expect(meta.IsStatusConditionTrue(build.Status.Conditions, "Succeeded")).To(BeTrue()) + }).Should(Succeed()) + + Expect(k8s.Patch(ctx, k8sClient, app, func() { + app.Spec.CurrentDropletRef.Name = build.Name + })).To(Succeed()) + + return app +} + +func uploadAppBits(appGUID, packageGUID string) { + GinkgoHelper() + + Expect(k8sClient.Create(ctx, &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testSpace.Status.GUID, + Name: cfUser + "-admin", + }, + Subjects: []rbacv1.Subject{{ + Kind: "ServiceAccount", + Name: cfUser, + Namespace: rootNamespace, + }}, + RoleRef: rbacv1.RoleRef{ + Kind: "ClusterRole", + Name: "korifi-controllers-admin", + }, + })).To(Succeed()) + + controllersConfigMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "korifi", + Name: "korifi-controllers-config", + }, + } + Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(controllersConfigMap), controllersConfigMap)).To(Succeed()) + + controllersConfig := config.ControllerConfig{} + Expect(yaml.Unmarshal([]byte(controllersConfigMap.Data["config.yaml"]), &controllersConfig)).To(Succeed()) + repoCreator := registry.NewRepositoryCreator(controllersConfig.ContainerRegistryType) + Expect(repoCreator.CreateRepository(ctx, fmt.Sprintf("%s%s-packages", + controllersConfig.ContainerRepositoryPrefix, + appGUID, + ))).To(Succeed()) + + restClient := helpers.NewCorrelatedRestyClient(helpers.GetApiServerRoot(), + func() string { + return uuid.NewString() + }). + SetAuthScheme("Bearer"). + SetAuthToken(cfUserToken). + SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}) + + resp, err := restClient.R(). + SetFiles(map[string]string{ + "bits": defaultAppBitsFile, + }).Post("/v3/packages/" + packageGUID + "/upload") + Expect(err).NotTo(HaveOccurred()) + Expect(resp.StatusCode()).To(Equal(http.StatusOK)) +} diff --git a/tests/crds/crds_suite_test.go b/tests/crds/crds_suite_test.go index 45a59ee38..59108fb2c 100644 --- a/tests/crds/crds_suite_test.go +++ b/tests/crds/crds_suite_test.go @@ -1,43 +1,153 @@ package crds_test import ( + "context" + "encoding/json" + "fmt" "testing" + "time" + korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" "code.cloudfoundry.org/korifi/tests/helpers" + "code.cloudfoundry.org/korifi/tests/helpers/fail_handler" + "code.cloudfoundry.org/korifi/tools" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + k8sclient "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" "github.com/google/uuid" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - . "github.com/onsi/gomega/gexec" ) +func init() { + utilruntime.Must(korifiv1alpha1.AddToScheme(scheme.Scheme)) +} + func TestCrds(t *testing.T) { - RegisterFailHandler(Fail) + failHandler = fail_handler.New("CRDs Tests", fail_handler.Hook{ + Matcher: fail_handler.Always, + Hook: func(config *rest.Config, message string) { + fail_handler.PrintKorifiLogs(config, "") + }, + }) + RegisterFailHandler(failHandler.Fail) SetDefaultEventuallyTimeout(helpers.EventuallyTimeout()) SetDefaultEventuallyPollingInterval(helpers.EventuallyPollingInterval()) RunSpecs(t, "CRDs Suite") } var ( - rootNamespace string - serviceAccountFactory *helpers.ServiceAccountFactory - cfUser string + failHandler *fail_handler.Handler + defaultAppBitsFile string + + ctx context.Context + k8sClient client.Client + k8sClientSet *k8sclient.Clientset + + rootNamespace string + cfUser string + cfUserToken string + + testOrg *korifiv1alpha1.CFOrg + testSpace *korifiv1alpha1.CFSpace ) -var _ = BeforeSuite(func() { - rootNamespace = helpers.GetDefaultedEnvVar("ROOT_NAMESPACE", "cf") - serviceAccountFactory = helpers.NewServiceAccountFactory(rootNamespace) +type sharedSetupData struct { + DefaultAppBitsFile string + RootNamespace string + CfUser string + CfUserToken string +} - Expect( - helpers.Kubectl("get", "namespace/"+rootNamespace), - ).To(Exit(0), "Could not find root namespace called %q", rootNamespace) +var _ = SynchronizedBeforeSuite(func() []byte { + rootNamespace = helpers.GetDefaultedEnvVar("ROOT_NAMESPACE", "cf") + serviceAccountFactory := helpers.NewServiceAccountFactory(rootNamespace) cfUser = uuid.NewString() - cfUserToken := serviceAccountFactory.CreateServiceAccount(cfUser) + cfUserToken = serviceAccountFactory.CreateServiceAccount(cfUser) helpers.AddUserToKubeConfig(cfUser, cfUserToken) + + bs, err := json.Marshal(sharedSetupData{ + DefaultAppBitsFile: helpers.ZipDirectory( + helpers.GetDefaultedEnvVar("DEFAULT_APP_BITS_PATH", "../assets/dorifi"), + ), + RootNamespace: rootNamespace, + CfUser: cfUser, + CfUserToken: cfUserToken, + }) + Expect(err).NotTo(HaveOccurred()) + + return bs +}, func(bs []byte) { + var sharedSetup sharedSetupData + err := json.Unmarshal(bs, &sharedSetup) + Expect(err).NotTo(HaveOccurred()) + + defaultAppBitsFile = sharedSetup.DefaultAppBitsFile + rootNamespace = sharedSetup.RootNamespace + cfUser = sharedSetup.CfUser + cfUserToken = sharedSetup.CfUserToken + + ctx = context.Background() + + kubeconfig, err := ctrl.GetConfig() + Expect(err).NotTo(HaveOccurred()) + + k8sClient, err = client.New(kubeconfig, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + + k8sClientSet, err = k8sclient.NewForConfig(kubeconfig) + Expect(err).NotTo(HaveOccurred()) }) -var _ = AfterSuite(func() { +var _ = SynchronizedAfterSuite(func() { +}, func() { + serviceAccountFactory := helpers.NewServiceAccountFactory(rootNamespace) serviceAccountFactory.DeleteServiceAccount(cfUser) helpers.RemoveUserFromKubeConfig(cfUser) }) + +var _ = BeforeEach(func() { + testOrg = &korifiv1alpha1.CFOrg{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: rootNamespace, + Name: uuid.NewString(), + }, + Spec: korifiv1alpha1.CFOrgSpec{ + DisplayName: fmt.Sprintf("org-%d", time.Now().UnixMicro()), + }, + } + + Expect(k8sClient.Create(ctx, testOrg)).To(Succeed()) + Eventually(func(g Gomega) { + g.Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(testOrg), testOrg)).To(Succeed()) + g.Expect(testOrg.Status.GUID).NotTo(BeEmpty()) + }).Should(Succeed()) + + testSpace = &korifiv1alpha1.CFSpace{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testOrg.Status.GUID, + Name: uuid.NewString(), + }, + Spec: korifiv1alpha1.CFSpaceSpec{ + DisplayName: fmt.Sprintf("space-%d", time.Now().UnixMicro()), + }, + } + + Expect(k8sClient.Create(ctx, testSpace)).To(Succeed()) + Eventually(func(g Gomega) { + g.Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(testSpace), testSpace)).To(Succeed()) + g.Expect(testSpace.Status.GUID).NotTo(BeEmpty()) + }).Should(Succeed()) +}) + +var _ = AfterEach(func() { + Expect(k8sClient.Delete(ctx, testOrg, &client.DeleteOptions{ + PropagationPolicy: tools.PtrTo(metav1.DeletePropagationBackground), + })).To(Succeed()) +}) diff --git a/tests/crds/crds_test.go b/tests/crds/crds_test.go deleted file mode 100644 index 0834ea7c1..000000000 --- a/tests/crds/crds_test.go +++ /dev/null @@ -1,189 +0,0 @@ -package crds_test - -import ( - "fmt" - - . "code.cloudfoundry.org/korifi/controllers/controllers/workloads/testutils" - "code.cloudfoundry.org/korifi/tests/helpers" - - "github.com/google/uuid" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - . "github.com/onsi/gomega/gexec" -) - -var _ = Describe("Using the k8s API directly", Ordered, func() { - var ( - orgGUID string - orgDisplayName string - spaceGUID string - spaceDisplayName string - bindingName string - bindingUser string - propagatedBindingName string - korifiAPIEndpoint string - ) - - BeforeAll(func() { - orgGUID = PrefixedGUID("org") - orgDisplayName = PrefixedGUID("Org") - spaceGUID = PrefixedGUID("space") - spaceDisplayName = PrefixedGUID("Space") - - korifiAPIEndpoint = helpers.GetRequiredEnvVar("API_SERVER_ROOT") - - bindingName = cfUser + "-root-namespace-user" - bindingUser = rootNamespace + ":" + cfUser - - propagatedBindingName = uuid.NewString() - }) - - AfterAll(func() { - Expect( - helpers.Kubectl("delete", "--ignore-not-found=true", "-n="+rootNamespace, "cforg", orgGUID), - ).To(Exit(0)) - Expect( - helpers.Kubectl("delete", "--ignore-not-found=true", "-n="+rootNamespace, "rolebinding", bindingName), - ).To(Exit(0)) - }) - - It("can create a CFOrg", func() { - Expect(helpers.KubectlApply(`--- - apiVersion: korifi.cloudfoundry.org/v1alpha1 - kind: CFOrg - metadata: - namespace: %s - name: %s - spec: - displayName: %s - `, rootNamespace, orgGUID, orgDisplayName), - ).To(Exit(0)) - - Expect( - helpers.Kubectl("wait", "--for=condition=ready", "-n="+rootNamespace, "cforg/"+orgGUID, fmt.Sprintf("--timeout=%s", helpers.EventuallyTimeout())), - ).To(Exit(0)) - - Expect( - helpers.Kubectl("get", "namespace/"+orgGUID), - ).To(Exit(0)) - }) - - It("can create a CFSpace", func() { - Expect(helpers.KubectlApply(`--- - apiVersion: korifi.cloudfoundry.org/v1alpha1 - kind: CFSpace - metadata: - namespace: %s - name: %s - spec: - displayName: %s - `, orgGUID, spaceGUID, spaceDisplayName), - ).To(Exit(0)) - - Expect( - helpers.Kubectl("wait", "--for=condition=ready", "-n="+orgGUID, "cfspace/"+spaceGUID, fmt.Sprintf("--timeout=%s", helpers.EventuallyTimeout())), - ).To(Exit(0)) - - Expect( - helpers.Kubectl("get", "namespace/"+spaceGUID), - ).To(Exit(0)) - }) - - It("can grant the necessary roles to push an app via the CLI", func() { - Expect( - helpers.Kubectl("create", "rolebinding", "-n="+rootNamespace, "--serviceaccount="+bindingUser, "--clusterrole=korifi-controllers-root-namespace-user", bindingName), - ).To(Exit(0)) - Expect( - helpers.Kubectl("label", "rolebinding", bindingName, "-n="+rootNamespace, "cloudfoundry.org/role-guid="+GenerateGUID()), - ).To(Exit(0)) - - Expect( - helpers.Kubectl("create", "rolebinding", "-n="+orgGUID, "--serviceaccount="+bindingUser, "--clusterrole=korifi-controllers-organization-user", cfUser+"-org-user"), - ).To(Exit(0)) - Expect( - helpers.Kubectl("label", "rolebinding", cfUser+"-org-user", "-n="+orgGUID, "cloudfoundry.org/role-guid="+GenerateGUID()), - ).To(Exit(0)) - - Expect( - helpers.Kubectl("create", "rolebinding", "-n="+spaceGUID, "--serviceaccount="+bindingUser, "--clusterrole=korifi-controllers-space-developer", cfUser+"-space-developer"), - ).To(Exit(0)) - Expect( - helpers.Kubectl("label", "rolebinding", cfUser+"-space-developer", "-n="+spaceGUID, "cloudfoundry.org/role-guid="+GenerateGUID()), - ).To(Exit(0)) - - Expect(helpers.Cf("api", korifiAPIEndpoint, "--skip-ssl-validation")).To(Exit(0)) - Expect(helpers.Cf("auth", cfUser)).To(Exit(0)) - Expect(helpers.Cf("target", "-o", orgDisplayName, "-s", spaceDisplayName)).To(Exit(0)) - - Expect( - helpers.Cf("push", PrefixedGUID("crds-test-app"), "-p", "../assets/dorifi", "--no-start"), // This could be any app - ).To(Exit(0)) - }) - - It("can create cf-admin rolebinding which propagates to child namespaces", func() { - Expect(helpers.KubectlApply(`--- - apiVersion: rbac.authorization.k8s.io/v1 - kind: RoleBinding - metadata: - annotations: - cloudfoundry.org/propagate-cf-role: "true" - namespace: %s - name: %s - roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: korifi-controllers-admin - subjects: - - kind: ServiceAccount - name: %s - namespace: %s - `, rootNamespace, propagatedBindingName, cfUser, rootNamespace), - ).To(Exit(0)) - - Eventually(func(g Gomega) { - g.Expect(helpers.Kubectl("get", "rolebinding/"+propagatedBindingName, "-n", rootNamespace)).To(Exit(0)) - }).Should(Succeed()) - - Eventually(func(g Gomega) { - g.Expect(helpers.Kubectl("get", "rolebinding/"+propagatedBindingName, "-n", orgGUID)).To(Exit(0)) - }).Should(Succeed()) - - Eventually(func(g Gomega) { - g.Expect(helpers.Kubectl("get", "rolebinding/"+propagatedBindingName, "-n", orgGUID)).To(Exit(0)) - }).Should(Succeed()) - - Eventually(func(g Gomega) { - g.Expect(helpers.Kubectl("get", "rolebinding/"+propagatedBindingName, "-n", spaceGUID)).To(Exit(0)) - }).Should(Succeed()) - }) - - It("can delete the cf-admin rolebinding", func() { - Expect( - helpers.Kubectl("delete", "--ignore-not-found=true", "-n="+rootNamespace, "rolebinding/"+propagatedBindingName), - ).To(Exit(0)) - - Expect( - helpers.Kubectl("wait", "--for=delete", "rolebinding/"+propagatedBindingName, "-n", rootNamespace, fmt.Sprintf("--timeout=%s", helpers.EventuallyTimeout())), - ).To(Exit(0)) - - Expect( - helpers.Kubectl("wait", "--for=delete", "rolebinding/"+propagatedBindingName, "-n", orgGUID, fmt.Sprintf("--timeout=%s", helpers.EventuallyTimeout())), - ).To(Exit(0)) - - Expect( - helpers.Kubectl("wait", "--for=delete", "rolebinding/"+propagatedBindingName, "-n", spaceGUID, fmt.Sprintf("--timeout=%s", helpers.EventuallyTimeout())), - ).To(Exit(0)) - }) - - It("can delete the space", func() { - Expect(helpers.Kubectl("delete", "--ignore-not-found=true", "-n="+orgGUID, "cfspace/"+spaceGUID)).To(Exit(0)) - Expect(helpers.Kubectl("wait", "--for=delete", "namespace/"+spaceGUID)).To(Exit(0)) - }) - - It("can delete the org", func() { - Expect(helpers.Kubectl("delete", "--ignore-not-found=true", "-n="+rootNamespace, "cforgs/"+orgGUID)).To(Exit(0)) - - Expect(helpers.Kubectl("wait", "--for=delete", "cforg/"+orgGUID, "-n", rootNamespace)).To(Exit(0)) - Expect(helpers.Kubectl("wait", "--for=delete", "namespace/"+orgGUID)).To(Exit(0)) - }) -}) diff --git a/tests/e2e/e2e_suite_test.go b/tests/e2e/e2e_suite_test.go index 397ddbff9..c4714fae0 100644 --- a/tests/e2e/e2e_suite_test.go +++ b/tests/e2e/e2e_suite_test.go @@ -1,7 +1,6 @@ package e2e_test import ( - "archive/zip" "context" "crypto/tls" "encoding/base64" @@ -10,7 +9,6 @@ import ( "io" "net/http" "os" - "path/filepath" "regexp" "strings" "sync" @@ -25,7 +23,6 @@ import ( "github.com/google/uuid" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "github.com/onsi/gomega/types" "gopkg.in/yaml.v3" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" @@ -48,7 +45,6 @@ var ( appFQDN string commonTestOrgGUID string commonTestOrgName string - assetsTmpDir string defaultAppBitsFile string multiProcessAppBitsFile string ) @@ -282,31 +278,26 @@ type cfErr struct { } func TestE2E(t *testing.T) { - RegisterFailHandler(fail_handler.New("E2E Tests", map[types.GomegaMatcher]func(*rest.Config, string){ - fail_handler.Always: func(config *rest.Config, _ string) { - fail_handler.PrintPodsLogs(config, []fail_handler.PodContainerDescriptor{ - { - Namespace: systemNamespace(), - LabelKey: "app", - LabelValue: "korifi-api", - Container: "korifi-api", - CorrelationId: correlationId, - }, - { - Namespace: systemNamespace(), - LabelKey: "app", - LabelValue: "korifi-controllers", - Container: "manager", - }, - }) + RegisterFailHandler(fail_handler.New("E2E Tests", + fail_handler.Hook{ + Matcher: fail_handler.Always, + Hook: func(config *rest.Config, _ string) { + fail_handler.PrintKorifiLogs(config, correlationId) + }, }, - ContainSubstring("Droplet not found"): func(config *rest.Config, message string) { - printDropletNotFoundDebugInfo(config, message) + fail_handler.Hook{ + Matcher: ContainSubstring("Droplet not found"), + Hook: func(config *rest.Config, message string) { + printDropletNotFoundDebugInfo(config, message) + }, }, - ContainSubstring("404"): func(config *rest.Config, _ string) { - printAllRoleBindings(config) + fail_handler.Hook{ + Matcher: ContainSubstring("404"), + Hook: func(config *rest.Config, message string) { + printAllRoleBindings(config) + }, }, - })) + ).Fail) RunSpecs(t, "E2E Suite") } @@ -330,19 +321,13 @@ var _ = SynchronizedBeforeSuite(func() []byte { commonTestOrgName = generateGUID("common-test-org") commonTestOrgGUID = createOrg(commonTestOrgName) - var err error - assetsTmpDir, err = os.MkdirTemp("", "e2e-test-assets") - Expect(err).NotTo(HaveOccurred()) - sharedData := sharedSetupData{ CommonOrgName: commonTestOrgName, CommonOrgGUID: commonTestOrgGUID, - // Some environments where Korifi does not manage the ClusterBuilder lack a standalone Procfile buildpack - // The DEFAULT_APP_BITS_PATH and DEFAULT_APP_RESPONSE environment variables are a workaround to allow e2e tests to run - // with a different app in these environments. - // See https://github.com/cloudfoundry/korifi/issues/2355 for refactoring ideas - DefaultAppBitsFile: zipAsset(helpers.GetDefaultedEnvVar("DEFAULT_APP_BITS_PATH", "../assets/dorifi")), - MultiProcessAppBitsFile: zipAsset("../assets/multi-process"), + DefaultAppBitsFile: helpers.ZipDirectory( + helpers.GetDefaultedEnvVar("DEFAULT_APP_BITS_PATH", "../assets/dorifi"), + ), + MultiProcessAppBitsFile: helpers.ZipDirectory("../assets/multi-process"), AdminServiceAccount: adminServiceAccount, AdminServiceAccountToken: adminServiceAccountToken, } @@ -373,7 +358,6 @@ var _ = SynchronizedBeforeSuite(func() []byte { var _ = SynchronizedAfterSuite(func() { }, func() { - os.RemoveAll(assetsTmpDir) deleteOrg(commonTestOrgGUID) serviceAccountFactory.DeleteServiceAccount(adminServiceAccount) }) @@ -1056,68 +1040,6 @@ func commonTestSetup() { serviceAccountFactory = helpers.NewServiceAccountFactory(rootNamespace) } -func zipAsset(src string) string { - GinkgoHelper() - - file, err := os.CreateTemp("", "*.zip") - if err != nil { - Expect(err).NotTo(HaveOccurred()) - } - defer file.Close() - - w := zip.NewWriter(file) - defer w.Close() - - walker := func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - if info.IsDir() { - return nil - } - fp, err := os.Open(path) - if err != nil { - return err - } - defer fp.Close() - - rel, err := filepath.Rel(src, path) - if err != nil { - return err - } - - fh := &zip.FileHeader{ - Name: rel, - } - fh.SetMode(info.Mode()) - - f, err := w.CreateHeader(fh) - if err != nil { - return err - } - - _, err = io.Copy(f, fp) - if err != nil { - return err - } - - return nil - } - err = filepath.Walk(src, walker) - Expect(err).NotTo(HaveOccurred()) - - return file.Name() -} - -func systemNamespace() string { - systemNS, found := os.LookupEnv("SYSTEM_NAMESPACE") - if found { - return systemNS - } - - return "korifi" -} - func getCorrelationId() string { return correlationId } @@ -1125,41 +1047,13 @@ func getCorrelationId() string { func printDropletNotFoundDebugInfo(config *rest.Config, message string) { fmt.Fprint(GinkgoWriter, "\n\n========== Droplet not found debug log (start) ==========\n") - fmt.Fprint(GinkgoWriter, "\n========== Kpack logs ==========\n") - fail_handler.PrintPodsLogs(config, []fail_handler.PodContainerDescriptor{ - { - Namespace: "kpack", - LabelKey: "app", - LabelValue: "kpack-controller", - }, - { - Namespace: "kpack", - LabelKey: "app", - LabelValue: "kpack-webhook", - }, - }) - dropletGUID, err := getDropletGUID(message) if err != nil { fmt.Fprintf(GinkgoWriter, "Failed to get droplet GUID from message %v\n", err) return } - fmt.Fprint(GinkgoWriter, "\n\n========== Droplet build logs ==========\n") - fmt.Fprintf(GinkgoWriter, "DropletGUID: %q\n", dropletGUID) - fail_handler.PrintPodsLogs(config, []fail_handler.PodContainerDescriptor{ - { - LabelKey: "korifi.cloudfoundry.org/build-workload-name", - LabelValue: dropletGUID, - }, - }) - fail_handler.PrintPodEvents(config, []fail_handler.PodContainerDescriptor{ - { - LabelKey: "korifi.cloudfoundry.org/build-workload-name", - LabelValue: dropletGUID, - }, - }) - + fail_handler.PrintBuildLogs(config, dropletGUID) fmt.Fprint(GinkgoWriter, "\n\n========== Droplet not found debug log (end) ==========\n\n") } diff --git a/tests/helpers/cf.go b/tests/helpers/cf.go index 87d32f94a..01e0ca392 100644 --- a/tests/helpers/cf.go +++ b/tests/helpers/cf.go @@ -2,12 +2,16 @@ package helpers import ( "github.com/cloudfoundry/cf-test-helpers/cf" - . "github.com/onsi/ginkgo/v2" //lint:ignore ST1001 this is a test file - "github.com/onsi/gomega/gexec" + . "github.com/onsi/ginkgo/v2" //lint:ignore ST1001 this is a test file + . "github.com/onsi/gomega/gexec" //lint:ignore ST1001 this is a test file ) -func Cf(args ...string) *gexec.Session { +func Cf(args ...string) *Session { GinkgoHelper() return cf.Cf(args...).Wait() } + +func GetApiServerRoot() string { + return GetRequiredEnvVar("API_SERVER_ROOT") +} diff --git a/tests/helpers/fail_handler/handler.go b/tests/helpers/fail_handler/handler.go index 5e1317abf..810d2c597 100644 --- a/tests/helpers/fail_handler/handler.go +++ b/tests/helpers/fail_handler/handler.go @@ -12,36 +12,56 @@ import ( var Always types.GomegaMatcher = gomega.ContainSubstring("") -func New(name string, hooks map[types.GomegaMatcher]func(config *rest.Config, message string)) func(message string, callerSkip ...int) { +type Hook struct { + Matcher types.GomegaMatcher + Hook func(config *rest.Config, message string) +} + +type Handler struct { + name string + hooks []Hook +} + +func (h *Handler) RegisterFailHandler(hook Hook) { + h.hooks = append(h.hooks, hook) +} + +func (h *Handler) Fail(message string, callerSkip ...int) { + fmt.Fprintf(ginkgo.GinkgoWriter, "Fail Handler %s\n", h.name) + + if len(callerSkip) > 0 { + callerSkip[0] = callerSkip[0] + 2 + } else { + callerSkip = []int{2} + } + + defer func() { + fmt.Fprintf(ginkgo.GinkgoWriter, "Fail Handler %s: completed\n", h.name) + ginkgo.Fail(message, callerSkip...) + }() + config, err := controllerruntime.GetConfig() if err != nil { - panic(err) + fmt.Fprintf(ginkgo.GinkgoWriter, "Fail to get controller config: %v\n", err) + return } - return func(message string, callerSkip ...int) { - fmt.Fprintf(ginkgo.GinkgoWriter, "Fail Handler %s\n", name) - - if len(callerSkip) > 0 { - callerSkip[0] = callerSkip[0] + 2 - } else { - callerSkip = []int{2} + for _, hook := range h.hooks { + matchingMessage, err := hook.Matcher.Match(message) + if err != nil { + fmt.Fprintf(ginkgo.GinkgoWriter, "Failed to match message: %v\n", err) + return } - defer func() { - fmt.Fprintf(ginkgo.GinkgoWriter, "Fail Handler %s: completed\n", name) - ginkgo.Fail(message, callerSkip...) - }() - - for matcher, hook := range hooks { - matchingMessage, err := matcher.Match(message) - if err != nil { - fmt.Fprintf(ginkgo.GinkgoWriter, "Failed to match message: %v\n", err) - return - } - - if matchingMessage { - hook(config, message) - } + if matchingMessage { + hook.Hook(config, message) } } } + +func New(name string, hooks ...Hook) *Handler { + return &Handler{ + name: name, + hooks: hooks, + } +} diff --git a/tests/helpers/fail_handler/pods.go b/tests/helpers/fail_handler/pods.go index 4a40f1592..6b9174307 100644 --- a/tests/helpers/fail_handler/pods.go +++ b/tests/helpers/fail_handler/pods.go @@ -180,3 +180,38 @@ func getPodContainerLog(clientset kubernetes.Interface, pod corev1.Pod, containe func fullLogOnErr() bool { return os.Getenv("FULL_LOG_ON_ERR") != "" } + +func PrintBuildLogs(config *rest.Config, buildGUID string) { + fmt.Fprint(ginkgo.GinkgoWriter, "\n\n========== Droplet build logs ==========\n") + fmt.Fprintf(ginkgo.GinkgoWriter, "DropletGUID: %q\n", buildGUID) + PrintPodsLogs(config, []PodContainerDescriptor{ + { + LabelKey: "korifi.cloudfoundry.org/build-workload-name", + LabelValue: buildGUID, + }, + }) + PrintPodEvents(config, []PodContainerDescriptor{ + { + LabelKey: "korifi.cloudfoundry.org/build-workload-name", + LabelValue: buildGUID, + }, + }) +} + +func PrintKorifiLogs(config *rest.Config, correlationId string) { + PrintPodsLogs(config, []PodContainerDescriptor{ + { + Namespace: "korifi", + LabelKey: "app", + LabelValue: "korifi-api", + Container: "korifi-api", + CorrelationId: correlationId, + }, + { + Namespace: "korifi", + LabelKey: "app", + LabelValue: "korifi-controllers", + Container: "manager", + }, + }) +} diff --git a/tests/helpers/k8s.go b/tests/helpers/k8s.go index b28c37344..cd905dcca 100644 --- a/tests/helpers/k8s.go +++ b/tests/helpers/k8s.go @@ -1,14 +1,8 @@ package helpers import ( - "fmt" - "strings" - - "github.com/cloudfoundry/cf-test-helpers/commandreporter" - "github.com/cloudfoundry/cf-test-helpers/commandstarter" - . "github.com/onsi/ginkgo/v2" //lint:ignore ST1001 this is a test file - . "github.com/onsi/gomega" //lint:ignore ST1001 this is a test file - . "github.com/onsi/gomega/gexec" //lint:ignore ST1001 this is a test file + . "github.com/onsi/ginkgo/v2" //lint:ignore ST1001 this is a test file + . "github.com/onsi/gomega" //lint:ignore ST1001 this is a test file "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" @@ -48,26 +42,3 @@ func RemoveUserFromKubeConfig(userName string) { Expect(clientcmd.ModifyConfig(configAccess, *config, false)).To(Succeed()) } - -func Kubectl(args ...string) *Session { - cmdStarter := commandstarter.NewCommandStarter() - return kubectlWithCustomReporter(cmdStarter, commandreporter.NewCommandReporter(), args...) -} - -func KubectlApply(stdinText string, sprintfArgs ...any) *Session { - cmdStarter := commandstarter.NewCommandStarterWithStdin( - strings.NewReader( - fmt.Sprintf(stdinText, sprintfArgs...), - ), - ) - return kubectlWithCustomReporter(cmdStarter, commandreporter.NewCommandReporter(), "apply", "-f=-") -} - -func kubectlWithCustomReporter(cmdStarter *commandstarter.CommandStarter, reporter *commandreporter.CommandReporter, args ...string) *Session { - request, err := cmdStarter.Start(reporter, "kubectl", args...) - if err != nil { - panic(err) - } - - return request.Wait() -} diff --git a/tests/helpers/zip.go b/tests/helpers/zip.go new file mode 100644 index 000000000..049d88d7c --- /dev/null +++ b/tests/helpers/zip.go @@ -0,0 +1,64 @@ +package helpers + +import ( + "archive/zip" + "io" + "os" + "path/filepath" + + . "github.com/onsi/ginkgo/v2" //lint:ignore ST1001 this is a test file + . "github.com/onsi/gomega" //lint:ignore ST1001 this is a test file +) + +func ZipDirectory(srcDir string) string { + GinkgoHelper() + + file, err := os.CreateTemp("", "*.zip") + if err != nil { + Expect(err).NotTo(HaveOccurred()) + } + defer file.Close() + + w := zip.NewWriter(file) + defer w.Close() + + walker := func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } + fp, err := os.Open(path) + if err != nil { + return err + } + defer fp.Close() + + rel, err := filepath.Rel(srcDir, path) + if err != nil { + return err + } + + fh := &zip.FileHeader{ + Name: rel, + } + fh.SetMode(info.Mode()) + + f, err := w.CreateHeader(fh) + if err != nil { + return err + } + + _, err = io.Copy(f, fp) + if err != nil { + return err + } + + return nil + } + err = filepath.Walk(srcDir, walker) + Expect(err).NotTo(HaveOccurred()) + + return file.Name() +} diff --git a/tests/smoke/smoke_suite_test.go b/tests/smoke/smoke_suite_test.go index 188f4776d..bbb09ad2f 100644 --- a/tests/smoke/smoke_suite_test.go +++ b/tests/smoke/smoke_suite_test.go @@ -15,7 +15,6 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" . "github.com/onsi/gomega/gexec" - gomegatypes "github.com/onsi/gomega/types" "gopkg.in/yaml.v2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" utilruntime "k8s.io/apimachinery/pkg/util/runtime" @@ -40,25 +39,15 @@ var ( ) func TestSmoke(t *testing.T) { - RegisterFailHandler(fail_handler.New("Smoke Tests", map[gomegatypes.GomegaMatcher]func(*rest.Config, string){ - fail_handler.Always: func(config *rest.Config, _ string) { - printCfApp(config) - fail_handler.PrintPodsLogs(config, []fail_handler.PodContainerDescriptor{ - { - Namespace: "korifi", - LabelKey: "app", - LabelValue: "korifi-api", - Container: "korifi-api", - }, - { - Namespace: "korifi", - LabelKey: "app", - LabelValue: "korifi-controllers", - Container: "manager", - }, - }) - }, - })) + RegisterFailHandler(fail_handler.New("Smoke Tests", + fail_handler.Hook{ + Matcher: fail_handler.Always, + Hook: func(config *rest.Config, message string) { + printCfApp(config) + fail_handler.PrintKorifiLogs(config, "") + }, + }).Fail) + SetDefaultEventuallyTimeout(helpers.EventuallyTimeout()) SetDefaultEventuallyPollingInterval(helpers.EventuallyPollingInterval()) RunSpecs(t, "Smoke Tests Suite") @@ -70,10 +59,6 @@ var _ = BeforeSuite(func() { rootNamespace = helpers.GetDefaultedEnvVar("ROOT_NAMESPACE", "cf") serviceAccountFactory = helpers.NewServiceAccountFactory(rootNamespace) - Expect( - helpers.Kubectl("get", "namespace/"+rootNamespace), - ).To(Exit(0), "Could not find root namespace called %q", rootNamespace) - cfAdmin = uuid.NewString() cfAdminToken := serviceAccountFactory.CreateAdminServiceAccount(cfAdmin) helpers.AddUserToKubeConfig(cfAdmin, cfAdminToken)