diff --git a/api/v1/runtime_types.go b/api/v1/runtime_types.go index 8b48c95b..b4aa6f67 100644 --- a/api/v1/runtime_types.go +++ b/api/v1/runtime_types.go @@ -278,6 +278,13 @@ func (k *Runtime) IsConditionSetWithStatus(c RuntimeConditionType, r RuntimeCond return false } +func (k *Runtime) IsConditionConfiguredSuccess() bool { + return k.IsConditionSetWithStatus( + ConditionTypeRuntimeConfigured, + ConditionReasonShootCreationCompleted, + metav1.ConditionTrue) +} + func (k *Runtime) ValidateRequiredLabels() error { var requiredLabelKeys = []string{ LabelKymaInstanceID, diff --git a/internal/controller/runtime/fsm/runtime_fsm_apply_clusterrolebindings.go b/internal/controller/runtime/fsm/runtime_fsm_apply_clusterrolebindings.go index 6c5923ab..1eda4e64 100644 --- a/internal/controller/runtime/fsm/runtime_fsm_apply_clusterrolebindings.go +++ b/internal/controller/runtime/fsm/runtime_fsm_apply_clusterrolebindings.go @@ -22,6 +22,42 @@ var ( } ) +func sFnApplyClusterRoleBindings(ctx context.Context, m *fsm, s *systemState) (stateFn, *ctrl.Result, error) { + // prepare subresource client to request admin kubeconfig + srscClient := m.ShootClient.SubResource("adminkubeconfig") + shootAdminClient, err := GetShootClient(ctx, srscClient, s.shoot) + if err != nil { + updateCRBApplyFailed(&s.instance) + return updateStatusAndStopWithError(err) + } + // list existing cluster role bindings + var crbList rbacv1.ClusterRoleBindingList + if err := shootAdminClient.List(ctx, &crbList); err != nil { + updateCRBApplyFailed(&s.instance) + return updateStatusAndStopWithError(err) + } + + removed := getRemoved(crbList.Items, s.instance.Spec.Security.Administrators) + missing := getMissing(crbList.Items, s.instance.Spec.Security.Administrators) + + for _, fn := range []func() error{ + newDelCRBs(ctx, shootAdminClient, removed), + newAddCRBs(ctx, shootAdminClient, missing), + } { + if err := fn(); err != nil { + updateCRBApplyFailed(&s.instance) + return updateStatusAndStopWithError(err) + } + } + + s.instance.UpdateStateReady( + imv1.ConditionTypeRuntimeConfigured, + imv1.ConditionReasonConfigurationCompleted, + "kubeconfig admin access updated", + ) + return updateStatusAndStop() +} + //nolint:gochecknoglobals var GetShootClient = func(ctx context.Context, adminKubeconfigClient client.SubResourceClient, shoot *gardener_api.Shoot) (client.Client, error) { @@ -150,39 +186,3 @@ func updateCRBApplyFailed(rt *imv1.Runtime) { "failed to update kubeconfig admin access", ) } - -func sFnApplyClusterRoleBindings(ctx context.Context, m *fsm, s *systemState) (stateFn, *ctrl.Result, error) { - // prepare subresource client to request admin kubeconfig - srscClient := m.ShootClient.SubResource("adminkubeconfig") - shootAdminClient, err := GetShootClient(ctx, srscClient, s.shoot) - if err != nil { - updateCRBApplyFailed(&s.instance) - return updateStatusAndStopWithError(err) - } - // list existing cluster role bindings - var crbList rbacv1.ClusterRoleBindingList - if err := shootAdminClient.List(ctx, &crbList); err != nil { - updateCRBApplyFailed(&s.instance) - return updateStatusAndStopWithError(err) - } - - removed := getRemoved(crbList.Items, s.instance.Spec.Security.Administrators) - missing := getMissing(crbList.Items, s.instance.Spec.Security.Administrators) - - // FIXME add status check - if len(removed) == 0 && len(missing) == 0 { - stop() - } - - for _, fn := range []func() error{ - newDelCRBs(ctx, shootAdminClient, removed), - newAddCRBs(ctx, shootAdminClient, missing), - } { - if err := fn(); err != nil { - updateCRBApplyFailed(&s.instance) - return updateStatusAndStopWithError(err) - } - } - - return updateStatusAndRequeue() -} diff --git a/internal/controller/runtime/fsm/runtime_fsm_apply_crb_test.go b/internal/controller/runtime/fsm/runtime_fsm_apply_crb_test.go new file mode 100644 index 00000000..c42bbdab --- /dev/null +++ b/internal/controller/runtime/fsm/runtime_fsm_apply_crb_test.go @@ -0,0 +1,236 @@ +package fsm + +import ( + "context" + "fmt" + "time" + + gardener_api "github.com/gardener/gardener/pkg/apis/core/v1beta1" + imv1 "github.com/kyma-project/infrastructure-manager/api/v1" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +var _ = Describe(`runtime_fsm_apply_crb`, Label("applyCRB"), func() { + + var testErr = fmt.Errorf("test error") + + DescribeTable("isRBACUserKind", + func(s rbacv1.Subject, expected bool) { + actual := isRBACUserKind(s) + Expect(actual).To(Equal(expected)) + }, + Entry("shoud detect if subject is not user kind", rbacv1.Subject{}, false), + Entry("shoud detect if subject is from invalid group", + rbacv1.Subject{ + Kind: rbacv1.UserKind, + }, false), + Entry("shoud detect if subject user from valid group", + rbacv1.Subject{ + APIGroup: rbacv1.GroupName, + Kind: rbacv1.UserKind, + }, true), + ) + + DescribeTable("getMissing", + func(tc tcGetCRB) { + actual := getMissing(tc.crbs, tc.admins) + Expect(actual).To(BeComparableTo(tc.expected)) + }, + Entry("should return a list with CRBs to be created", tcGetCRB{ + admins: []string{"test1", "test2"}, + crbs: nil, + expected: []rbacv1.ClusterRoleBinding{ + toAdminClusterRoleBinding("test1"), + toAdminClusterRoleBinding("test2"), + }, + }), + Entry("should return nil list if no admins missing", tcGetCRB{ + admins: []string{"test1"}, + crbs: []rbacv1.ClusterRoleBinding{ + toAdminClusterRoleBinding("test1"), + }, + expected: nil, + }), + ) + + DescribeTable("getRemoved", + func(tc tcGetCRB) { + actual := getRemoved(tc.crbs, tc.admins) + Expect(actual).To(BeComparableTo(tc.expected)) + }, + Entry("should return nil list if CRB list is nil", tcGetCRB{ + admins: []string{"test1"}, + crbs: nil, + expected: nil, + }), + Entry("should return nil list if CRB list is empty", tcGetCRB{ + admins: []string{"test1"}, + crbs: []rbacv1.ClusterRoleBinding{}, + expected: nil, + }), + Entry("should return nil list if no admins to remove", tcGetCRB{ + admins: []string{"test1"}, + crbs: []rbacv1.ClusterRoleBinding{toAdminClusterRoleBinding("test1")}, + expected: nil, + }), + Entry("should return list if with CRBs to remove", tcGetCRB{ + admins: []string{"test2"}, + crbs: []rbacv1.ClusterRoleBinding{ + toAdminClusterRoleBinding("test1"), + toAdminClusterRoleBinding("test2"), + toAdminClusterRoleBinding("test3"), + }, + expected: []rbacv1.ClusterRoleBinding{ + toAdminClusterRoleBinding("test1"), + toAdminClusterRoleBinding("test3"), + }, + }), + ) + + testRuntime := imv1.Runtime{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testme1", + Namespace: "default", + }, + } + + testRuntimeWithAdmin := imv1.Runtime{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testme1", + Namespace: "default", + }, + Spec: imv1.RuntimeSpec{ + Security: imv1.Security{ + Administrators: []string{ + "test-admin1", + }, + }, + }, + } + + testScheme, err := newTestScheme() + Expect(err).ShouldNot(HaveOccurred()) + + defaultSetup := func(f *fsm) error { + GetShootClient = func( + _ context.Context, + _ client.SubResourceClient, + _ *gardener_api.Shoot) (client.Client, error) { + return f.Client, nil + } + return nil + } + + DescribeTable("sFnAppluClusterRoleBindings", + func(tc tcApplySfn) { + // initialize test data if required + Expect(tc.init()).ShouldNot(HaveOccurred()) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10000) + defer cancel() + + actualResult, actualErr := tc.fsm.Run(ctx, tc.instance) + Expect(actualResult).Should(BeComparableTo(tc.expected.result)) + + matchErr := BeNil() + if tc.expected.err != nil { + matchErr = MatchError(tc.expected.err) + } + Expect(actualErr).Should(matchErr) + }, + + Entry("add admin", tcApplySfn{ + instance: testRuntimeWithAdmin, + expected: tcSfnExpected{ + err: nil, + }, + fsm: must( + newFakeFSM, + withFakedK8sClient(testScheme, &testRuntimeWithAdmin), + withFn(sFnApplyClusterRoleBindings), + withFakeEventRecorder(1), + ), + setup: defaultSetup, + }), + + Entry("nothing change", tcApplySfn{ + instance: testRuntime, + expected: tcSfnExpected{ + err: nil, + }, + fsm: must( + newFakeFSM, + withFakedK8sClient(testScheme, &testRuntime), + withFn(sFnApplyClusterRoleBindings), + withFakeEventRecorder(1), + ), + setup: defaultSetup, + }), + + Entry("error getting client", tcApplySfn{ + expected: tcSfnExpected{ + err: testErr, + }, + fsm: must( + newFakeFSM, + withFakedK8sClient(testScheme, &testRuntime), + withFn(sFnApplyClusterRoleBindings), + withFakeEventRecorder(1), + ), + setup: defaultSetup, + }), + ) +}) + +type tcGetCRB struct { + crbs []rbacv1.ClusterRoleBinding + admins []string + expected []rbacv1.ClusterRoleBinding +} + +type tcSfnExpected struct { + result ctrl.Result + err error +} + +type tcApplySfn struct { + expected tcSfnExpected + setup func(m *fsm) error + fsm *fsm + instance imv1.Runtime +} + +func (c *tcApplySfn) init() error { + if c.setup != nil { + return c.setup(c.fsm) + } + return nil +} + +func toCRBs(admins []string) (result []rbacv1.ClusterRoleBinding) { + for _, crb := range admins { + result = append(result, toAdminClusterRoleBinding(crb)) + } + return result +} + +func newTestScheme() (*runtime.Scheme, error) { + schema := runtime.NewScheme() + + for _, fn := range []func(*runtime.Scheme) error{ + imv1.AddToScheme, + rbacv1.AddToScheme, + } { + if err := fn(schema); err != nil { + return nil, err + } + } + return schema, nil +} diff --git a/internal/controller/runtime/fsm/runtime_fsm_create_kubeconfig.go b/internal/controller/runtime/fsm/runtime_fsm_create_kubeconfig.go index 97ec0e79..c7fea093 100644 --- a/internal/controller/runtime/fsm/runtime_fsm_create_kubeconfig.go +++ b/internal/controller/runtime/fsm/runtime_fsm_create_kubeconfig.go @@ -26,7 +26,12 @@ func sFnCreateKubeconfig(ctx context.Context, m *fsm, s *systemState) (stateFn, if err != nil { if !k8serrors.IsNotFound(err) { m.log.Error(err, "GardenerCluster CR read error", "name", runtimeID) - s.instance.UpdateStatePending(imv1.ConditionTypeRuntimeKubeconfigReady, imv1.ConditionReasonKubernetesAPIErr, "False", err.Error()) + s.instance.UpdateStatePending( + imv1.ConditionTypeRuntimeKubeconfigReady, + imv1.ConditionReasonKubernetesAPIErr, + "False", + err.Error(), + ) return updateStatusAndStop() } @@ -34,7 +39,12 @@ func sFnCreateKubeconfig(ctx context.Context, m *fsm, s *systemState) (stateFn, err = m.Create(ctx, makeGardenerClusterForRuntime(s.instance, s.shoot)) if err != nil { m.log.Error(err, "GardenerCluster CR create error", "name", runtimeID) - s.instance.UpdateStatePending(imv1.ConditionTypeRuntimeKubeconfigReady, imv1.ConditionReasonKubernetesAPIErr, "False", err.Error()) + s.instance.UpdateStatePending( + imv1.ConditionTypeRuntimeKubeconfigReady, + imv1.ConditionReasonKubernetesAPIErr, + "False", + err.Error(), + ) return updateStatusAndStop() } diff --git a/internal/controller/runtime/fsm/runtime_fsm_create_shoot.go b/internal/controller/runtime/fsm/runtime_fsm_create_shoot.go index 8256b893..7f6ea9b6 100644 --- a/internal/controller/runtime/fsm/runtime_fsm_create_shoot.go +++ b/internal/controller/runtime/fsm/runtime_fsm_create_shoot.go @@ -13,7 +13,11 @@ func sFnCreateShoot(ctx context.Context, m *fsm, s *systemState) (stateFn, *ctrl newShoot, err := convertShoot(&s.instance, m.ConverterConfig) if err != nil { m.log.Error(err, "Failed to convert Runtime instance to shoot object") - return updateStatePendingWithErrorAndStop(&s.instance, imv1.ConditionTypeRuntimeProvisioned, imv1.ConditionReasonConversionError, "Runtime conversion error") + return updateStatePendingWithErrorAndStop( + &s.instance, + imv1.ConditionTypeRuntimeProvisioned, + imv1.ConditionReasonConversionError, + "Runtime conversion error") } err = m.ShootClient.Create(ctx, &newShoot) @@ -29,7 +33,12 @@ func sFnCreateShoot(ctx context.Context, m *fsm, s *systemState) (stateFn, *ctrl ) return updateStatusAndRequeueAfter(gardenerRequeueDuration) } - m.log.Info("Gardener shoot for runtime initialised successfully", "Name", newShoot.Name, "Namespace", newShoot.Namespace) + + m.log.Info( + "Gardener shoot for runtime initialised successfully", + "Name", newShoot.Name, + "Namespace", newShoot.Namespace, + ) s.instance.UpdateStatePending( imv1.ConditionTypeRuntimeProvisioned, diff --git a/internal/controller/runtime/fsm/runtime_fsm_waiting_for_shoot_reconcile.go b/internal/controller/runtime/fsm/runtime_fsm_waiting_for_shoot_reconcile.go index f8c65e8a..b70e09a2 100644 --- a/internal/controller/runtime/fsm/runtime_fsm_waiting_for_shoot_reconcile.go +++ b/internal/controller/runtime/fsm/runtime_fsm_waiting_for_shoot_reconcile.go @@ -38,13 +38,18 @@ func sFnWaitForShootReconcile(_ context.Context, m *fsm, s *systemState) (stateF imv1.ConditionTypeRuntimeProvisioned, imv1.ConditionReasonProcessingErr, "False", - string(reason)) - + string(reason), + ) return updateStatusAndStop() case gardener.LastOperationStateSucceeded: m.log.Info(fmt.Sprintf("Shoot %s successfully updated, moving to processing", s.shoot.Name)) - return ensureStatusConditionIsSetAndContinue(&s.instance, imv1.ConditionTypeRuntimeProvisioned, imv1.ConditionReasonProcessing, "Shoot update is completed", sFnApplyClusterRoleBindings) + return ensureStatusConditionIsSetAndContinue( + &s.instance, + imv1.ConditionTypeRuntimeProvisioned, + imv1.ConditionReasonConfigurationCompleted, + "Runtime processing completed successfully", + sFnApplyClusterRoleBindings) } m.log.Info("Update did not processed, exiting with no retry") diff --git a/internal/controller/runtime/fsm/runtime_fsm_waiting_shoot_creation.go b/internal/controller/runtime/fsm/runtime_fsm_waiting_shoot_creation.go index f006e8bd..16b16597 100644 --- a/internal/controller/runtime/fsm/runtime_fsm_waiting_shoot_creation.go +++ b/internal/controller/runtime/fsm/runtime_fsm_waiting_shoot_creation.go @@ -57,7 +57,12 @@ func sFnWaitForShootCreation(_ context.Context, m *fsm, s *systemState) (stateFn case gardener.LastOperationStateSucceeded: m.log.Info(fmt.Sprintf("Shoot %s successfully created", s.shoot.Name)) - return ensureStatusConditionIsSetAndContinue(&s.instance, imv1.ConditionTypeRuntimeProvisioned, imv1.ConditionReasonShootCreationCompleted, "Shoot creation completed", sFnCreateKubeconfig) + return ensureStatusConditionIsSetAndContinue( + &s.instance, + imv1.ConditionTypeRuntimeProvisioned, + imv1.ConditionReasonShootCreationCompleted, + "Shoot creation completed", + sFnCreateKubeconfig) default: m.log.Info("Unknown shoot operation state, exiting with no retry") diff --git a/internal/controller/runtime/fsm/utilz_for_test.go b/internal/controller/runtime/fsm/utilz_for_test.go index 257f6378..172d3885 100644 --- a/internal/controller/runtime/fsm/utilz_for_test.go +++ b/internal/controller/runtime/fsm/utilz_for_test.go @@ -5,6 +5,7 @@ import ( . "github.com/onsi/gomega" //nolint:revive "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/tools/record" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" ) @@ -42,22 +43,29 @@ var ( k8sClient := fake.NewClientBuilder(). WithScheme(scheme). WithObjects(objs...). + WithStatusSubresource(objs...). Build() return func(fsm *fsm) error { fsm.Client = k8sClient + fsm.ShootClient = k8sClient return nil } } - /* linter fix for unused code - withMockedShootClient = func(c *gardener_mocks.ShootClient) fakeFSMOpt { + withFn = func(fn stateFn) fakeFSMOpt { return func(fsm *fsm) error { - fsm.ShootClient = c + fsm.fn = fn + return nil + } + } + + withFakeEventRecorder = func(buffer int) fakeFSMOpt { + return func(fsm *fsm) error { + fsm.EventRecorder = record.NewFakeRecorder(buffer) return nil } } - */ ) func newFakeFSM(opts ...fakeFSMOpt) (*fsm, error) { diff --git a/internal/controller/runtime/runtime_controller_test.go b/internal/controller/runtime/runtime_controller_test.go index ca97cc82..699539d2 100644 --- a/internal/controller/runtime/runtime_controller_test.go +++ b/internal/controller/runtime/runtime_controller_test.go @@ -113,7 +113,7 @@ var _ = Describe("Runtime Controller", func() { return false } - if !runtime.IsConditionSet(imv1.ConditionTypeRuntimeProvisioned, imv1.ConditionReasonConfigurationCompleted) { + if !runtime.IsConditionSet(imv1.ConditionTypeRuntimeConfigured, imv1.ConditionReasonConfigurationCompleted) { return false }