From 5beea86579514a427959b7d6987d8c7f1c1bb8a3 Mon Sep 17 00:00:00 2001 From: Danail Branekov Date: Fri, 5 Apr 2024 15:20:48 +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 | 20 +- api/repositories/app_repository_test.go | 13 +- api/repositories/conditions/await.go | 21 +- api/repositories/conditions/await_test.go | 104 +++++++--- .../conditions/conditions_suite_test.go | 6 +- api/repositories/fakeawaiter/await.go | 51 +++++ 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 | 39 ---- api/repositories/role_repository_test.go | 5 +- .../service_binding_repository.go | 4 +- .../service_binding_repository_test.go | 5 +- .../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 | 2 + controllers/api/v1alpha1/cfapp_types.go | 2 + controllers/api/v1alpha1/cfprocess_types.go | 2 + .../api/v1alpha1/zz_generated.deepcopy.go | 10 + .../controllers/workloads/cfapp_controller.go | 44 +++- .../workloads/cfprocess_controller.go | 16 ++ .../workloads/cfprocess_controller_test.go | 17 ++ .../korifi.cloudfoundry.org_appworkloads.yaml | 5 + .../crds/korifi.cloudfoundry.org_cfapps.yaml | 5 + .../korifi.cloudfoundry.org_cfprocesses.yaml | 5 + .../controllers/appworkload_controller.go | 3 + .../appworkload_controller_test.go | 17 ++ tests/crds/apps_test.go | 181 +++++++++++++++++ tests/crds/crds_suite_test.go | 110 +++++++++- tests/crds/crds_test.go | 189 ------------------ tests/e2e/e2e_suite_test.go | 71 +------ tests/helpers/cf.go | 11 +- tests/helpers/k8s.go | 33 +-- tests/helpers/zip.go | 64 ++++++ tests/smoke/smoke_suite_test.go | 4 - 42 files changed, 687 insertions(+), 438 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..e888ec996 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)) } @@ -462,6 +462,16 @@ func (f *AppRepo) SetAppDesiredState(ctx context.Context, authInfo authorization 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.DesiredState(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 } @@ -653,7 +663,7 @@ func cfAppToAppRecord(cfApp korifiv1alpha1.CFApp) AppRecord { DropletGUID: cfApp.Spec.CurrentDropletRef.Name, Labels: cfApp.Labels, Annotations: cfApp.Annotations, - State: DesiredState(cfApp.Spec.DesiredState), + State: DesiredState(cfApp.Status.ActualState), Lifecycle: Lifecycle{ Type: string(cfApp.Spec.Lifecycle.Type), Data: LifecycleData{ diff --git a/api/repositories/app_repository_test.go b/api/repositories/app_repository_test.go index 58c037153..ce2a64390 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() { 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..0b42c7e73 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,26 @@ 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() + + Expect(k8s.Patch(ctx, k8sClient, task, func() { + patchTask(task) + })).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 +52,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/fakeawaiter/await.go b/api/repositories/fakeawaiter/await.go new file mode 100644 index 000000000..0f8a327f9 --- /dev/null +++ b/api/repositories/fakeawaiter/await.go @@ -0,0 +1,51 @@ +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 { + 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) { + return object.(T), nil +} + +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..1a3bc1bfb 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{ 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..9600fb762 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, 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..5904a0a84 100644 --- a/controllers/api/v1alpha1/appworkload_types.go +++ b/controllers/api/v1alpha1/appworkload_types.go @@ -61,6 +61,8 @@ type AppWorkloadStatus struct { // ObservedGeneration captures the latest generation of the AppWorkload that has been reconciled ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + 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..679e07d5b 100644 --- a/controllers/api/v1alpha1/cfapp_types.go +++ b/controllers/api/v1alpha1/cfapp_types.go @@ -68,6 +68,8 @@ type CFAppStatus struct { // ObservedGeneration captures the latest generation of the CFApp that has been reconciled ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + ActualState DesiredState `json:"actualState"` } //+kubebuilder:object:root=true diff --git a/controllers/api/v1alpha1/cfprocess_types.go b/controllers/api/v1alpha1/cfprocess_types.go index b01fa2211..3bd5f3550 100644 --- a/controllers/api/v1alpha1/cfprocess_types.go +++ b/controllers/api/v1alpha1/cfprocess_types.go @@ -90,6 +90,8 @@ type CFProcessStatus struct { // ObservedGeneration captures the latest generation of the CFProcess that has been reconciled ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + ActualInstances *int32 `json:"actualInstances"` } //+kubebuilder:object:root=true diff --git a/controllers/api/v1alpha1/zz_generated.deepcopy.go b/controllers/api/v1alpha1/zz_generated.deepcopy.go index 5cb4db4c4..0e3856061 100644 --- a/controllers/api/v1alpha1/zz_generated.deepcopy.go +++ b/controllers/api/v1alpha1/zz_generated.deepcopy.go @@ -148,6 +148,11 @@ func (in *AppWorkloadStatus) DeepCopyInto(out *AppWorkloadStatus) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.ActualInstances != nil { + in, out := &in.ActualInstances, &out.ActualInstances + *out = new(int32) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppWorkloadStatus. @@ -1053,6 +1058,11 @@ func (in *CFProcessStatus) DeepCopyInto(out *CFProcessStatus) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.ActualInstances != nil { + in, out := &in.ActualInstances, &out.ActualInstances + *out = new(int32) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CFProcessStatus. diff --git a/controllers/controllers/workloads/cfapp_controller.go b/controllers/controllers/workloads/cfapp_controller.go index 1fb61a5d8..24a831e0d 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), @@ -162,11 +163,25 @@ 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 } + processInstances := int32(0) + for _, p := range reconciledProcesses { + if p.Status.ActualInstances == nil { + continue + } + processInstances += *p.Status.ActualInstances + } + + if processInstances == 0 { + cfApp.Status.ActualState = korifiv1alpha1.StoppedState + } else { + cfApp.Status.ActualState = korifiv1alpha1.StartedState + } + return ctrl.Result{}, nil } @@ -189,9 +204,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 +216,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 +255,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 +274,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/cfprocess_controller.go b/controllers/controllers/workloads/cfprocess_controller.go index b78be91b9..04601db7b 100644 --- a/controllers/controllers/workloads/cfprocess_controller.go +++ b/controllers/controllers/workloads/cfprocess_controller.go @@ -26,6 +26,7 @@ import ( "code.cloudfoundry.org/korifi/controllers/config" "code.cloudfoundry.org/korifi/controllers/controllers/shared" "code.cloudfoundry.org/korifi/controllers/controllers/workloads/ports" + "code.cloudfoundry.org/korifi/tools" "code.cloudfoundry.org/korifi/tools/k8s" "github.com/go-logr/logr" @@ -71,6 +72,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,6 +175,20 @@ func (r *CFProcessReconciler) ReconcileResource(ctx context.Context, cfProcess * ObservedGeneration: cfProcess.Generation, }) + appWorkloads, err := r.fetchAppWorkloadsForProcess(ctx, cfProcess) + if err != nil { + return ctrl.Result{}, err + } + + actualInstances := int32(0) + for _, w := range appWorkloads { + if w.Status.ActualInstances == nil { + continue + } + actualInstances += *w.Status.ActualInstances + } + cfProcess.Status.ActualInstances = tools.PtrTo(actualInstances) + return ctrl.Result{}, nil } diff --git a/controllers/controllers/workloads/cfprocess_controller_test.go b/controllers/controllers/workloads/cfprocess_controller_test.go index ef786af94..478d9d0cf 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 = tools.PtrTo(int32(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(PointTo(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/helm/korifi/controllers/crds/korifi.cloudfoundry.org_appworkloads.yaml b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_appworkloads.yaml index 179df9980..e10a1b9f2 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 @@ -790,6 +793,8 @@ spec: the AppWorkload that has been reconciled format: int64 type: integer + required: + - actualInstances type: object type: object served: true diff --git a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfapps.yaml b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfapps.yaml index 869535af4..34c1422c3 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: DesiredState defines the desired state of CFApp. + type: string conditions: items: description: "Condition contains details for one aspect of the current @@ -201,6 +204,8 @@ spec: description: VCAPServicesSecretName contains the name of the CFApp's VCAP_SERVICES Secret, which should exist in the same namespace type: string + required: + - actualState type: object type: object served: true diff --git a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfprocesses.yaml b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfprocesses.yaml index c679da30a..c9772bf91 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 @@ -201,6 +204,8 @@ spec: the CFProcess that has been reconciled format: int64 type: integer + required: + - actualInstances type: object type: object served: true diff --git a/statefulset-runner/controllers/appworkload_controller.go b/statefulset-runner/controllers/appworkload_controller.go index c5ceb17c6..f1686e5cf 100644 --- a/statefulset-runner/controllers/appworkload_controller.go +++ b/statefulset-runner/controllers/appworkload_controller.go @@ -21,6 +21,7 @@ import ( korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" "code.cloudfoundry.org/korifi/controllers/controllers/shared" + "code.cloudfoundry.org/korifi/tools" "code.cloudfoundry.org/korifi/tools/k8s" "github.com/go-logr/logr" @@ -109,6 +110,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 +205,7 @@ func (r *AppWorkloadReconciler) ReconcileResource(ctx context.Context, appWorklo return ctrl.Result{}, err } + appWorkload.Status.ActualInstances = tools.PtrTo(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..cc0b7fcac 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(gstruct.PointTo(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..9ef0d88aa --- /dev/null +++ b/tests/crds/apps_test.go @@ -0,0 +1,181 @@ +package crds_test + +import ( + "crypto/tls" + "fmt" + "net/http" + "time" + + korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" + "code.cloudfoundry.org/korifi/tests/helpers" + "code.cloudfoundry.org/korifi/tools/k8s" + "github.com/google/uuid" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gstruct" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "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(PointTo(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(PointTo(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(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()) + + 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(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()) + + 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..f9e8977e1 100644 --- a/tests/crds/crds_suite_test.go +++ b/tests/crds/crds_suite_test.go @@ -1,16 +1,31 @@ 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/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" + 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) SetDefaultEventuallyTimeout(helpers.EventuallyTimeout()) @@ -19,25 +34,104 @@ func TestCrds(t *testing.T) { } var ( + defaultAppBitsFile string + + ctx context.Context + k8sClient client.Client + k8sClientSet *k8sclient.Clientset + rootNamespace string serviceAccountFactory *helpers.ServiceAccountFactory cfUser string + cfUserToken string + + testOrg *korifiv1alpha1.CFOrg + testSpace *korifiv1alpha1.CFSpace ) -var _ = BeforeSuite(func() { +type sharedSetupData struct { + DefaultAppBitsFile string +} + +var _ = SynchronizedBeforeSuite(func() []byte { + sharedData := sharedSetupData{ + DefaultAppBitsFile: helpers.ZipDirectory( + helpers.GetDefaultedEnvVar("DEFAULT_APP_BITS_PATH", "../assets/dorifi"), + ), + } + + bs, err := json.Marshal(sharedData) + Expect(err).NotTo(HaveOccurred()) + + return bs +}, func(bs []byte) { + var sharedSetup sharedSetupData + err := json.Unmarshal(bs, &sharedSetup) + Expect(err).NotTo(HaveOccurred()) + + defaultAppBitsFile = sharedSetup.DefaultAppBitsFile + + 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()) + 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) - cfUser = uuid.NewString() - cfUserToken := serviceAccountFactory.CreateServiceAccount(cfUser) + cfUserToken = serviceAccountFactory.CreateServiceAccount(cfUser) helpers.AddUserToKubeConfig(cfUser, cfUserToken) }) -var _ = AfterSuite(func() { +var _ = SynchronizedAfterSuite(func() { +}, func() { 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..1595a9e86 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" @@ -48,7 +46,6 @@ var ( appFQDN string commonTestOrgGUID string commonTestOrgName string - assetsTmpDir string defaultAppBitsFile string multiProcessAppBitsFile string ) @@ -330,19 +327,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 +364,6 @@ var _ = SynchronizedBeforeSuite(func() []byte { var _ = SynchronizedAfterSuite(func() { }, func() { - os.RemoveAll(assetsTmpDir) deleteOrg(commonTestOrgGUID) serviceAccountFactory.DeleteServiceAccount(adminServiceAccount) }) @@ -1056,59 +1046,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 { diff --git a/tests/helpers/cf.go b/tests/helpers/cf.go index 87d32f94a..331f4d3cc 100644 --- a/tests/helpers/cf.go +++ b/tests/helpers/cf.go @@ -2,12 +2,17 @@ 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" //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/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..d6724f25d 100644 --- a/tests/smoke/smoke_suite_test.go +++ b/tests/smoke/smoke_suite_test.go @@ -70,10 +70,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)