From 9fb2dad39f621b6898314ef93444502ee0367b66 Mon Sep 17 00:00:00 2001 From: Danail Branekov Date: Tue, 30 Apr 2024 12:01:53 +0000 Subject: [PATCH] App stop/start waits for service bindings readiness * The app controller sets the ready state if all of the criteria below are met: - app service bindings (if any) become ready - the app actual state is equal to the desired state - the current droplet is set * The app repository waits for the app `Ready` state in `SetAppDesiredState` to ensure synchronous app stop/start * The condition awaiter verifies that the condition observed generation matches the object generation. Thus we ensure that the condition is not outdated. Co-authored-by: Georgi Sabev fixes: #3217 --- api/main.go | 14 +- api/repositories/app_repository.go | 8 +- api/repositories/app_repository_test.go | 14 +- api/repositories/conditions/await.go | 36 +- api/repositories/conditions/await_test.go | 94 +-- api/repositories/fakeawaiter/await.go | 22 - api/repositories/shared.go | 1 - .../services/bindings/controller.go | 28 +- .../services/bindings/controller_test.go | 25 - .../services/bindings/suite_test.go | 4 +- .../services/instances/controller.go | 17 +- .../services/instances/suite_test.go | 2 +- .../controller.go} | 77 ++- .../workloads/apps/controller_test.go | 497 +++++++++++++++ .../controllers/workloads/apps/suite_test.go | 123 ++++ .../workloads/cfapp_controller_test.go | 576 ------------------ .../controllers/workloads/suite_test.go | 20 +- controllers/main.go | 9 +- tests/e2e/apps_test.go | 92 +-- 19 files changed, 837 insertions(+), 822 deletions(-) rename controllers/controllers/workloads/{cfapp_controller.go => apps/controller.go} (81%) create mode 100644 controllers/controllers/workloads/apps/controller_test.go create mode 100644 controllers/controllers/workloads/apps/suite_test.go delete mode 100644 controllers/controllers/workloads/cfapp_controller_test.go diff --git a/api/main.go b/api/main.go index 867959ebc..b8a413c0f 100644 --- a/api/main.go +++ b/api/main.go @@ -127,14 +127,14 @@ func main() { privilegedCRClient, userClientFactory, nsPermissions, - conditions.NewStateAwaiter[*korifiv1alpha1.CFOrg, korifiv1alpha1.CFOrgList](conditionTimeout), + conditions.NewConditionAwaiter[*korifiv1alpha1.CFOrg, korifiv1alpha1.CFOrgList](conditionTimeout), ) spaceRepo := repositories.NewSpaceRepo( namespaceRetriever, orgRepo, userClientFactory, nsPermissions, - conditions.NewStateAwaiter[*korifiv1alpha1.CFSpace, korifiv1alpha1.CFSpaceList](conditionTimeout), + conditions.NewConditionAwaiter[*korifiv1alpha1.CFSpace, korifiv1alpha1.CFSpaceList](conditionTimeout), ) processRepo := repositories.NewProcessRepo( namespaceRetriever, @@ -148,7 +148,7 @@ func main() { namespaceRetriever, userClientFactory, nsPermissions, - conditions.NewStateAwaiter[*korifiv1alpha1.CFApp, korifiv1alpha1.CFAppList](conditionTimeout), + conditions.NewConditionAwaiter[*korifiv1alpha1.CFApp, korifiv1alpha1.CFAppList](conditionTimeout), ) dropletRepo := repositories.NewDropletRepo( userClientFactory, @@ -184,19 +184,19 @@ func main() { nsPermissions, toolsregistry.NewRepositoryCreator(cfg.ContainerRegistryType), cfg.ContainerRepositoryPrefix, - conditions.NewStateAwaiter[*korifiv1alpha1.CFPackage, korifiv1alpha1.CFPackageList](conditionTimeout), + conditions.NewConditionAwaiter[*korifiv1alpha1.CFPackage, korifiv1alpha1.CFPackageList](conditionTimeout), ) serviceInstanceRepo := repositories.NewServiceInstanceRepo( namespaceRetriever, userClientFactory, nsPermissions, - conditions.NewStateAwaiter[*korifiv1alpha1.CFServiceInstance, korifiv1alpha1.CFServiceInstanceList](conditionTimeout), + conditions.NewConditionAwaiter[*korifiv1alpha1.CFServiceInstance, korifiv1alpha1.CFServiceInstanceList](conditionTimeout), ) serviceBindingRepo := repositories.NewServiceBindingRepo( namespaceRetriever, userClientFactory, nsPermissions, - conditions.NewStateAwaiter[*korifiv1alpha1.CFServiceBinding, korifiv1alpha1.CFServiceBindingList](conditionTimeout), + conditions.NewConditionAwaiter[*korifiv1alpha1.CFServiceBinding, korifiv1alpha1.CFServiceBindingList](conditionTimeout), ) buildpackRepo := repositories.NewBuildpackRepository(cfg.BuilderName, userClientFactory, @@ -223,7 +223,7 @@ func main() { userClientFactory, namespaceRetriever, nsPermissions, - conditions.NewStateAwaiter[*korifiv1alpha1.CFTask, korifiv1alpha1.CFTaskList](conditionTimeout), + conditions.NewConditionAwaiter[*korifiv1alpha1.CFTask, korifiv1alpha1.CFTaskList](conditionTimeout), ) metricsRepo := repositories.NewMetricsRepo(userClientFactory) diff --git a/api/repositories/app_repository.go b/api/repositories/app_repository.go index 1da967da2..88ed32a78 100644 --- a/api/repositories/app_repository.go +++ b/api/repositories/app_repository.go @@ -461,13 +461,7 @@ 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.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 { + if _, err := f.appAwaiter.AwaitCondition(ctx, userClient, cfApp, korifiv1alpha1.ReadyConditionType); err != nil { return AppRecord{}, apierrors.FromK8sError(err, AppResourceType) } diff --git a/api/repositories/app_repository_test.go b/api/repositories/app_repository_test.go index a7d66d5fa..3e021e941 100644 --- a/api/repositories/app_repository_test.go +++ b/api/repositories/app_repository_test.go @@ -1218,11 +1218,12 @@ var _ = Describe("AppRepository", func() { Expect(returnedAppRecord.SpaceGUID).To(Equal(cfSpace.Name)) }) - It("waits for the desired state", func() { - Expect(appAwaiter.AwaitStateCallCount()).To(Equal(1)) - actualCFApp := appAwaiter.AwaitStateArgsForCall(0) + It("waits for the app ready condition", func() { + Expect(appAwaiter.AwaitConditionCallCount()).To(Equal(1)) + actualCFApp, actualCondition := appAwaiter.AwaitConditionArgsForCall(0) Expect(actualCFApp.GetName()).To(Equal(cfApp.Name)) Expect(actualCFApp.GetNamespace()).To(Equal(cfApp.Namespace)) + Expect(actualCondition).To(Equal(korifiv1alpha1.ReadyConditionType)) }) It("changes the desired state of the App", func() { @@ -1242,11 +1243,12 @@ var _ = Describe("AppRepository", func() { Expect(returnedErr).ToNot(HaveOccurred()) }) - It("waits for the desired state", func() { - Expect(appAwaiter.AwaitStateCallCount()).To(Equal(1)) - actualCFApp := appAwaiter.AwaitStateArgsForCall(0) + It("waits for the app ready condition", func() { + Expect(appAwaiter.AwaitConditionCallCount()).To(Equal(1)) + actualCFApp, actualCondition := appAwaiter.AwaitConditionArgsForCall(0) Expect(actualCFApp.GetName()).To(Equal(cfApp.Name)) Expect(actualCFApp.GetNamespace()).To(Equal(cfApp.Namespace)) + Expect(actualCondition).To(Equal(korifiv1alpha1.ReadyConditionType)) }) It("changes the desired state of the App", func() { diff --git a/api/repositories/conditions/await.go b/api/repositories/conditions/await.go index 710c9eb9e..fdd53f285 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 NewStateAwaiter[T RuntimeObjectWithStatusConditions, L any, PL ObjectList[L]](timeout time.Duration) *Awaiter[T, L, PL] { +func NewConditionAwaiter[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]) AwaitState(ctx context.Context, k8sClient client.WithWatch, object client.Object, checkState func(T) error) (T, error) { +func (a *Awaiter[T, L, PL]) AwaitCondition(ctx context.Context, k8sClient client.WithWatch, object client.Object, conditionType string) (T, error) { var empty T objList := PL(new(L)) @@ -47,29 +47,37 @@ func (a *Awaiter[T, L, PL]) AwaitState(ctx context.Context, k8sClient client.Wit } defer watch.Stop() - var stateCheckErr error + var conditionCheckErr error for e := range watch.ResultChan() { obj, ok := e.Object.(T) if !ok { continue } - stateCheckErr = checkState(obj) - if stateCheckErr == nil { + conditionCheckErr = checkConditionIsTrue(ctx, obj, conditionType) + if conditionCheckErr == nil { return obj, nil } } - 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(), + return empty, fmt.Errorf("object %s/%s status condition %s did not become true in %.2f s: %s", + object.GetNamespace(), object.GetName(), conditionType, a.timeout.Seconds(), conditionCheckErr.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) - }) +func checkConditionIsTrue[T RuntimeObjectWithStatusConditions](ctx context.Context, obj T, conditionType string) error { + condition := meta.FindStatusCondition(obj.StatusConditions(), conditionType) + + if condition == nil { + return fmt.Errorf("condition %s not set yet", conditionType) + } + + if condition.ObservedGeneration != obj.GetGeneration() { + return fmt.Errorf("condition %s is outdated", conditionType) + } + + if condition.Status == metav1.ConditionTrue { + 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 71974612a..6b45ed999 100644 --- a/api/repositories/conditions/await_test.go +++ b/api/repositories/conditions/await_test.go @@ -1,7 +1,6 @@ package conditions_test import ( - "errors" "sync" "time" @@ -12,9 +11,10 @@ import ( . "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("StateAwaiter", func() { +var _ = Describe("ConditionAwaiter", func() { var ( awaiter *conditions.Awaiter[*korifiv1alpha1.CFTask, korifiv1alpha1.CFTaskList, *korifiv1alpha1.CFTaskList] task *korifiv1alpha1.CFTask @@ -42,7 +42,7 @@ var _ = Describe("StateAwaiter", func() { } BeforeEach(func() { - awaiter = conditions.NewStateAwaiter[*korifiv1alpha1.CFTask, korifiv1alpha1.CFTaskList](time.Second) + awaiter = conditions.NewConditionAwaiter[*korifiv1alpha1.CFTask, korifiv1alpha1.CFTaskList](time.Second) awaitedTask = nil awaitErr = nil @@ -56,65 +56,67 @@ var _ = Describe("StateAwaiter", func() { Expect(k8sClient.Create(ctx, task)).To(Succeed()) }) - 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") - } + JustBeforeEach(func() { + awaitedTask, awaitErr = awaiter.AwaitCondition(ctx, k8sClient, task, korifiv1alpha1.TaskInitializedConditionType) + }) + + It("returns an error", func() { + Expect(awaitErr).To(MatchError(ContainSubstring("condition Initialized not set yet"))) + }) - return nil + When("the condition becomes false", func() { + BeforeEach(func() { + asyncPatchTask(func(cfTask *korifiv1alpha1.CFTask) { + meta.SetStatusCondition(&cfTask.Status.Conditions, metav1.Condition{ + Type: korifiv1alpha1.TaskInitializedConditionType, + Status: metav1.ConditionFalse, + Reason: "initialized", + ObservedGeneration: task.Generation, + }) }) }) - It("returns an error as the desired state is never reached", func() { - Expect(awaitErr).To(MatchError(ContainSubstring("droplet ref not set"))) + It("returns an error", func() { + Expect(awaitErr).To(MatchError(ContainSubstring("expected the Initialized condition to be true"))) }) + }) - When("the desired state is reached", func() { - BeforeEach(func() { - asyncPatchTask(func(cfTask *korifiv1alpha1.CFTask) { - cfTask.Status.DropletRef.Name = "some-droplet" + 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", + ObservedGeneration: task.Generation, }) }) - - It("succeeds and returns the updated object", func() { - Expect(awaitErr).NotTo(HaveOccurred()) - Expect(awaitedTask).NotTo(BeNil()) - - Expect(awaitedTask.Name).To(Equal(task.Name)) - Expect(awaitedTask.Status.DropletRef.Name).To(Equal("some-droplet")) - }) }) - }) - Describe("AwaitCondition", func() { - JustBeforeEach(func() { - awaitedTask, awaitErr = awaiter.AwaitCondition(ctx, k8sClient, task, korifiv1alpha1.TaskInitializedConditionType) - }) + It("succeeds and returns the updated object", func() { + Expect(awaitErr).NotTo(HaveOccurred()) + Expect(awaitedTask).NotTo(BeNil()) - It("returns an error as the condition never becomes true", func() { - Expect(awaitErr).To(MatchError(ContainSubstring("expected the Initialized condition to be true"))) + Expect(awaitedTask.Name).To(Equal(task.Name)) + Expect(meta.IsStatusConditionTrue(awaitedTask.Status.Conditions, korifiv1alpha1.TaskInitializedConditionType)).To(BeTrue()) }) + }) - 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", - }) + When("the condition becomes true but is outdated", func() { + BeforeEach(func() { + Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(task), task)).To(Succeed()) + asyncPatchTask(func(cfTask *korifiv1alpha1.CFTask) { + meta.SetStatusCondition(&cfTask.Status.Conditions, metav1.Condition{ + Type: korifiv1alpha1.TaskInitializedConditionType, + Status: metav1.ConditionTrue, + Reason: "initialized", + ObservedGeneration: task.Generation - 1, }) }) + }) - 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()) - }) + It("returns an error", func() { + Expect(awaitErr).To(MatchError(ContainSubstring("condition Initialized is outdated"))) }) }) }) diff --git a/api/repositories/fakeawaiter/await.go b/api/repositories/fakeawaiter/await.go index 4fda21020..bfd67f6ae 100644 --- a/api/repositories/fakeawaiter/await.go +++ b/api/repositories/fakeawaiter/await.go @@ -8,35 +8,13 @@ import ( ) 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 diff --git a/api/repositories/shared.go b/api/repositories/shared.go index 9848383ea..4a950964e 100644 --- a/api/repositories/shared.go +++ b/api/repositories/shared.go @@ -18,7 +18,6 @@ type RepositoryCreator interface { } 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) } diff --git a/controllers/controllers/services/bindings/controller.go b/controllers/controllers/services/bindings/controller.go index 88df50da5..2c5dabfc5 100644 --- a/controllers/controllers/services/bindings/controller.go +++ b/controllers/controllers/services/bindings/controller.go @@ -49,23 +49,22 @@ const ( ServiceBindingSecretTypePrefix = "servicebinding.io/" ) -// CFServiceBindingReconciler reconciles a CFServiceBinding object -type CFServiceBindingReconciler struct { +type Reconciler struct { k8sClient client.Client scheme *runtime.Scheme log logr.Logger } -func NewCFServiceBindingReconciler( +func NewReconciler( k8sClient client.Client, scheme *runtime.Scheme, log logr.Logger, ) *k8s.PatchingReconciler[korifiv1alpha1.CFServiceBinding, *korifiv1alpha1.CFServiceBinding] { - cfBindingReconciler := &CFServiceBindingReconciler{k8sClient: k8sClient, scheme: scheme, log: log} + cfBindingReconciler := &Reconciler{k8sClient: k8sClient, scheme: scheme, log: log} return k8s.NewPatchingReconciler[korifiv1alpha1.CFServiceBinding, *korifiv1alpha1.CFServiceBinding](log, k8sClient, cfBindingReconciler) } -func (r *CFServiceBindingReconciler) SetupWithManager(mgr ctrl.Manager) *builder.Builder { +func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) *builder.Builder { return ctrl.NewControllerManagedBy(mgr). For(&korifiv1alpha1.CFServiceBinding{}). Owns(&servicebindingv1beta1.ServiceBinding{}). @@ -79,7 +78,7 @@ func (r *CFServiceBindingReconciler) SetupWithManager(mgr ctrl.Manager) *builder ) } -func (r *CFServiceBindingReconciler) serviceInstanceToServiceBindings(ctx context.Context, o client.Object) []reconcile.Request { +func (r *Reconciler) serviceInstanceToServiceBindings(ctx context.Context, o client.Object) []reconcile.Request { serviceInstance := o.(*korifiv1alpha1.CFServiceInstance) serviceBindings := korifiv1alpha1.CFServiceBindingList{} @@ -103,7 +102,7 @@ func (r *CFServiceBindingReconciler) serviceInstanceToServiceBindings(ctx contex return requests } -func (r *CFServiceBindingReconciler) appToServiceBindings(ctx context.Context, o client.Object) []reconcile.Request { +func (r *Reconciler) appToServiceBindings(ctx context.Context, o client.Object) []reconcile.Request { cfApp := o.(*korifiv1alpha1.CFApp) serviceBindings := &korifiv1alpha1.CFServiceBindingList{} @@ -132,7 +131,7 @@ func (r *CFServiceBindingReconciler) appToServiceBindings(ctx context.Context, o //+kubebuilder:rbac:groups=korifi.cloudfoundry.org,resources=cfservicebindings/status,verbs=get;update;patch //+kubebuilder:rbac:groups=servicebinding.io,resources=servicebindings,verbs=get;list;create;update;patch;watch -func (r *CFServiceBindingReconciler) ReconcileResource(ctx context.Context, cfServiceBinding *korifiv1alpha1.CFServiceBinding) (ctrl.Result, error) { +func (r *Reconciler) ReconcileResource(ctx context.Context, cfServiceBinding *korifiv1alpha1.CFServiceBinding) (ctrl.Result, error) { log := logr.FromContextOrDiscard(ctx) cfServiceBinding.Status.ObservedGeneration = cfServiceBinding.Generation @@ -187,13 +186,6 @@ func (r *CFServiceBindingReconciler) ReconcileResource(ctx context.Context, cfSe return ctrl.Result{}, err } - if cfApp.Status.VCAPServicesSecretName == "" { - log.V(1).Info("did not find VCAPServiceSecret name on status of CFApp", "CFServiceBinding", cfServiceBinding.Name) - readyConditionBuilder.WithReason("VcapServicesSecretNotAvailable") - - return ctrl.Result{RequeueAfter: 2 * time.Second}, nil - } - sbServiceBinding, err := r.reconcileSBServiceBinding(ctx, cfServiceBinding, credentialsSecret) if err != nil { log.Info("error creating/updating servicebinding.io servicebinding", "reason", err) @@ -222,7 +214,7 @@ func isSbServiceBindingReady(sbServiceBinding *servicebindingv1beta1.ServiceBind return sbServiceBinding.Generation == sbServiceBinding.Status.ObservedGeneration } -func (r *CFServiceBindingReconciler) reconcileCredentials(ctx context.Context, cfServiceInstance *korifiv1alpha1.CFServiceInstance, cfServiceBinding *korifiv1alpha1.CFServiceBinding) (*corev1.Secret, error) { +func (r *Reconciler) reconcileCredentials(ctx context.Context, cfServiceInstance *korifiv1alpha1.CFServiceInstance, cfServiceBinding *korifiv1alpha1.CFServiceBinding) (*corev1.Secret, error) { cfServiceBinding.Status.Credentials.Name = cfServiceInstance.Status.Credentials.Name if isLegacyServiceBinding(cfServiceBinding, cfServiceInstance) { @@ -294,7 +286,7 @@ func isLegacyServiceBinding(cfServiceBinding *korifiv1alpha1.CFServiceBinding, c return cfServiceInstance.Name == cfServiceBinding.Status.Binding.Name && cfServiceInstance.Spec.SecretName == cfServiceBinding.Status.Binding.Name } -func (r *CFServiceBindingReconciler) reconcileSBServiceBinding(ctx context.Context, cfServiceBinding *korifiv1alpha1.CFServiceBinding, credentialsSecret *corev1.Secret) (*servicebindingv1beta1.ServiceBinding, error) { +func (r *Reconciler) reconcileSBServiceBinding(ctx context.Context, cfServiceBinding *korifiv1alpha1.CFServiceBinding, credentialsSecret *corev1.Secret) (*servicebindingv1beta1.ServiceBinding, error) { sbServiceBinding := r.toSBServiceBinding(cfServiceBinding) _, err := controllerutil.CreateOrPatch(ctx, r.k8sClient, sbServiceBinding, func() error { @@ -318,7 +310,7 @@ func (r *CFServiceBindingReconciler) reconcileSBServiceBinding(ctx context.Conte return sbServiceBinding, nil } -func (r *CFServiceBindingReconciler) toSBServiceBinding(cfServiceBinding *korifiv1alpha1.CFServiceBinding) *servicebindingv1beta1.ServiceBinding { +func (r *Reconciler) toSBServiceBinding(cfServiceBinding *korifiv1alpha1.CFServiceBinding) *servicebindingv1beta1.ServiceBinding { return &servicebindingv1beta1.ServiceBinding{ ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf("cf-binding-%s", cfServiceBinding.Name), diff --git a/controllers/controllers/services/bindings/controller_test.go b/controllers/controllers/services/bindings/controller_test.go index f55d328de..b459ea4fc 100644 --- a/controllers/controllers/services/bindings/controller_test.go +++ b/controllers/controllers/services/bindings/controller_test.go @@ -55,12 +55,6 @@ var _ = Describe("CFServiceBinding", func() { adminClient.Create(ctx, cfApp), ).To(Succeed()) - Expect(k8s.Patch(ctx, adminClient, cfApp, func() { - cfApp.Status = korifiv1alpha1.CFAppStatus{ - VCAPServicesSecretName: "foo", - } - })).To(Succeed()) - credentialsBytes, err := json.Marshal(map[string]any{ "obj": map[string]any{ "foo": "bar", @@ -521,25 +515,6 @@ var _ = Describe("CFServiceBinding", func() { }) }) - When("the app VCAP_SERVICES secret is not set in the CFApp status", func() { - BeforeEach(func() { - Expect(k8s.Patch(ctx, adminClient, cfApp, func() { - cfApp.Status.VCAPServicesSecretName = "" - })).To(Succeed()) - }) - - It("sets the Ready condition to false", func() { - Eventually(func(g Gomega) { - g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(binding), binding)).To(Succeed()) - g.Expect(binding.Status.Conditions).To(ContainElement(SatisfyAll( - HasType(Equal(korifiv1alpha1.StatusConditionReady)), - HasStatus(Equal(metav1.ConditionFalse)), - HasReason(Equal("VcapServicesSecretNotAvailable")), - ))) - }).Should(Succeed()) - }) - }) - When("the binding references a 'legacy' instance credentials secret", func() { JustBeforeEach(func() { Expect(k8s.Patch(ctx, adminClient, instance, func() { diff --git a/controllers/controllers/services/bindings/suite_test.go b/controllers/controllers/services/bindings/suite_test.go index 8dadee338..dd82e0d1e 100644 --- a/controllers/controllers/services/bindings/suite_test.go +++ b/controllers/controllers/services/bindings/suite_test.go @@ -78,11 +78,11 @@ var _ = BeforeSuite(func() { adminClient, stopClientCache = helpers.NewCachedClient(testEnv.Config) - err = (bindings.NewCFServiceBindingReconciler( + err = bindings.NewReconciler( k8sManager.GetClient(), k8sManager.GetScheme(), ctrl.Log.WithName("controllers").WithName("CFServiceBinding"), - )).SetupWithManager(k8sManager) + ).SetupWithManager(k8sManager) Expect(err).ToNot(HaveOccurred()) stopManager = helpers.StartK8sManager(k8sManager) diff --git a/controllers/controllers/services/instances/controller.go b/controllers/controllers/services/instances/controller.go index a80f1c857..e34cbadde 100644 --- a/controllers/controllers/services/instances/controller.go +++ b/controllers/controllers/services/instances/controller.go @@ -45,23 +45,22 @@ import ( const CredentialsSecretAvailableCondition = "CredentialSecretAvailable" -// CFServiceInstanceReconciler reconciles a CFServiceInstance object -type CFServiceInstanceReconciler struct { +type Reconciler struct { k8sClient client.Client scheme *runtime.Scheme log logr.Logger } -func NewCFServiceInstanceReconciler( +func NewReconciler( client client.Client, scheme *runtime.Scheme, log logr.Logger, ) *k8s.PatchingReconciler[korifiv1alpha1.CFServiceInstance, *korifiv1alpha1.CFServiceInstance] { - serviceInstanceReconciler := CFServiceInstanceReconciler{k8sClient: client, scheme: scheme, log: log} + serviceInstanceReconciler := Reconciler{k8sClient: client, scheme: scheme, log: log} return k8s.NewPatchingReconciler[korifiv1alpha1.CFServiceInstance, *korifiv1alpha1.CFServiceInstance](log, client, &serviceInstanceReconciler) } -func (r *CFServiceInstanceReconciler) SetupWithManager(mgr ctrl.Manager) *builder.Builder { +func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) *builder.Builder { return ctrl.NewControllerManagedBy(mgr). For(&korifiv1alpha1.CFServiceInstance{}). Watches( @@ -70,7 +69,7 @@ func (r *CFServiceInstanceReconciler) SetupWithManager(mgr ctrl.Manager) *builde ) } -func (r *CFServiceInstanceReconciler) secretToServiceInstance(ctx context.Context, o client.Object) []reconcile.Request { +func (r *Reconciler) secretToServiceInstance(ctx context.Context, o client.Object) []reconcile.Request { serviceInstances := korifiv1alpha1.CFServiceInstanceList{} if err := r.k8sClient.List(ctx, &serviceInstances, client.InNamespace(o.GetNamespace()), @@ -97,7 +96,7 @@ func (r *CFServiceInstanceReconciler) secretToServiceInstance(ctx context.Contex //+kubebuilder:rbac:groups=korifi.cloudfoundry.org,resources=cfserviceinstances/status,verbs=get;update;patch //+kubebuilder:rbac:groups=korifi.cloudfoundry.org,resources=cfserviceinstances/finalizers,verbs=update -func (r *CFServiceInstanceReconciler) ReconcileResource(ctx context.Context, cfServiceInstance *korifiv1alpha1.CFServiceInstance) (ctrl.Result, error) { +func (r *Reconciler) ReconcileResource(ctx context.Context, cfServiceInstance *korifiv1alpha1.CFServiceInstance) (ctrl.Result, error) { log := logr.FromContextOrDiscard(ctx) cfServiceInstance.Status.ObservedGeneration = cfServiceInstance.Generation @@ -160,7 +159,7 @@ func (r *CFServiceInstanceReconciler) ReconcileResource(ctx context.Context, cfS return ctrl.Result{}, nil } -func (r *CFServiceInstanceReconciler) reconcileCredentials(ctx context.Context, credentialsSecret *corev1.Secret, cfServiceInstance *korifiv1alpha1.CFServiceInstance) (*corev1.Secret, error) { +func (r *Reconciler) reconcileCredentials(ctx context.Context, credentialsSecret *corev1.Secret, cfServiceInstance *korifiv1alpha1.CFServiceInstance) (*corev1.Secret, error) { if !strings.HasPrefix(string(credentialsSecret.Type), bindings.ServiceBindingSecretTypePrefix) { return credentialsSecret, nil } @@ -200,7 +199,7 @@ func (r *CFServiceInstanceReconciler) reconcileCredentials(ctx context.Context, return migratedSecret, nil } -func (r *CFServiceInstanceReconciler) validateCredentials(ctx context.Context, credentialsSecret *corev1.Secret) error { +func (r *Reconciler) validateCredentials(ctx context.Context, credentialsSecret *corev1.Secret) error { return errors.Wrapf( json.Unmarshal(credentialsSecret.Data[korifiv1alpha1.CredentialsSecretKey], &map[string]any{}), "invalid credentials secret %q", diff --git a/controllers/controllers/services/instances/suite_test.go b/controllers/controllers/services/instances/suite_test.go index 2909d20f5..f0f30ef38 100644 --- a/controllers/controllers/services/instances/suite_test.go +++ b/controllers/controllers/services/instances/suite_test.go @@ -75,7 +75,7 @@ var _ = BeforeSuite(func() { adminClient, stopClientCache = helpers.NewCachedClient(testEnv.Config) - err = (instances.NewCFServiceInstanceReconciler( + err = (instances.NewReconciler( k8sManager.GetClient(), k8sManager.GetScheme(), ctrl.Log.WithName("controllers").WithName("CFServiceInstance"), diff --git a/controllers/controllers/workloads/cfapp_controller.go b/controllers/controllers/workloads/apps/controller.go similarity index 81% rename from controllers/controllers/workloads/cfapp_controller.go rename to controllers/controllers/workloads/apps/controller.go index 884ea92fe..a0875c323 100644 --- a/controllers/controllers/workloads/cfapp_controller.go +++ b/controllers/controllers/workloads/apps/controller.go @@ -1,4 +1,4 @@ -package workloads +package apps import ( "context" @@ -29,8 +29,7 @@ type EnvValueBuilder interface { BuildEnvValue(context.Context, *korifiv1alpha1.CFApp) (map[string][]byte, error) } -// CFAppReconciler reconciles a CFApp object -type CFAppReconciler struct { +type Reconciler struct { log logr.Logger k8sClient client.Client scheme *runtime.Scheme @@ -38,8 +37,8 @@ type CFAppReconciler struct { vcapApplicationEnvBuilder EnvValueBuilder } -func NewCFAppReconciler(k8sClient client.Client, scheme *runtime.Scheme, log logr.Logger, vcapServicesBuilder, vcapApplicationBuilder EnvValueBuilder) *k8s.PatchingReconciler[korifiv1alpha1.CFApp, *korifiv1alpha1.CFApp] { - appReconciler := CFAppReconciler{ +func NewReconciler(k8sClient client.Client, scheme *runtime.Scheme, log logr.Logger, vcapServicesBuilder, vcapApplicationBuilder EnvValueBuilder) *k8s.PatchingReconciler[korifiv1alpha1.CFApp, *korifiv1alpha1.CFApp] { + appReconciler := Reconciler{ log: log, k8sClient: k8sClient, scheme: scheme, @@ -49,7 +48,7 @@ func NewCFAppReconciler(k8sClient client.Client, scheme *runtime.Scheme, log log return k8s.NewPatchingReconciler[korifiv1alpha1.CFApp, *korifiv1alpha1.CFApp](log, k8sClient, &appReconciler) } -func (r *CFAppReconciler) SetupWithManager(mgr ctrl.Manager) *builder.Builder { +func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) *builder.Builder { return ctrl.NewControllerManagedBy(mgr). For(&korifiv1alpha1.CFApp{}). Owns(&korifiv1alpha1.CFProcess{}). @@ -101,20 +100,20 @@ func serviceBindingToApp(ctx context.Context, o client.Object) []reconcile.Reque //+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;patch -func (r *CFAppReconciler) ReconcileResource(ctx context.Context, cfApp *korifiv1alpha1.CFApp) (ctrl.Result, error) { +func (r *Reconciler) ReconcileResource(ctx context.Context, cfApp *korifiv1alpha1.CFApp) (ctrl.Result, error) { log := logr.FromContextOrDiscard(ctx) - cfApp.Status.ObservedGeneration = cfApp.Generation - log.V(1).Info("set observed generation", "generation", cfApp.Status.ObservedGeneration) - - cfApp.Status.ActualState = korifiv1alpha1.StoppedState - var err error readyConditionBuilder := k8s.NewReadyConditionBuilder(cfApp) defer func() { meta.SetStatusCondition(&cfApp.Status.Conditions, readyConditionBuilder.WithError(err).Build()) }() + cfApp.Status.ObservedGeneration = cfApp.Generation + log.V(1).Info("set observed generation", "generation", cfApp.Status.ObservedGeneration) + + cfApp.Status.ActualState = korifiv1alpha1.StoppedState + if !cfApp.GetDeletionTimestamp().IsZero() { return r.finalizeCFApp(ctx, cfApp) } @@ -126,6 +125,16 @@ func (r *CFAppReconciler) ReconcileResource(ctx context.Context, cfApp *korifiv1 cfApp.Annotations[korifiv1alpha1.CFAppLastStopRevisionKey] = cfApp.Annotations[korifiv1alpha1.CFAppRevisionKey] } + bindingsReady, err := r.serviceBindingsReady(ctx, cfApp) + if err != nil { + return ctrl.Result{}, err + } + + if !bindingsReady { + readyConditionBuilder.WithReason("BindingNotReady") + return ctrl.Result{}, nil + } + secretName := cfApp.Name + "-vcap-application" err = r.reconcileVCAPSecret(ctx, cfApp, secretName, r.vcapApplicationEnvBuilder) if err != nil { @@ -158,11 +167,33 @@ func (r *CFAppReconciler) ReconcileResource(ctx context.Context, cfApp *korifiv1 } cfApp.Status.ActualState = getActualState(reconciledProcesses) + if cfApp.Status.ActualState != cfApp.Spec.DesiredState { + readyConditionBuilder.WithReason("DesiredStateNotReached") + return ctrl.Result{}, nil + } readyConditionBuilder.Ready() return ctrl.Result{}, nil } +func (r *Reconciler) serviceBindingsReady(ctx context.Context, cfApp *korifiv1alpha1.CFApp) (bool, error) { + bindings := &korifiv1alpha1.CFServiceBindingList{} + if err := r.k8sClient.List(ctx, bindings, + client.InNamespace(cfApp.Namespace), + client.MatchingFields{shared.IndexServiceBindingAppGUID: cfApp.Name}, + ); err != nil { + return false, err + } + + for _, binding := range bindings.Items { + if !meta.IsStatusConditionTrue(binding.Status.Conditions, korifiv1alpha1.StatusConditionReady) { + return false, nil + } + } + + return true, nil +} + func getActualState(processes []*korifiv1alpha1.CFProcess) korifiv1alpha1.AppState { processInstances := int32(0) for _, p := range processes { @@ -175,7 +206,7 @@ func getActualState(processes []*korifiv1alpha1.CFProcess) korifiv1alpha1.AppSta return korifiv1alpha1.StartedState } -func (r *CFAppReconciler) getDroplet(ctx context.Context, cfApp *korifiv1alpha1.CFApp) (*korifiv1alpha1.BuildDropletStatus, error) { +func (r *Reconciler) getDroplet(ctx context.Context, cfApp *korifiv1alpha1.CFApp) (*korifiv1alpha1.BuildDropletStatus, error) { log := logr.FromContextOrDiscard(ctx).WithName("getDroplet").WithValues("dropletName", cfApp.Spec.CurrentDropletRef.Name) var cfBuild korifiv1alpha1.CFBuild @@ -194,7 +225,7 @@ func (r *CFAppReconciler) getDroplet(ctx context.Context, cfApp *korifiv1alpha1. return cfBuild.Status.Droplet, nil } -func (r *CFAppReconciler) reconcileProcesses(ctx context.Context, cfApp *korifiv1alpha1.CFApp, droplet *korifiv1alpha1.BuildDropletStatus) ([]*korifiv1alpha1.CFProcess, error) { +func (r *Reconciler) reconcileProcesses(ctx context.Context, cfApp *korifiv1alpha1.CFApp, droplet *korifiv1alpha1.BuildDropletStatus) ([]*korifiv1alpha1.CFProcess, error) { log := logr.FromContextOrDiscard(ctx).WithName("startApp") reconciledProcess := []*korifiv1alpha1.CFProcess{} @@ -239,13 +270,13 @@ func addWebIfMissing(processTypes []korifiv1alpha1.ProcessType) []korifiv1alpha1 return append([]korifiv1alpha1.ProcessType{{Type: korifiv1alpha1.ProcessTypeWeb}}, processTypes...) } -func (r *CFAppReconciler) updateCFProcess(ctx context.Context, process *korifiv1alpha1.CFProcess, command string) error { +func (r *Reconciler) updateCFProcess(ctx context.Context, process *korifiv1alpha1.CFProcess, command string) error { return k8s.Patch(ctx, r.k8sClient, process, func() { process.Spec.DetectedCommand = command }) } -func (r *CFAppReconciler) createCFProcess(ctx context.Context, process korifiv1alpha1.ProcessType, cfApp *korifiv1alpha1.CFApp) (*korifiv1alpha1.CFProcess, error) { +func (r *Reconciler) createCFProcess(ctx context.Context, process korifiv1alpha1.ProcessType, cfApp *korifiv1alpha1.CFApp) (*korifiv1alpha1.CFProcess, error) { desiredCFProcess := &korifiv1alpha1.CFProcess{ ObjectMeta: metav1.ObjectMeta{ Namespace: cfApp.Namespace, @@ -275,7 +306,7 @@ func (r *CFAppReconciler) createCFProcess(ctx context.Context, process korifiv1a return desiredCFProcess, nil } -func (r *CFAppReconciler) fetchProcessByType(ctx context.Context, appGUID, appNamespace, processType string) (*korifiv1alpha1.CFProcess, error) { +func (r *Reconciler) fetchProcessByType(ctx context.Context, appGUID, appNamespace, processType string) (*korifiv1alpha1.CFProcess, error) { selector, err := labels.ValidatedSelectorFromSet(map[string]string{ korifiv1alpha1.CFAppGUIDLabelKey: appGUID, korifiv1alpha1.CFProcessTypeLabelKey: processType, @@ -299,7 +330,7 @@ func (r *CFAppReconciler) fetchProcessByType(ctx context.Context, appGUID, appNa return &cfProcessList.Items[0], nil } -func (r *CFAppReconciler) finalizeCFApp(ctx context.Context, cfApp *korifiv1alpha1.CFApp) (ctrl.Result, error) { +func (r *Reconciler) finalizeCFApp(ctx context.Context, cfApp *korifiv1alpha1.CFApp) (ctrl.Result, error) { log := logr.FromContextOrDiscard(ctx).WithName("finalizeCFApp") if !controllerutil.ContainsFinalizer(cfApp, korifiv1alpha1.CFAppFinalizerName) { @@ -327,7 +358,7 @@ func (r *CFAppReconciler) finalizeCFApp(ctx context.Context, cfApp *korifiv1alph return ctrl.Result{}, nil } -func (r *CFAppReconciler) finalizeCFAppRoutes(ctx context.Context, cfApp *korifiv1alpha1.CFApp) error { +func (r *Reconciler) finalizeCFAppRoutes(ctx context.Context, cfApp *korifiv1alpha1.CFApp) error { cfRoutes, err := r.getCFRoutes(ctx, cfApp.Name, cfApp.Namespace) if err != nil { return err @@ -341,7 +372,7 @@ func (r *CFAppReconciler) finalizeCFAppRoutes(ctx context.Context, cfApp *korifi return nil } -func (r *CFAppReconciler) finalizeCFServiceBindings(ctx context.Context, cfApp *korifiv1alpha1.CFApp) (ctrl.Result, error) { +func (r *Reconciler) finalizeCFServiceBindings(ctx context.Context, cfApp *korifiv1alpha1.CFApp) (ctrl.Result, error) { log := logr.FromContextOrDiscard(ctx).WithName("finalizeCFServiceBindings") sbList := korifiv1alpha1.CFServiceBindingList{} @@ -366,7 +397,7 @@ func (r *CFAppReconciler) finalizeCFServiceBindings(ctx context.Context, cfApp * return ctrl.Result{RequeueAfter: time.Second}, nil } -func (r *CFAppReconciler) updateRouteDestinations(ctx context.Context, cfAppGUID string, cfRoutes []korifiv1alpha1.CFRoute) error { +func (r *Reconciler) updateRouteDestinations(ctx context.Context, cfAppGUID string, cfRoutes []korifiv1alpha1.CFRoute) error { log := logr.FromContextOrDiscard(ctx).WithName("updateRouteDestinations") for i := range cfRoutes { @@ -394,7 +425,7 @@ func (r *CFAppReconciler) updateRouteDestinations(ctx context.Context, cfAppGUID return nil } -func (r *CFAppReconciler) getCFRoutes(ctx context.Context, cfAppGUID string, cfAppNamespace string) ([]korifiv1alpha1.CFRoute, error) { +func (r *Reconciler) getCFRoutes(ctx context.Context, cfAppGUID string, cfAppNamespace string) ([]korifiv1alpha1.CFRoute, error) { log := logr.FromContextOrDiscard(ctx).WithName("getCFRoutes") var foundRoutes korifiv1alpha1.CFRouteList @@ -408,7 +439,7 @@ func (r *CFAppReconciler) getCFRoutes(ctx context.Context, cfAppGUID string, cfA return foundRoutes.Items, nil } -func (r *CFAppReconciler) reconcileVCAPSecret( +func (r *Reconciler) reconcileVCAPSecret( ctx context.Context, cfApp *korifiv1alpha1.CFApp, secretName string, diff --git a/controllers/controllers/workloads/apps/controller_test.go b/controllers/controllers/workloads/apps/controller_test.go new file mode 100644 index 000000000..d6b4ed5e8 --- /dev/null +++ b/controllers/controllers/workloads/apps/controller_test.go @@ -0,0 +1,497 @@ +package apps_test + +import ( + korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" + . "code.cloudfoundry.org/korifi/controllers/controllers/workloads/testutils" + . "code.cloudfoundry.org/korifi/tests/matchers" + "code.cloudfoundry.org/korifi/tools" + "code.cloudfoundry.org/korifi/tools/k8s" + + "github.com/google/uuid" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gstruct" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var _ = Describe("CFAppReconciler Integration Tests", func() { + var ( + cfBuild *korifiv1alpha1.CFBuild + cfApp *korifiv1alpha1.CFApp + ) + + BeforeEach(func() { + appName := uuid.NewString() + + cfPackage := &korifiv1alpha1.CFPackage{ + ObjectMeta: metav1.ObjectMeta{ + Name: uuid.NewString(), + Namespace: testNamespace, + }, + Spec: korifiv1alpha1.CFPackageSpec{ + Type: "bits", + AppRef: corev1.LocalObjectReference{ + Name: appName, + }, + Source: korifiv1alpha1.PackageSource{ + Registry: korifiv1alpha1.Registry{ + Image: "ref", + ImagePullSecrets: []corev1.LocalObjectReference{{Name: "source-registry-image-pull-secret"}}, + }, + }, + }, + } + Expect(adminClient.Create(ctx, cfPackage)).To(Succeed()) + + cfBuild = &korifiv1alpha1.CFBuild{ + ObjectMeta: metav1.ObjectMeta{ + Name: uuid.NewString(), + Namespace: testNamespace, + }, + Spec: korifiv1alpha1.CFBuildSpec{ + PackageRef: corev1.LocalObjectReference{ + Name: cfPackage.Name, + }, + AppRef: corev1.LocalObjectReference{ + Name: appName, + }, + StagingMemoryMB: 1024, + StagingDiskMB: 1024, + Lifecycle: korifiv1alpha1.Lifecycle{ + Type: "buildpack", + // TODO: no need for Data as it empty + Data: korifiv1alpha1.LifecycleData{ + Buildpacks: nil, + Stack: "", + }, + }, + }, + } + Expect(adminClient.Create(ctx, cfBuild)).To(Succeed()) + Expect(k8s.Patch(ctx, adminClient, cfBuild, func() { + cfBuild.Status = korifiv1alpha1.CFBuildStatus{ + Droplet: &korifiv1alpha1.BuildDropletStatus{ + Registry: korifiv1alpha1.Registry{ + Image: "image/registry/url", + ImagePullSecrets: []corev1.LocalObjectReference{{Name: "some-image-pull-secret"}}, + }, + Stack: "cflinuxfs3", + }, + } + })).To(Succeed()) + + cfApp = &korifiv1alpha1.CFApp{ + ObjectMeta: metav1.ObjectMeta{ + Name: appName, + Namespace: testNamespace, + Annotations: map[string]string{ + korifiv1alpha1.CFAppRevisionKey: "42", + }, + Finalizers: []string{ + korifiv1alpha1.CFAppFinalizerName, + }, + }, + Spec: korifiv1alpha1.CFAppSpec{ + DisplayName: "test-app", + DesiredState: korifiv1alpha1.StoppedState, + Lifecycle: korifiv1alpha1.Lifecycle{ + Type: "buildpack", + }, + CurrentDropletRef: corev1.LocalObjectReference{ + Name: cfBuild.Name, + }, + }, + } + Expect(adminClient.Create(ctx, cfApp)).To(Succeed()) + Expect(k8s.Patch(ctx, adminClient, cfApp, func() { + cfApp.Status.ActualState = korifiv1alpha1.StoppedState + })).To(Succeed()) + }) + + It("sets the observed generation in the cfapp status", func() { + Eventually(func(g Gomega) { + g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(cfApp), cfApp)).To(Succeed()) + g.Expect(cfApp.Status.ObservedGeneration).To(BeEquivalentTo(cfApp.Generation)) + }).Should(Succeed()) + }) + + It("sets the app actual state to the desired state", func() { + Eventually(func(g Gomega) { + g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(cfApp), cfApp)).To(Succeed()) + g.Expect(cfApp.Status.ActualState).To(Equal(korifiv1alpha1.StoppedState)) + }).Should(Succeed()) + }) + + It("sets the last-stop-app-rev annotation to the value of the app-rev annotation", func() { + Eventually(func(g Gomega) { + g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(cfApp), cfApp)).To(Succeed()) + g.Expect(cfApp.Annotations[korifiv1alpha1.CFAppLastStopRevisionKey]).To(Equal("42")) + }).Should(Succeed()) + }) + + It("sets status.VCAPApplicationSecretName", func() { + Eventually(func(g Gomega) { + g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(cfApp), cfApp)).To(Succeed()) + + g.Expect(cfApp.Status.VCAPApplicationSecretName).NotTo(BeEmpty()) + g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(&corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: cfApp.Namespace, + Name: cfApp.Status.VCAPApplicationSecretName, + }, + }), &corev1.Secret{})).To(Succeed()) + }).Should(Succeed()) + }) + + It("set status.VCAPServicesSecretName", func() { + Eventually(func(g Gomega) { + g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(cfApp), cfApp)).To(Succeed()) + g.Expect(cfApp.Status.VCAPServicesSecretName).NotTo(BeEmpty()) + + g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(&corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: cfApp.Namespace, + Name: cfApp.Status.VCAPServicesSecretName, + }, + }), &corev1.Secret{})).To(Succeed()) + }).Should(Succeed()) + }) + + When("lastStopAppRev annotation is set", func() { + BeforeEach(func() { + Eventually(func(g Gomega) { + g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(cfApp), cfApp)).To(Succeed()) + g.Expect(cfApp.Annotations[korifiv1alpha1.CFAppLastStopRevisionKey]).To(Equal("42")) + }).Should(Succeed()) + + Expect(k8s.PatchResource(ctx, adminClient, cfApp, func() { + cfApp.Annotations[korifiv1alpha1.CFAppLastStopRevisionKey] = "2" + })).To(Succeed()) + }) + + It("does not override it", func() { + Eventually(func(g Gomega) { + g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(cfApp), cfApp)).To(Succeed()) + g.Expect(cfApp.Annotations[korifiv1alpha1.CFAppLastStopRevisionKey]).To(Equal("2")) + }).Should(Succeed()) + Consistently(func(g Gomega) { + g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(cfApp), cfApp)).To(Succeed()) + g.Expect(cfApp.Annotations[korifiv1alpha1.CFAppLastStopRevisionKey]).To(Equal("2")) + }, "1s").Should(Succeed()) + }) + }) + + It("sets the ready condition to true", func() { + Eventually(func(g Gomega) { + g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(cfApp), cfApp)).To(Succeed()) + g.Expect(cfApp.Status.Conditions).To(ContainElement(SatisfyAll( + HasType(Equal(korifiv1alpha1.StatusConditionReady)), + HasStatus(Equal(metav1.ConditionTrue)), + ))) + }).Should(Succeed()) + }) + + It("creates a default web CFProcess", func() { + Eventually(func(g Gomega) { + cfProcessList := &korifiv1alpha1.CFProcessList{} + g.Expect( + adminClient.List(ctx, cfProcessList, &client.ListOptions{ + Namespace: cfApp.Namespace, + }), + ).To(Succeed()) + g.Expect(cfProcessList.Items).To(ConsistOf( + MatchFields(IgnoreExtras, Fields{ + "Spec": MatchFields(IgnoreExtras, Fields{ + "ProcessType": Equal("web"), + "DetectedCommand": BeEmpty(), + "Command": BeEmpty(), + "AppRef": Equal(corev1.LocalObjectReference{Name: cfApp.Name}), + }), + "ObjectMeta": MatchFields(IgnoreExtras, Fields{ + "OwnerReferences": ConsistOf(MatchFields(IgnoreExtras, Fields{ + "Name": Equal(cfApp.Name), + })), + }), + }), + )) + }).Should(Succeed()) + }) + + When("the droplet specifies processes", func() { + BeforeEach(func() { + Expect(k8s.Patch(ctx, adminClient, cfBuild, func() { + cfBuild.Status.Droplet.ProcessTypes = []korifiv1alpha1.ProcessType{{ + Type: "web", + Command: "web-process command", + }, { + Type: "worker", + Command: "process-worker command", + }} + })).To(Succeed()) + }) + + It("creates a CFProcess per droplet process", func() { + Eventually(func(g Gomega) { + cfProcessList := &korifiv1alpha1.CFProcessList{} + g.Expect( + adminClient.List(ctx, cfProcessList, &client.ListOptions{ + Namespace: cfApp.Namespace, + }), + ).To(Succeed()) + g.Expect(cfProcessList.Items).To(ConsistOf( + MatchFields(IgnoreExtras, Fields{ + "Spec": MatchFields(IgnoreExtras, Fields{ + "ProcessType": Equal("web"), + "DetectedCommand": Equal("web-process command"), + "Command": BeEmpty(), + }), + }), + MatchFields(IgnoreExtras, Fields{ + "Spec": MatchFields(IgnoreExtras, Fields{ + "ProcessType": Equal("worker"), + "DetectedCommand": Equal("process-worker command"), + "Command": BeEmpty(), + }), + }), + )) + }).Should(Succeed()) + }) + }) + + When("the app desired state does not match the actual state", func() { + BeforeEach(func() { + Eventually(func(g Gomega) { + cfProcessList := &korifiv1alpha1.CFProcessList{} + g.Expect(adminClient.List(ctx, cfProcessList, &client.ListOptions{ + Namespace: cfApp.Namespace, + })).To(Succeed()) + g.Expect(cfProcessList.Items).To(HaveLen(1)) + + process := &cfProcessList.Items[0] + g.Expect(k8s.Patch(ctx, adminClient, process, func() { + process.Status.ActualInstances = 1 + })).To(Succeed()) + + g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(cfApp), cfApp)).To(Succeed()) + g.Expect(cfApp.Status.ActualState).To(Equal(korifiv1alpha1.StartedState)) + }).Should(Succeed()) + }) + + It("sets the ready condition to false", func() { + Eventually(func(g Gomega) { + g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(cfApp), cfApp)).To(Succeed()) + g.Expect(cfApp.Status.Conditions).To(ContainElement(SatisfyAll( + HasType(Equal(korifiv1alpha1.StatusConditionReady)), + HasStatus(Equal(metav1.ConditionFalse)), + HasReason(Equal("DesiredStateNotReached")), + ))) + }).Should(Succeed()) + }) + }) + + When("the cfapp droplet ref is not set", func() { + BeforeEach(func() { + Expect(k8s.Patch(ctx, adminClient, cfApp, func() { + cfApp.Spec.CurrentDropletRef.Name = "" + })).To(Succeed()) + }) + + It("sets the ready condition to false", func() { + Eventually(func(g Gomega) { + g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(cfApp), cfApp)).To(Succeed()) + g.Expect(cfApp.Status.Conditions).To(ContainElement(SatisfyAll( + HasType(Equal(korifiv1alpha1.StatusConditionReady)), + HasStatus(Equal(metav1.ConditionFalse)), + HasReason(Equal("DropletNotAssigned")), + ))) + }).Should(Succeed()) + }) + }) + + When("the current droplet does not exist", func() { + BeforeEach(func() { + Expect(k8s.PatchResource(ctx, adminClient, cfApp, func() { + cfApp.Spec.CurrentDropletRef.Name = "i-do-not-exist" + })).To(Succeed()) + }) + + It("sets the ready condition to false", func() { + Eventually(func(g Gomega) { + g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(cfApp), cfApp)).To(Succeed()) + g.Expect(cfApp.Status.Conditions).To(ContainElement(SatisfyAll( + HasType(Equal(korifiv1alpha1.StatusConditionReady)), + HasStatus(Equal(metav1.ConditionFalse)), + HasReason(Equal("CannotResolveCurrentDropletRef")), + ))) + }).Should(Succeed()) + }) + }) + + When("the app has a service binding", func() { + var binding *korifiv1alpha1.CFServiceBinding + + BeforeEach(func() { + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: uuid.NewString(), + Namespace: cfApp.Namespace, + }, + Data: map[string][]byte{ + korifiv1alpha1.CredentialsSecretKey: []byte("{}"), + }, + } + Expect(adminClient.Create(ctx, secret)).To(Succeed()) + + instance := &korifiv1alpha1.CFServiceInstance{ + ObjectMeta: metav1.ObjectMeta{ + Name: uuid.NewString(), + Namespace: cfApp.Namespace, + }, + Spec: korifiv1alpha1.CFServiceInstanceSpec{ + SecretName: secret.Name, + Type: "user-provided", + }, + } + Expect(adminClient.Create(ctx, instance)).To(Succeed()) + + binding = &korifiv1alpha1.CFServiceBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: uuid.NewString(), + Namespace: cfApp.Namespace, + }, + Spec: korifiv1alpha1.CFServiceBindingSpec{ + DisplayName: new(string), + Service: corev1.ObjectReference{ + Namespace: cfApp.Namespace, + Name: instance.Name, + }, + AppRef: corev1.LocalObjectReference{Name: cfApp.Name}, + }, + } + Expect(adminClient.Create(ctx, binding)).To(Succeed()) + + Expect(k8s.Patch(ctx, adminClient, binding, func() { + binding.Status.Credentials.Name = secret.Name + })).To(Succeed()) + }) + + It("sets the ready condition to false", func() { + Eventually(func(g Gomega) { + g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(cfApp), cfApp)).To(Succeed()) + g.Expect(cfApp.Status.Conditions).To(ContainElement(SatisfyAll( + HasType(Equal(korifiv1alpha1.StatusConditionReady)), + HasStatus(Equal(metav1.ConditionFalse)), + HasReason(Equal("BindingNotReady")), + ))) + }).Should(Succeed()) + }) + + When("the binding becomes ready", func() { + BeforeEach(func() { + Expect(k8s.Patch(ctx, adminClient, binding, func() { + meta.SetStatusCondition(&binding.Status.Conditions, metav1.Condition{ + Type: korifiv1alpha1.StatusConditionReady, + Status: metav1.ConditionTrue, + Reason: "BindingReady", + }) + })).To(Succeed()) + }) + + It("sets the ready condition to true", func() { + Eventually(func(g Gomega) { + g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(cfApp), cfApp)).To(Succeed()) + g.Expect(cfApp.Status.Conditions).To(ContainElement(SatisfyAll( + HasType(Equal(korifiv1alpha1.StatusConditionReady)), + HasStatus(Equal(metav1.ConditionTrue)), + ))) + }).Should(Succeed()) + }) + }) + }) + + Describe("finalization", func() { + var ( + cfDomainGUID string + cfRoute *korifiv1alpha1.CFRoute + ) + + BeforeEach(func() { + cfDomainGUID = uuid.NewString() + cfDomain := &korifiv1alpha1.CFDomain{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testNamespace, + Name: cfDomainGUID, + }, + Spec: korifiv1alpha1.CFDomainSpec{ + Name: "a" + uuid.NewString() + ".com", + }, + } + Expect(adminClient.Create(ctx, cfDomain)).To(Succeed()) + + cfRoute = &korifiv1alpha1.CFRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: uuid.NewString(), + Namespace: testNamespace, + }, + Spec: korifiv1alpha1.CFRouteSpec{ + Host: "test-route-host", + Path: "", + Protocol: "http", + DomainRef: corev1.ObjectReference{ + Name: cfDomainGUID, + Namespace: testNamespace, + }, + Destinations: []korifiv1alpha1.Destination{ + { + GUID: "destination-1-guid", + AppRef: corev1.LocalObjectReference{ + Name: cfApp.Name, + }, + ProcessType: "web", + Protocol: tools.PtrTo("http1"), + }, + }, + }, + } + Expect(adminClient.Create(ctx, cfRoute)).To(Succeed()) + + cfServiceBinding := korifiv1alpha1.CFServiceBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: PrefixedGUID("service-binding"), + Namespace: testNamespace, + }, + Spec: korifiv1alpha1.CFServiceBindingSpec{ + AppRef: corev1.LocalObjectReference{ + Name: cfApp.Name, + }, + }, + } + Expect(adminClient.Create(ctx, &cfServiceBinding)).To(Succeed()) + + Expect(adminClient.Delete(ctx, cfApp)).To(Succeed()) + Eventually(func(g Gomega) { + err := adminClient.Get(ctx, client.ObjectKeyFromObject(cfApp), cfApp) + g.Expect(apierrors.IsNotFound(err)).To(BeTrue()) + }).Should(Succeed()) + }) + + It("deletes the destination on the CFRoute", func() { + Eventually(func(g Gomega) { + g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(cfRoute), cfRoute)).To(Succeed()) + g.Expect(cfRoute.Spec.Destinations).To(BeEmpty()) + }).Should(Succeed()) + }) + + It("deletes the referencing service bindings", func() { + Eventually(func(g Gomega) { + sbList := korifiv1alpha1.CFServiceBindingList{} + g.Expect(adminClient.List(ctx, &sbList, client.InNamespace(cfApp.Namespace))).To(Succeed()) + g.Expect(sbList.Items).To(BeEmpty()) + }).Should(Succeed()) + }) + }) +}) diff --git a/controllers/controllers/workloads/apps/suite_test.go b/controllers/controllers/workloads/apps/suite_test.go new file mode 100644 index 000000000..ef9e97087 --- /dev/null +++ b/controllers/controllers/workloads/apps/suite_test.go @@ -0,0 +1,123 @@ +package apps_test + +import ( + "context" + "path/filepath" + "testing" + "time" + + korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" + "code.cloudfoundry.org/korifi/controllers/controllers/shared" + "code.cloudfoundry.org/korifi/controllers/controllers/workloads/apps" + "code.cloudfoundry.org/korifi/controllers/controllers/workloads/env" + "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" + "go.uber.org/zap/zapcore" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/scheme" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" +) + +var ( + ctx context.Context + stopManager context.CancelFunc + stopClientCache context.CancelFunc + testEnv *envtest.Environment + adminClient client.Client + testNamespace string +) + +func TestWorkloadsControllers(t *testing.T) { + SetDefaultEventuallyTimeout(10 * time.Second) + SetDefaultEventuallyPollingInterval(250 * time.Millisecond) + + RegisterFailHandler(Fail) + RunSpecs(t, "CFApp Controller Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true), zap.Level(zapcore.DebugLevel))) + + ctx = context.Background() + + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{ + filepath.Join("..", "..", "..", "..", "helm", "korifi", "controllers", "crds"), + }, + ErrorIfCRDPathMissing: true, + } + + _, err := testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + + Expect(korifiv1alpha1.AddToScheme(scheme.Scheme)).To(Succeed()) + Expect(corev1.AddToScheme(scheme.Scheme)).To(Succeed()) + + k8sManager := helpers.NewK8sManager(testEnv, filepath.Join("helm", "korifi", "controllers", "role.yaml")) + Expect(shared.SetupIndexWithManager(k8sManager)).To(Succeed()) + + adminClient, stopClientCache = helpers.NewCachedClient(testEnv.Config) + + err = apps.NewReconciler( + k8sManager.GetClient(), + k8sManager.GetScheme(), + ctrl.Log.WithName("controllers").WithName("CFApp"), + env.NewVCAPServicesEnvValueBuilder(k8sManager.GetClient()), + env.NewVCAPApplicationEnvValueBuilder(k8sManager.GetClient(), nil), + ).SetupWithManager(k8sManager) + Expect(err).NotTo(HaveOccurred()) + + stopManager = helpers.StartK8sManager(k8sManager) +}) + +var _ = AfterSuite(func() { + stopManager() + stopClientCache() + Expect(testEnv.Stop()).To(Succeed()) +}) + +var _ = BeforeEach(func() { + testNamespace = uuid.NewString() + Expect(adminClient.Create(ctx, &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: testNamespace, + }, + })).To(Succeed()) + + cfOrg := &korifiv1alpha1.CFOrg{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testNamespace, + Name: testNamespace, + }, + Spec: korifiv1alpha1.CFOrgSpec{ + DisplayName: uuid.NewString(), + }, + } + Expect(adminClient.Create(ctx, cfOrg)).To(Succeed()) + Expect(k8s.Patch(ctx, adminClient, cfOrg, func() { + cfOrg.Status.GUID = testNamespace + })).To(Succeed()) + + cfSpace := &korifiv1alpha1.CFSpace{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testNamespace, + Name: testNamespace, + }, + Spec: korifiv1alpha1.CFSpaceSpec{ + DisplayName: uuid.NewString(), + }, + } + Expect(adminClient.Create(ctx, cfSpace)).To(Succeed()) + Expect(k8s.Patch(ctx, adminClient, cfSpace, func() { + cfSpace.Status.GUID = testNamespace + })).To(Succeed()) +}) diff --git a/controllers/controllers/workloads/cfapp_controller_test.go b/controllers/controllers/workloads/cfapp_controller_test.go deleted file mode 100644 index f6c2b5c80..000000000 --- a/controllers/controllers/workloads/cfapp_controller_test.go +++ /dev/null @@ -1,576 +0,0 @@ -package workloads_test - -import ( - "context" - "time" - - korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" - . "code.cloudfoundry.org/korifi/controllers/controllers/workloads/testutils" - "code.cloudfoundry.org/korifi/tools" - "code.cloudfoundry.org/korifi/tools/k8s" - - . "code.cloudfoundry.org/korifi/tests/matchers" - "github.com/google/uuid" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - corev1 "k8s.io/api/core/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -var _ = Describe("CFAppReconciler Integration Tests", func() { - var ( - cfSpace *korifiv1alpha1.CFSpace - cfDomainGUID string - ) - - BeforeEach(func() { - cfSpace = createSpace(testOrg) - cfDomainGUID = PrefixedGUID("test-domain") - cfDomain := &korifiv1alpha1.CFDomain{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: cfSpace.Status.GUID, - Name: cfDomainGUID, - }, - Spec: korifiv1alpha1.CFDomainSpec{ - Name: "a" + uuid.NewString() + ".com", - }, - } - Expect(adminClient.Create(ctx, cfDomain)).To(Succeed()) - }) - - When("a new CFApp resource is created", func() { - var ( - cfAppGUID string - cfApp *korifiv1alpha1.CFApp - serviceBinding *korifiv1alpha1.CFServiceBinding - ) - - BeforeEach(func() { - ctx = context.Background() - cfAppGUID = PrefixedGUID("create-app") - - cfApp = &korifiv1alpha1.CFApp{ - ObjectMeta: metav1.ObjectMeta{ - Name: cfAppGUID, - Namespace: cfSpace.Status.GUID, - Annotations: map[string]string{ - korifiv1alpha1.CFAppRevisionKey: "42", - }, - }, - Spec: korifiv1alpha1.CFAppSpec{ - DisplayName: "test-app", - DesiredState: "STOPPED", - Lifecycle: korifiv1alpha1.Lifecycle{ - Type: "buildpack", - }, - }, - } - - serviceInstanceSecret := &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: PrefixedGUID("service-instance-secret"), - Namespace: cfSpace.Status.GUID, - }, - Data: map[string][]byte{ - korifiv1alpha1.CredentialsSecretKey: []byte(`{"foo": "bar"}`), - }, - } - Expect(adminClient.Create(context.Background(), serviceInstanceSecret)).To(Succeed()) - - serviceInstance := &korifiv1alpha1.CFServiceInstance{ - ObjectMeta: metav1.ObjectMeta{ - Name: PrefixedGUID("app-service-instance"), - Namespace: cfSpace.Status.GUID, - }, - Spec: korifiv1alpha1.CFServiceInstanceSpec{ - Type: "user-provided", - SecretName: serviceInstanceSecret.Name, - }, - } - Expect(adminClient.Create(ctx, serviceInstance)).To(Succeed()) - - serviceBinding = &korifiv1alpha1.CFServiceBinding{ - ObjectMeta: metav1.ObjectMeta{ - Name: PrefixedGUID("app-service-binding"), - Namespace: cfSpace.Status.GUID, - }, - Spec: korifiv1alpha1.CFServiceBindingSpec{ - AppRef: corev1.LocalObjectReference{ - Name: cfAppGUID, - }, - Service: corev1.ObjectReference{ - Namespace: cfSpace.Status.GUID, - Name: serviceInstance.Name, - }, - }, - } - Expect(adminClient.Create(ctx, serviceBinding)).To(Succeed()) - Expect(k8s.Patch(ctx, adminClient, serviceBinding, func() { - serviceBinding.Status.Credentials = corev1.LocalObjectReference{ - Name: serviceInstanceSecret.Name, - } - })).To(Succeed()) - }) - - JustBeforeEach(func() { - Expect(adminClient.Create(ctx, cfApp)).To(Succeed()) - }) - - It("sets the last-stop-app-rev annotation to the value of the app-rev annotation", func() { - Eventually(func(g Gomega) { - g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(cfApp), cfApp)).To(Succeed()) - g.Expect(cfApp.Annotations[korifiv1alpha1.CFAppLastStopRevisionKey]).To(Equal("42")) - }).Should(Succeed()) - }) - - When("status.lastStopAppRev is not empty", func() { - BeforeEach(func() { - cfApp.Annotations[korifiv1alpha1.CFAppLastStopRevisionKey] = "2" - }) - - It("doesn't set it", func() { - Consistently(func(g Gomega) { - g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(cfApp), cfApp)).To(Succeed()) - g.Expect(cfApp.Annotations[korifiv1alpha1.CFAppLastStopRevisionKey]).To(Equal("2")) - }, "1s").Should(Succeed()) - }) - }) - - It("sets status.vcapApplicationSecretName and creates the corresponding secret", func() { - var createdSecret corev1.Secret - - Eventually(func(g Gomega) { - g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(cfApp), cfApp)).To(Succeed()) - - vcapApplicationSecretName := cfApp.Status.VCAPApplicationSecretName - g.Expect(vcapApplicationSecretName).NotTo(BeEmpty()) - - vcapApplicationSecretLookupKey := types.NamespacedName{Name: vcapApplicationSecretName, Namespace: cfSpace.Status.GUID} - g.Expect(adminClient.Get(ctx, vcapApplicationSecretLookupKey, &createdSecret)).To(Succeed()) - }).Should(Succeed()) - - Expect(createdSecret.Data).To(HaveKeyWithValue("VCAP_APPLICATION", ContainSubstring("application_id"))) - Expect(createdSecret.OwnerReferences).To(HaveLen(1)) - Expect(createdSecret.OwnerReferences[0].Name).To(Equal(cfApp.Name)) - }) - - getVCAPServicesSecret := func() *corev1.Secret { - secret := new(corev1.Secret) - Eventually(func(g Gomega) { - g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(cfApp), cfApp)).To(Succeed()) - - vcapServicesSecretName := cfApp.Status.VCAPServicesSecretName - g.Expect(vcapServicesSecretName).NotTo(BeEmpty()) - - vcapServicesSecretLookupKey := types.NamespacedName{Name: vcapServicesSecretName, Namespace: cfSpace.Status.GUID} - g.Expect(adminClient.Get(ctx, vcapServicesSecretLookupKey, secret)).To(Succeed()) - g.Expect(secret.Data).To(HaveKeyWithValue("VCAP_SERVICES", ContainSubstring("user-provided"))) - }).Should(Succeed()) - - return secret - } - - It("sets status.vcapServicesSecretName and creates the corresponding secret", func() { - createdSecret := getVCAPServicesSecret() - Expect(createdSecret.Data).To(HaveKeyWithValue("VCAP_SERVICES", ContainSubstring("user-provided"))) - Expect(createdSecret.OwnerReferences).To(HaveLen(1)) - Expect(createdSecret.OwnerReferences[0].Name).To(Equal(cfApp.Name)) - }) - - It("sets its status conditions", func() { - Eventually(func(g Gomega) { - g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(cfApp), cfApp)).To(Succeed()) - readyStatusCondition := meta.FindStatusCondition(cfApp.Status.Conditions, korifiv1alpha1.StatusConditionReady) - g.Expect(readyStatusCondition).NotTo(BeNil()) - g.Expect(readyStatusCondition.Status).To(Equal(metav1.ConditionFalse)) - g.Expect(readyStatusCondition.Reason).To(Equal("DropletNotAssigned")) - g.Expect(readyStatusCondition.ObservedGeneration).To(Equal(cfApp.Generation)) - }).Should(Succeed()) - }) - - It("sets the ObservedGeneration status field", func() { - Eventually(func(g Gomega) { - g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(cfApp), cfApp)).To(Succeed()) - g.Expect(cfApp.Status.ObservedGeneration).To(Equal(cfApp.Generation)) - }).Should(Succeed()) - }) - - When("the service binding is deleted", func() { - var vcapServicesSecret *corev1.Secret - - JustBeforeEach(func() { - vcapServicesSecret = getVCAPServicesSecret() - Expect(adminClient.Delete(ctx, serviceBinding)).To(Succeed()) - }) - - It("updates the VCAP_SERVICES secret", func() { - Eventually(func(g Gomega) { - g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(vcapServicesSecret), vcapServicesSecret)).To(Succeed()) - g.Expect(vcapServicesSecret.Data).To(HaveKeyWithValue("VCAP_SERVICES", BeEquivalentTo("{}"))) - }).Should(Succeed()) - }) - }) - }) - - When("the app references a non-existing droplet", func() { - var ( - cfAppGUID string - cfApp *korifiv1alpha1.CFApp - ) - - BeforeEach(func() { - cfAppGUID = GenerateGUID() - - cfApp = BuildCFAppCRObject(cfAppGUID, cfSpace.Status.GUID) - cfApp.Spec.CurrentDropletRef.Name = "droplet-that-does-not-exist" - Expect( - adminClient.Create(context.Background(), cfApp), - ).To(Succeed()) - }) - - It("sets the ready condition to false", func() { - Eventually(func(g Gomega) { - g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(cfApp), cfApp)).To(Succeed()) - readyStatusCondition := meta.FindStatusCondition(cfApp.Status.Conditions, korifiv1alpha1.StatusConditionReady) - g.Expect(readyStatusCondition).NotTo(BeNil()) - g.Expect(readyStatusCondition.Status).To(Equal(metav1.ConditionFalse)) - g.Expect(readyStatusCondition.Reason).To(Equal("CannotResolveCurrentDropletRef")) - g.Expect(readyStatusCondition.ObservedGeneration).To(Equal(cfApp.Generation)) - }).Should(Succeed()) - }) - }) - - When("a CFApp resource exists with a valid currentDropletRef set", func() { - const ( - processTypeWeb = "web" - processTypeWebCommand = "bundle exec rackup config.ru -p $PORT -o 0.0.0.0" - processTypeWorker = "worker" - processTypeWorkerCommand = "bundle exec rackup config.ru" - port8080 = 8080 - port9000 = 9000 - ) - - var ( - cfAppGUID string - cfBuildGUID string - cfPackageGUID string - cfApp *korifiv1alpha1.CFApp - cfPackage *korifiv1alpha1.CFPackage - cfBuild *korifiv1alpha1.CFBuild - dropletProcessTypes map[string]string - ) - - BeforeEach(func() { - cfAppGUID = GenerateGUID() - cfPackageGUID = GenerateGUID() - cfBuildGUID = GenerateGUID() - - cfApp = BuildCFAppCRObject(cfAppGUID, cfSpace.Status.GUID) - Expect( - adminClient.Create(context.Background(), cfApp), - ).To(Succeed()) - - cfPackage = BuildCFPackageCRObject(cfPackageGUID, cfSpace.Status.GUID, cfAppGUID, "ref") - Expect( - adminClient.Create(context.Background(), cfPackage), - ).To(Succeed()) - - cfBuild = BuildCFBuildObject(cfBuildGUID, cfSpace.Status.GUID, cfPackageGUID, cfAppGUID) - dropletProcessTypes = map[string]string{ - processTypeWeb: processTypeWebCommand, - processTypeWorker: processTypeWorkerCommand, - } - }) - - JustBeforeEach(func() { - buildDropletStatus := BuildCFBuildDropletStatusObject(dropletProcessTypes) - cfBuild = createBuildWithDroplet(context.Background(), adminClient, cfBuild, buildDropletStatus) - - patchAppWithDroplet(context.Background(), adminClient, cfAppGUID, cfSpace.Status.GUID, cfBuildGUID) - }) - - When("CFProcesses do not exist for the app", func() { - It("eventually creates CFProcess for each process listed on the droplet", func() { - testCtx := context.Background() - droplet := cfBuild.Status.Droplet - processTypes := droplet.ProcessTypes - for _, process := range processTypes { - cfProcessList := korifiv1alpha1.CFProcessList{} - Eventually(func() []korifiv1alpha1.CFProcess { - Expect( - adminClient.List(testCtx, &cfProcessList, &client.ListOptions{ - LabelSelector: labelSelectorForAppAndProcess(cfAppGUID, process.Type), - Namespace: cfApp.Namespace, - }), - ).To(Succeed()) - return cfProcessList.Items - }).Should(HaveLen(1), "expected CFProcess to eventually be created") - createdCFProcess := cfProcessList.Items[0] - Expect(createdCFProcess.Spec.DetectedCommand).To(Equal(process.Command), "cfprocess command does not match with droplet command") - Expect(createdCFProcess.Spec.AppRef.Name).To(Equal(cfAppGUID), "cfprocess app ref does not match app-guid") - Expect(createdCFProcess.ObjectMeta.OwnerReferences).To(ConsistOf([]metav1.OwnerReference{ - { - APIVersion: "korifi.cloudfoundry.org/v1alpha1", - Kind: "CFApp", - Name: cfApp.Name, - UID: cfApp.GetUID(), - Controller: tools.PtrTo(true), - BlockOwnerDeletion: tools.PtrTo(true), - }, - })) - } - }) - }) - - When("CFProcesses exist for the app", func() { - var ( - cfProcessForTypeWebGUID string - cfProcessForTypeWeb *korifiv1alpha1.CFProcess - ) - - BeforeEach(func() { - beforeCtx := context.Background() - cfProcessForTypeWebGUID = GenerateGUID() - cfProcessForTypeWeb = BuildCFProcessCRObject(cfProcessForTypeWebGUID, cfSpace.Status.GUID, cfAppGUID, processTypeWeb, processTypeWebCommand, "") - cfProcessForTypeWeb.Spec.Command = "" - Expect(adminClient.Create(beforeCtx, cfProcessForTypeWeb)).To(Succeed()) - }) - - When("a process from processTypes does not exist", func() { - It("creates it", func() { - Expect(findProcessWithType(cfApp, processTypeWorker)).NotTo(BeNil()) - }) - }) - - It("updates the process detected command", func() { - Eventually(func(g Gomega) { - proc := findProcessWithType(cfApp, processTypeWeb) - g.Expect(proc.Spec.DetectedCommand).To(Equal(processTypeWebCommand)) - }).Should(Succeed()) - }) - - When("the command on the web process is not empty", func() { - BeforeEach(func() { - Expect(k8s.PatchResource(context.Background(), adminClient, cfProcessForTypeWeb, func() { - cfProcessForTypeWeb.Spec.Command = "something else" - })).To(Succeed()) - }) - - It("should save the detectedCommand but not change the command", func() { - Eventually(func(g Gomega) { - proc := findProcessWithType(cfApp, processTypeWeb) - g.Expect(proc.Spec.DetectedCommand).To(Equal(processTypeWebCommand)) - }).Should(Succeed()) - Consistently(func(g Gomega) { - proc := findProcessWithType(cfApp, processTypeWeb) - g.Expect(proc.Spec.Command).To(Equal("something else")) - }).Should(Succeed()) - }) - }) - }) - - When("the build/droplet doesn't include a `web` process", func() { - BeforeEach(func() { - dropletProcessTypes = map[string]string{ - processTypeWorker: processTypeWorkerCommand, - } - }) - - When("no `web` CFProcess exists for the app", func() { - It("adds a `web` process without a start command", func() { - var webProcessesList korifiv1alpha1.CFProcessList - Eventually(func() []korifiv1alpha1.CFProcess { - Expect( - adminClient.List(context.Background(), &webProcessesList, &client.ListOptions{ - LabelSelector: labelSelectorForAppAndProcess(cfAppGUID, processTypeWeb), - Namespace: cfApp.Namespace, - }), - ).To(Succeed()) - return webProcessesList.Items - }).Should(HaveLen(1)) - - webProcess := webProcessesList.Items[0] - Expect(webProcess.Spec.AppRef.Name).To(Equal(cfAppGUID)) - Expect(webProcess.Spec.Command).To(Equal("")) - Expect(webProcess.Spec.DetectedCommand).To(Equal("")) - }) - }) - - When("the `web` CFProcess already exists", func() { - var existingWebProcess *korifiv1alpha1.CFProcess - - BeforeEach(func() { - existingWebProcess = BuildCFProcessCRObject(GenerateGUID(), cfSpace.Status.GUID, cfAppGUID, processTypeWeb, processTypeWebCommand, "") - Expect( - adminClient.Create(context.Background(), existingWebProcess), - ).To(Succeed()) - }) - - It("doesn't alter the existing `web` process", func() { - var webProcessesList korifiv1alpha1.CFProcessList - Consistently(func(g Gomega) { - g.Expect(adminClient.List(context.Background(), &webProcessesList, &client.ListOptions{ - LabelSelector: labelSelectorForAppAndProcess(cfAppGUID, processTypeWeb), - Namespace: cfApp.Namespace, - })).To(Succeed()) - g.Expect(webProcessesList.Items).To(HaveLen(1)) - }).WithTimeout(5 * time.Second).Should(Succeed()) - - Expect(webProcessesList.Items[0].Name).To(Equal(existingWebProcess.Name)) - Expect(webProcessesList.Items[0].Spec).To(Equal(existingWebProcess.Spec)) - }) - }) - }) - - It("sets the ready condition to true", func() { - Eventually(func(g Gomega) { - g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(cfApp), cfApp)).To(Succeed()) - readyStatusCondition := meta.FindStatusCondition(cfApp.Status.Conditions, korifiv1alpha1.StatusConditionReady) - g.Expect(readyStatusCondition).NotTo(BeNil()) - g.Expect(readyStatusCondition.Status).To(Equal(metav1.ConditionTrue)) - }).Should(Succeed()) - }) - - When("the droplet disappears", func() { - JustBeforeEach(func() { - Eventually(func(g Gomega) { - g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(cfApp), cfApp)).To(Succeed()) - g.Expect(cfApp.Status.Conditions).To(ContainElement(SatisfyAll( - HasType(Equal(korifiv1alpha1.StatusConditionReady)), - HasStatus(Equal(metav1.ConditionTrue)), - ))) - }).Should(Succeed()) - Expect(adminClient.Delete(context.Background(), cfBuild)).To(Succeed()) - }) - - It("sets the ready condition to false", func() { - Eventually(func(g Gomega) { - g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(cfApp), cfApp)).To(Succeed()) - readyStatusCondition := meta.FindStatusCondition(cfApp.Status.Conditions, korifiv1alpha1.StatusConditionReady) - g.Expect(readyStatusCondition).NotTo(BeNil()) - g.Expect(readyStatusCondition.Status).To(Equal(metav1.ConditionFalse)) - g.Expect(readyStatusCondition.Reason).To(Equal("CannotResolveCurrentDropletRef")) - g.Expect(readyStatusCondition.ObservedGeneration).To(Equal(cfApp.Generation)) - }).Should(Succeed()) - }) - }) - }) - - When("a CFApp resource is deleted", func() { - var ( - cfAppGUID string - cfRouteGUID string - cfApp *korifiv1alpha1.CFApp - cfRoute *korifiv1alpha1.CFRoute - ) - - BeforeEach(func() { - cfAppGUID = GenerateGUID() - cfApp = BuildCFAppCRObject(cfAppGUID, cfSpace.Status.GUID) - Expect(adminClient.Create(context.Background(), cfApp)).To(Succeed()) - - cfRouteGUID = GenerateGUID() - cfRoute = &korifiv1alpha1.CFRoute{ - ObjectMeta: metav1.ObjectMeta{ - Name: cfRouteGUID, - Namespace: cfSpace.Status.GUID, - }, - Spec: korifiv1alpha1.CFRouteSpec{ - Host: "test-route-host", - Path: "", - Protocol: "http", - DomainRef: corev1.ObjectReference{ - Name: cfDomainGUID, - Namespace: cfSpace.Status.GUID, - }, - Destinations: []korifiv1alpha1.Destination{ - { - GUID: "destination-1-guid", - AppRef: corev1.LocalObjectReference{ - Name: cfAppGUID, - }, - ProcessType: "web", - Protocol: tools.PtrTo("http1"), - }, - }, - }, - } - Expect(adminClient.Create(context.Background(), cfRoute)).To(Succeed()) - }) - - JustBeforeEach(func() { - Expect(adminClient.Delete(context.Background(), cfApp)).To(Succeed()) - }) - - It("eventually deletes the CFApp", func() { - Eventually(func() bool { - var createdCFApp korifiv1alpha1.CFApp - err := adminClient.Get(context.Background(), types.NamespacedName{Name: cfAppGUID, Namespace: cfSpace.Status.GUID}, &createdCFApp) - return apierrors.IsNotFound(err) - }).Should(BeTrue(), "timed out waiting for app to be deleted") - }) - - It("eventually deletes the destination on the CFRoute", func() { - Eventually(func(g Gomega) { - var createdCFRoute korifiv1alpha1.CFRoute - err := adminClient.Get(context.Background(), types.NamespacedName{Name: cfRouteGUID, Namespace: cfSpace.Status.GUID}, &createdCFRoute) - g.Expect(err).NotTo(HaveOccurred()) - g.Expect(createdCFRoute.Spec.Destinations).To(BeEmpty()) - }).Should(Succeed()) - }) - - When("the app is referenced by service bindings", func() { - BeforeEach(func() { - cfServiceBinding := korifiv1alpha1.CFServiceBinding{ - ObjectMeta: metav1.ObjectMeta{ - Name: PrefixedGUID("service-binding"), - Namespace: cfSpace.Status.GUID, - }, - Spec: korifiv1alpha1.CFServiceBindingSpec{ - AppRef: corev1.LocalObjectReference{ - Name: cfAppGUID, - }, - }, - } - Expect(adminClient.Create(context.Background(), &cfServiceBinding)).To(Succeed()) - }) - - It("deletes the referencing service bindings", func() { - Eventually(func(g Gomega) { - sbList := korifiv1alpha1.CFServiceBindingList{} - g.Expect(adminClient.List(context.Background(), &sbList, client.InNamespace(cfSpace.Status.GUID))).To(Succeed()) - g.Expect(sbList.Items).To(BeEmpty()) - }).Should(Succeed()) - }) - }) - }) -}) - -func findProcessWithType(cfApp *korifiv1alpha1.CFApp, processType string) *korifiv1alpha1.CFProcess { - cfProcessList := &korifiv1alpha1.CFProcessList{} - - Eventually(func(g Gomega) { - g.Expect(adminClient.List(ctx, cfProcessList, &client.ListOptions{ - LabelSelector: labelSelectorForAppAndProcess(cfApp.Name, processType), - Namespace: cfApp.Namespace, - })).To(Succeed()) - g.Expect(cfProcessList.Items).To(HaveLen(1)) - }).Should(Succeed()) - - return &cfProcessList.Items[0] -} - -func labelSelectorForAppAndProcess(appGUID, processType string) labels.Selector { - labelSelectorMap := labels.Set{ - CFAppLabelKey: appGUID, - CFProcessTypeLabelKey: processType, - } - selector, selectorValidationErr := labelSelectorMap.AsValidatedSelector() - Expect(selectorValidationErr).NotTo(HaveOccurred()) - return selector -} diff --git a/controllers/controllers/workloads/suite_test.go b/controllers/controllers/workloads/suite_test.go index 640aa76d8..7051d73d3 100644 --- a/controllers/controllers/workloads/suite_test.go +++ b/controllers/controllers/workloads/suite_test.go @@ -10,6 +10,7 @@ import ( "code.cloudfoundry.org/korifi/controllers/config" "code.cloudfoundry.org/korifi/controllers/controllers/shared" . "code.cloudfoundry.org/korifi/controllers/controllers/workloads" + "code.cloudfoundry.org/korifi/controllers/controllers/workloads/apps" buildfake "code.cloudfoundry.org/korifi/controllers/controllers/workloads/build/fake" "code.cloudfoundry.org/korifi/controllers/controllers/workloads/env" "code.cloudfoundry.org/korifi/controllers/controllers/workloads/fake" @@ -27,7 +28,6 @@ import ( "code.cloudfoundry.org/korifi/tests/helpers/oci" "code.cloudfoundry.org/korifi/tools" "code.cloudfoundry.org/korifi/tools/image" - "code.cloudfoundry.org/korifi/tools/k8s" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -125,13 +125,13 @@ var _ = BeforeSuite(func() { eventRecorder = new(controllerfake.EventRecorder) - err = (NewCFAppReconciler( + err = apps.NewReconciler( k8sManager.GetClient(), k8sManager.GetScheme(), ctrl.Log.WithName("controllers").WithName("CFApp"), env.NewVCAPServicesEnvValueBuilder(k8sManager.GetClient()), env.NewVCAPApplicationEnvValueBuilder(k8sManager.GetClient(), nil), - )).SetupWithManager(k8sManager) + ).SetupWithManager(k8sManager) Expect(err).NotTo(HaveOccurred()) cfBuildpackBuildReconciler := NewCFBuildpackBuildReconciler( @@ -271,6 +271,7 @@ func createBuildWithDroplet(ctx context.Context, k8sClient client.Client, cfBuil Expect( k8sClient.Create(ctx, cfBuild), ).To(Succeed()) + patchedCFBuild := cfBuild.DeepCopy() patchedCFBuild.Status.Droplet = droplet Expect( @@ -331,19 +332,6 @@ func createServiceAccount(ctx context.Context, serviceAccountName, namespace str return serviceAccount } -func patchAppWithDroplet(ctx context.Context, k8sClient client.Client, appGUID, spaceGUID, buildGUID string) *korifiv1alpha1.CFApp { - cfApp := &korifiv1alpha1.CFApp{ - ObjectMeta: metav1.ObjectMeta{ - Name: appGUID, - Namespace: spaceGUID, - }, - } - Expect(k8s.PatchResource(ctx, k8sClient, cfApp, func() { - cfApp.Spec.CurrentDropletRef = corev1.LocalObjectReference{Name: buildGUID} - })).To(Succeed()) - return cfApp -} - func createOrg(rootNamespace string) *korifiv1alpha1.CFOrg { org := &korifiv1alpha1.CFOrg{ ObjectMeta: metav1.ObjectMeta{ diff --git a/controllers/main.go b/controllers/main.go index 180158098..9b8135222 100644 --- a/controllers/main.go +++ b/controllers/main.go @@ -32,6 +32,7 @@ import ( "code.cloudfoundry.org/korifi/controllers/controllers/services/instances" "code.cloudfoundry.org/korifi/controllers/controllers/shared" workloadscontrollers "code.cloudfoundry.org/korifi/controllers/controllers/workloads" + "code.cloudfoundry.org/korifi/controllers/controllers/workloads/apps" "code.cloudfoundry.org/korifi/controllers/controllers/workloads/env" "code.cloudfoundry.org/korifi/controllers/controllers/workloads/labels" "code.cloudfoundry.org/korifi/controllers/coordination" @@ -152,13 +153,13 @@ func main() { if os.Getenv("ENABLE_CONTROLLERS") != "false" { imageClient := image.NewClient(k8sClient) - if err = (workloadscontrollers.NewCFAppReconciler( + if err = apps.NewReconciler( mgr.GetClient(), mgr.GetScheme(), ctrl.Log.WithName("controllers").WithName("CFApp"), env.NewVCAPServicesEnvValueBuilder(mgr.GetClient()), env.NewVCAPApplicationEnvValueBuilder(mgr.GetClient(), controllerConfig.ExtraVCAPApplicationValues), - )).SetupWithManager(mgr); err != nil { + ).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "CFApp") os.Exit(1) } @@ -210,7 +211,7 @@ func main() { os.Exit(1) } - if err = (instances.NewCFServiceInstanceReconciler( + if err = (instances.NewReconciler( mgr.GetClient(), mgr.GetScheme(), ctrl.Log.WithName("controllers").WithName("CFServiceInstance"), @@ -219,7 +220,7 @@ func main() { os.Exit(1) } - if err = (bindings.NewCFServiceBindingReconciler( + if err = (bindings.NewReconciler( mgr.GetClient(), mgr.GetScheme(), ctrl.Log.WithName("controllers").WithName("CFServiceBinding"), diff --git a/tests/e2e/apps_test.go b/tests/e2e/apps_test.go index dd0578a47..2f3d918f0 100644 --- a/tests/e2e/apps_test.go +++ b/tests/e2e/apps_test.go @@ -396,7 +396,7 @@ var _ = Describe("Apps", func() { }) }) - Describe("Restart an app", func() { + Describe("started apps", func() { var result map[string]interface{} BeforeEach(func() { @@ -408,37 +408,19 @@ var _ = Describe("Apps", func() { Expect(result).To(HaveKeyWithValue("state", "STARTED")) }) - JustBeforeEach(func() { - var err error - resp, err = adminClient.R().SetResult(&result).Post("/v3/apps/" + appGUID + "/actions/restart") - Expect(err).NotTo(HaveOccurred()) - }) - - It("succeeds", func() { - Expect(resp).To(HaveRestyStatusCode(http.StatusOK)) - Expect(result).To(HaveKeyWithValue("state", "STARTED")) - }) - - It("sets the app rev to 1", func() { - Eventually(func(g Gomega) { - var err error - resp, err = adminClient.R(). - SetResult(&result). - Get("/v3/apps/" + appGUID) - g.Expect(err).NotTo(HaveOccurred()) - g.Expect(resp).To(HaveRestyStatusCode(http.StatusOK)) - g.Expect(result).To(HaveKeyWithValue("metadata", HaveKeyWithValue("annotations", HaveKeyWithValue("korifi.cloudfoundry.org/app-rev", "1")))) - }).Should(Succeed()) - }) - - When("the app is restarted again", func() { + Describe("Restart an app", func() { JustBeforeEach(func() { var err error resp, err = adminClient.R().SetResult(&result).Post("/v3/apps/" + appGUID + "/actions/restart") Expect(err).NotTo(HaveOccurred()) }) - It("sets the app rev to 2", func() { + It("succeeds", func() { + Expect(resp).To(HaveRestyStatusCode(http.StatusOK)) + Expect(result).To(HaveKeyWithValue("state", "STARTED")) + }) + + It("sets the app rev to 1", func() { Eventually(func(g Gomega) { var err error resp, err = adminClient.R(). @@ -446,36 +428,56 @@ var _ = Describe("Apps", func() { Get("/v3/apps/" + appGUID) g.Expect(err).NotTo(HaveOccurred()) g.Expect(resp).To(HaveRestyStatusCode(http.StatusOK)) - g.Expect(result).To(HaveKeyWithValue("metadata", HaveKeyWithValue("annotations", HaveKeyWithValue("korifi.cloudfoundry.org/app-rev", "2")))) + g.Expect(result).To(HaveKeyWithValue("metadata", HaveKeyWithValue("annotations", HaveKeyWithValue("korifi.cloudfoundry.org/app-rev", "1")))) }).Should(Succeed()) }) - }) - When("app environment has been changed", func() { - BeforeEach(func() { - setEnv(appGUID, map[string]interface{}{ - "foo": "var", + When("the app is restarted again", func() { + JustBeforeEach(func() { + var err error + resp, err = adminClient.R().SetResult(&result).Post("/v3/apps/" + appGUID + "/actions/restart") + Expect(err).NotTo(HaveOccurred()) + }) + + It("sets the app rev to 2", func() { + Eventually(func(g Gomega) { + var err error + resp, err = adminClient.R(). + SetResult(&result). + Get("/v3/apps/" + appGUID) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(resp).To(HaveRestyStatusCode(http.StatusOK)) + g.Expect(result).To(HaveKeyWithValue("metadata", HaveKeyWithValue("annotations", HaveKeyWithValue("korifi.cloudfoundry.org/app-rev", "2")))) + }).Should(Succeed()) }) }) - It("sets the new env var to the app environment", func() { - Expect(getAppEnv(appGUID)).To(HaveKeyWithValue("environment_variables", HaveKeyWithValue("foo", "var"))) + When("app environment has been changed", func() { + BeforeEach(func() { + setEnv(appGUID, map[string]interface{}{ + "foo": "var", + }) + }) + + It("sets the new env var to the app environment", func() { + Expect(getAppEnv(appGUID)).To(HaveKeyWithValue("environment_variables", HaveKeyWithValue("foo", "var"))) + }) }) }) - }) - Describe("Stop an app", func() { - var result appResource + Describe("Stop an app", func() { + var result appResource - JustBeforeEach(func() { - var err error - resp, err = adminClient.R().SetResult(&result).Post("/v3/apps/" + appGUID + "/actions/stop") - Expect(err).NotTo(HaveOccurred()) - }) + JustBeforeEach(func() { + var err error + resp, err = adminClient.R().SetResult(&result).Post("/v3/apps/" + appGUID + "/actions/stop") + Expect(err).NotTo(HaveOccurred()) + }) - It("succeeds", func() { - Expect(resp).To(HaveRestyStatusCode(http.StatusOK)) - Expect(result.State).To(Equal("STOPPED")) + It("succeeds", func() { + Expect(resp).To(HaveRestyStatusCode(http.StatusOK)) + Expect(result.State).To(Equal("STOPPED")) + }) }) }) })