diff --git a/.github/workflows/build_kim.yaml b/.github/workflows/build_kim.yaml new file mode 100644 index 00000000..86dddc5d --- /dev/null +++ b/.github/workflows/build_kim.yaml @@ -0,0 +1,133 @@ +name: KIM + +on: + push: + branches: + - main + tags: + - "[0-9]+.[0-9]+.[0-9]+" + - "[0-9]+.[0-9]+.[0-9]+-*" + paths-ignore: + - .reuse + - hack/ + - LICENSES/ + - LICENSE + - .gitignore + - "**.md" + + pull_request_target: + types: [opened, synchronize, reopened] + paths-ignore: + - .reuse + - hack/ + - LICENSES/ + - LICENSE + - .gitignore + - "**.md" + +env: + trivy-table: trivy-table.txt + +permissions: + id-token: write # This is required for requesting the JWT token + contents: read # This is required for actions/checkout + +jobs: + setup: + permissions: + contents: read + runs-on: ubuntu-latest + outputs: + tag: ${{ steps.tag.outputs.tag }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.ref }} + repository: ${{ github.event.pull_request.head.repo.full_name }} + - id: tag + if: github.event_name == 'push' && github.ref_type == 'tag' + run: echo "tag=${{ github.ref_name }}" >> $GITHUB_OUTPUT + + trivy: + permissions: + contents: read + runs-on: "ubuntu-20.04" + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.ref }} + repository: ${{ github.event.pull_request.head.repo.full_name }} + + - name: Install trivy + run: | + mkdir ./trivy + curl -L https://github.com/aquasecurity/trivy/releases/download/v0.49.1/trivy_0.49.1_Linux-64bit.tar.gz | tar xvz --directory=./trivy + ./trivy/trivy --version + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@0.24.0 + with: + scan-type: "fs" + scan-ref: "." + + exit-code: 1 + severity: "UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL" + ignore-unfixed: false + timeout: "5m0s" + vuln-type: "os,library" + + format: table + output: ${{ env.trivy-table }} + + - name: Upload trivy table + if: success() || failure() + uses: actions/upload-artifact@v4 + with: + name: ${{ env.trivy-table }} + path: ${{ env.trivy-table }} + + - name: Print trivy table + if: success() || failure() + run: cat ${{ env.trivy-table }} + + build-image: + needs: setup + uses: kyma-project/test-infra/.github/workflows/image-builder.yml@main # Usage: kyma-project/test-infra/.github/workflows/image-builder.yml@main + with: + name: infrastructure-manager + dockerfile: Dockerfile + context: . + tags: ${{ needs.setup.outputs.tag }} + + summary: + runs-on: ubuntu-latest + needs: [build-image, trivy] + if: success() || failure() + steps: + - name: "Download trivy log" + uses: actions/download-artifact@v4 + continue-on-error: true + with: + name: ${{ env.trivy-table }} + - name: "Generate summary" + run: | + { + echo '# Kyma Infrastructure Manager' + # if trivy results table exists + if [ -f ${{ env.trivy-table }} ]; then + echo '## Trivy' + printf '\n```txt\n' + cat ${{ env.trivy-table }} + printf '\n```\n' + fi + + # if build-image was successful + if [ "${{ needs.build-image.result }}" == "success" ]; then + printf '\n\n## Image\n' + printf '\n```json\n' + echo '${{ needs.build-image.outputs.images }}' | jq + printf '\n```\n' + fi + } >> $GITHUB_STEP_SUMMARY diff --git a/Dockerfile b/Dockerfile index 6d06199e..8aaf66fc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build the manager binary -FROM golang:1.22.5 as builder +FROM golang:1.23.0-alpine3.20 as builder ARG TARGETOS ARG TARGETARCH diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index 0a02fcf1..011a828e 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -79,6 +79,7 @@ spec: capabilities: drop: - "ALL" + readOnlyRootFilesystem: true ports: - containerPort: 8080 name: metrics diff --git a/internal/auditlogging/auditlogging.go b/internal/auditlogging/auditlogging.go index 20c2dd28..5aaf150a 100644 --- a/internal/auditlogging/auditlogging.go +++ b/internal/auditlogging/auditlogging.go @@ -125,7 +125,7 @@ func (al *AuditLog) Enable(ctx context.Context, shoot *gardener.Shoot) (bool, er } } - return true, nil + return annotated, nil } func ApplyAuditLogConfig(shoot *gardener.Shoot, auditConfigFromFile map[string]map[string]AuditLogData, providerType string) (bool, error) { diff --git a/internal/controller/runtime/fsm/runtime_fsm_apply_crb_test.go b/internal/controller/runtime/fsm/runtime_fsm_apply_crb_test.go index 603dabf8..d20378ad 100644 --- a/internal/controller/runtime/fsm/runtime_fsm_apply_crb_test.go +++ b/internal/controller/runtime/fsm/runtime_fsm_apply_crb_test.go @@ -1,229 +1,226 @@ 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" -// rbacv1 "k8s.io/api/rbac/v1" -// metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -// "k8s.io/apimachinery/pkg/runtime" -// ctrl "sigs.k8s.io/controller-runtime" -// "sigs.k8s.io/controller-runtime/pkg/client" -//) -// -//var _ = Describe(`runtime_fsm_apply_crb`, Label("applyCRB"), func() { -// -// var testErr = fmt.Errorf("test error") -// -// 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) -// 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{ -// instance: testRuntime, -// expected: tcSfnExpected{ -// err: testErr, -// }, -// fsm: must( -// newFakeFSM, -// withFakedK8sClient(testScheme, &testRuntime), -// withFn(sFnApplyClusterRoleBindings), -// withFakeEventRecorder(1), -// ), -// setup: func(f *fsm) error { -// GetShootClient = func( -// _ context.Context, -// _ client.SubResourceClient, -// _ *gardener_api.Shoot) (client.Client, error) { -// return nil, testErr -// } -// return nil -// -// }, -// }), -// ) -//}) -// -//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 -//} +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" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var _ = Describe(`runtime_fsm_apply_crb`, Label("applyCRB"), func() { + + var testErr = fmt.Errorf("test error") + + 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) + 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, + result: ctrl.Result{RequeueAfter: time.Second * 15}, + }, + fsm: must( + newFakeFSM, + withAuditLogging(true, nil), + withFakedK8sClient(testScheme, &testRuntimeWithAdmin), + withFn(sFnApplyClusterRoleBindingsStateSetup), + withFakeEventRecorder(1), + ), + setup: defaultSetup, + }), + + Entry("nothing change", tcApplySfn{ + instance: testRuntime, + expected: tcSfnExpected{ + err: nil, + result: ctrl.Result{RequeueAfter: time.Second * 15}, + }, + fsm: must( + newFakeFSM, + withAuditLogging(true, nil), + withFakedK8sClient(testScheme, &testRuntime), + withFn(sFnApplyClusterRoleBindingsStateSetup), + withFakeEventRecorder(1), + ), + setup: defaultSetup, + }), + + Entry("error getting client", tcApplySfn{ + instance: testRuntime, + expected: tcSfnExpected{ + err: testErr, + }, + fsm: must( + newFakeFSM, + withAuditLogging(true, nil), + withFakedK8sClient(testScheme, &testRuntime), + withFn(sFnApplyClusterRoleBindingsStateSetup), + withFakeEventRecorder(1), + ), + setup: func(f *fsm) error { + GetShootClient = func( + _ context.Context, + _ client.SubResourceClient, + _ *gardener_api.Shoot) (client.Client, error) { + return nil, testErr + } + return nil + + }, + }), + ) +}) + +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 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_configure_auditlog.go b/internal/controller/runtime/fsm/runtime_fsm_configure_auditlog.go index b1ea7449..d63c57e9 100644 --- a/internal/controller/runtime/fsm/runtime_fsm_configure_auditlog.go +++ b/internal/controller/runtime/fsm/runtime_fsm_configure_auditlog.go @@ -14,21 +14,31 @@ func sFnConfigureAuditLog(ctx context.Context, m *fsm, s *systemState) (stateFn, if wasAuditLogEnabled { m.log.Info("Audit Log configured for shoot: " + s.shoot.Name) - s.instance.UpdateStateReady( + s.instance.UpdateStatePending( imv1.ConditionTypeAuditLogConfigured, imv1.ConditionReasonAuditLogConfigured, - "Audit Log configured", + "Unknown", + "Waiting for Gardener shoot to be Ready state after configuration of the Audit Logs", ) - return updateStatusAndStop() + return updateStatusAndRequeueAfter(gardenerRequeueDuration) + } + + if err != nil { + m.log.Error(err, "Failed to configure Audit Log") + s.instance.UpdateStatePending( + imv1.ConditionTypeAuditLogConfigured, + imv1.ConditionReasonAuditLogError, + "False", + err.Error(), + ) + } else { + s.instance.UpdateStateReady( + imv1.ConditionTypeAuditLogConfigured, + imv1.ConditionReasonAuditLogConfigured, + "Audit Log configured successfully", + ) } - m.log.Error(err, "Failed to configure Audit Log") - s.instance.UpdateStatePending( - imv1.ConditionTypeAuditLogConfigured, - imv1.ConditionReasonAuditLogError, - "False", - err.Error(), - ) - return updateStatusAndRequeueAfter(gardenerRequeueDuration) + return updateStatusAndStop() } diff --git a/internal/controller/runtime/fsm/runtime_fsm_configure_auditlog_test.go b/internal/controller/runtime/fsm/runtime_fsm_configure_auditlog_test.go index bd0dce9f..eab363b7 100644 --- a/internal/controller/runtime/fsm/runtime_fsm_configure_auditlog_test.go +++ b/internal/controller/runtime/fsm/runtime_fsm_configure_auditlog_test.go @@ -14,7 +14,7 @@ import ( ) func TestAuditLogState(t *testing.T) { - t.Run("Should set status on Runtime CR when Audit Log was successfully configured", func(t *testing.T) { + t.Run("Should set status on Runtime CR when Audit Log configuration was changed and Shoot enters into reconciliation", func(t *testing.T) { // given ctx := context.Background() auditLog := &mocks.AuditLogging{} @@ -27,9 +27,9 @@ func TestAuditLogState(t *testing.T) { expectedRuntimeConditions := []metav1.Condition{ { Type: string(v1.ConditionTypeAuditLogConfigured), - Status: "True", + Status: "Unknown", Reason: string(v1.ConditionReasonAuditLogConfigured), - Message: "Audit Log configured", + Message: "Waiting for Gardener shoot to be Ready state after configuration of the Audit Logs", }, } @@ -42,6 +42,41 @@ func TestAuditLogState(t *testing.T) { // set the time to its zero value for comparison purposes systemState.instance.Status.Conditions[0].LastTransitionTime = metav1.Time{} + // then + auditLog.AssertExpectations(t) + require.Contains(t, stateFn.name(), "sFnUpdateStatus") + assert.Equal(t, v1.RuntimeStatePending, string(systemState.instance.Status.State)) + assert.Equal(t, expectedRuntimeConditions, systemState.instance.Status.Conditions) + }) + + t.Run("Should set status on Runtime CR when Shoot is in Succeeded state after configuring Audit Log", func(t *testing.T) { + // given + ctx := context.Background() + auditLog := &mocks.AuditLogging{} + shoot := shootForTest() + instance := runtimeForTest() + systemState := &systemState{ + instance: instance, + shoot: shoot, + } + expectedRuntimeConditions := []metav1.Condition{ + { + Type: string(v1.ConditionTypeAuditLogConfigured), + Status: "True", + Reason: string(v1.ConditionReasonAuditLogConfigured), + Message: "Audit Log configured successfully", + }, + } + + auditLog.On("Enable", ctx, shoot).Return(false, nil).Once() + + // when + fsm := &fsm{AuditLogging: auditLog} + stateFn, _, _ := sFnConfigureAuditLog(ctx, fsm, systemState) + + // set the time to its zero value for comparison purposes + systemState.instance.Status.Conditions[0].LastTransitionTime = metav1.Time{} + // then auditLog.AssertExpectations(t) require.Contains(t, stateFn.name(), "sFnUpdateStatus") @@ -49,7 +84,7 @@ func TestAuditLogState(t *testing.T) { assert.Equal(t, expectedRuntimeConditions, systemState.instance.Status.Conditions) }) - t.Run("Should requeue in case of error during configuration and set status on Runtime CR", func(t *testing.T) { + t.Run("Should stop in case of error during configuration and set status on Runtime CR", func(t *testing.T) { // given ctx := context.Background() auditLog := &mocks.AuditLogging{} 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 b70e09a2..ce700502 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 @@ -47,9 +47,9 @@ func sFnWaitForShootReconcile(_ context.Context, m *fsm, s *systemState) (stateF return ensureStatusConditionIsSetAndContinue( &s.instance, imv1.ConditionTypeRuntimeProvisioned, - imv1.ConditionReasonConfigurationCompleted, + imv1.ConditionReasonAuditLogConfigured, "Runtime processing completed successfully", - sFnApplyClusterRoleBindings) + sFnConfigureAuditLog) } 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 ef654676..f5c4b3de 100644 --- a/internal/controller/runtime/fsm/runtime_fsm_waiting_shoot_creation.go +++ b/internal/controller/runtime/fsm/runtime_fsm_waiting_shoot_creation.go @@ -31,7 +31,7 @@ func sFnWaitForShootCreation(_ context.Context, m *fsm, s *systemState) (stateFn m.log.Info("Waiting for shoot creation state") switch s.shoot.Status.LastOperation.State { - case gardener.LastOperationStateProcessing, gardener.LastOperationStatePending, gardener.LastOperationStateAborted: + case gardener.LastOperationStateProcessing, gardener.LastOperationStatePending, gardener.LastOperationStateAborted, gardener.LastOperationStateError: m.log.Info(fmt.Sprintf("Shoot %s is in %s state, scheduling for retry", s.shoot.Name, s.shoot.Status.LastOperation.State)) s.instance.UpdateStatePending( diff --git a/internal/controller/runtime/fsm/utilz_for_test.go b/internal/controller/runtime/fsm/utilz_for_test.go index 3699df6a..9e92a2ef 100644 --- a/internal/controller/runtime/fsm/utilz_for_test.go +++ b/internal/controller/runtime/fsm/utilz_for_test.go @@ -1,14 +1,21 @@ package fsm import ( + "context" "fmt" + gardener "github.com/gardener/gardener/pkg/apis/core/v1beta1" + gardener_api "github.com/gardener/gardener/pkg/apis/core/v1beta1" "github.com/kyma-project/infrastructure-manager/internal/gardener/shoot" - . "github.com/onsi/gomega" //nolint:revive + . "github.com/onsi/ginkgo/v2" //nolint:revive + . "github.com/onsi/gomega" //nolint:revive + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/log/zap" ) type fakeFSMOpt func(*fsm) error @@ -74,10 +81,22 @@ var ( return nil } } + + withAuditLogging = func(isEnabled bool, err error) fakeFSMOpt { + return func(fsm *fsm) error { + fsm.AuditLogging = &stubAuditLogging{ + isEnabled: isEnabled, + err: err, + } + return nil + } + } ) func newFakeFSM(opts ...fakeFSMOpt) (*fsm, error) { - fsm := fsm{} + fsm := fsm{ + log: zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)), + } // apply opts for _, opt := range opts { if err := opt(&fsm); err != nil { @@ -90,3 +109,37 @@ func newFakeFSM(opts ...fakeFSMOpt) (*fsm, error) { } return &fsm, nil } + +// stubAuditLogging - a special type to allow to test audit logging +type stubAuditLogging struct { + isEnabled bool + err error +} + +func (s *stubAuditLogging) Enable(ctx context.Context, shoot *gardener.Shoot) (bool, error) { + return s.isEnabled, s.err +} + +func newSetupStateForTest(sfn stateFn, opts ...func(*systemState) error) stateFn { + return func(_ context.Context, _ *fsm, s *systemState) (stateFn, *ctrl.Result, error) { + for _, fn := range opts { + if err := fn(s); err != nil { + return nil, nil, fmt.Errorf("test state setup failed: %s", err) + } + } + return sfn, nil, nil + } +} + +// sFnApplyClusterRoleBindingsStateSetup a special function to setup system state in tests +var sFnApplyClusterRoleBindingsStateSetup = newSetupStateForTest(sFnApplyClusterRoleBindings, func(s *systemState) error { + + s.shoot = &gardener_api.Shoot{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-shoot", + Namespace: "test-namespace", + }, + } + + return nil +}) diff --git a/internal/controller/runtime/runtime_controller_test.go b/internal/controller/runtime/runtime_controller_test.go index 8811ef74..ed6fe337 100644 --- a/internal/controller/runtime/runtime_controller_test.go +++ b/internal/controller/runtime/runtime_controller_test.go @@ -157,7 +157,7 @@ var _ = Describe("Runtime Controller", func() { return false } - if !runtime.IsConditionSet(imv1.ConditionTypeRuntimeProvisioned, imv1.ConditionReasonConfigurationCompleted) { + if !runtime.IsConditionSet(imv1.ConditionTypeRuntimeProvisioned, imv1.ConditionReasonAuditLogConfigured) { return false } diff --git a/internal/controller/runtime/suite_test.go b/internal/controller/runtime/suite_test.go index b8ada71a..22c538b2 100644 --- a/internal/controller/runtime/suite_test.go +++ b/internal/controller/runtime/suite_test.go @@ -18,6 +18,10 @@ package runtime import ( "context" + "encoding/json" + "github.com/kyma-project/infrastructure-manager/internal/auditlogging" + v1 "k8s.io/api/autoscaling/v1" + v12 "k8s.io/api/core/v1" "path/filepath" "testing" "time" @@ -140,14 +144,14 @@ var _ = AfterSuite(func() { func setupGardenerTestClientForProvisioning() { baseShoot := getBaseShootForTestingSequence() shoots := fixShootsSequenceForProvisioning(&baseShoot) - seeds := fixSeedsSequence() + seeds := fixSeedsSequenceForProvisioning() setupGardenerClientWithSequence(shoots, seeds) } func setupGardenerTestClientForUpdate() { baseShoot := getBaseShootForTestingSequence() shoots := fixShootsSequenceForUpdate(&baseShoot) - seeds := fixSeedsSequence() + seeds := fixSeedsSequenceForUpdate() setupGardenerClientWithSequence(shoots, seeds) } @@ -205,14 +209,24 @@ func fixShootsSequenceForProvisioning(shoot *gardener_api.Shoot) []*gardener_api readyShoot.Status.LastOperation.State = gardener_api.LastOperationStateSucceeded + processingShootAfterAuditLogs := readyShoot.DeepCopy() + addAuditLogConfigToShoot(processingShootAfterAuditLogs) + processingShootAfterAuditLogs.Status.LastOperation.Type = gardener_api.LastOperationTypeReconcile + processingShootAfterAuditLogs.Status.LastOperation.State = gardener_api.LastOperationStateProcessing + + readyShootAfterAuditLogs := processingShootAfterAuditLogs.DeepCopy() + readyShootAfterAuditLogs.Status.LastOperation.State = gardener_api.LastOperationStateSucceeded + // processedShoot := processingShoot.DeepCopy() // will add specific data later - return []*gardener_api.Shoot{missingShoot, missingShoot, missingShoot, initialisedShoot, dnsShoot, pendingShoot, processingShoot, readyShoot, readyShoot, readyShoot, readyShoot, readyShoot} + return []*gardener_api.Shoot{missingShoot, missingShoot, missingShoot, initialisedShoot, dnsShoot, pendingShoot, processingShoot, readyShoot, readyShoot, readyShoot, readyShoot, readyShoot, processingShootAfterAuditLogs, readyShootAfterAuditLogs, readyShootAfterAuditLogs} } func fixShootsSequenceForUpdate(shoot *gardener_api.Shoot) []*gardener_api.Shoot { pendingShoot := shoot.DeepCopy() + addAuditLogConfigToShoot(pendingShoot) + pendingShoot.Spec.DNS = &gardener_api.DNS{ Domain: ptr.To("test.domain"), } @@ -236,7 +250,7 @@ func fixShootsSequenceForUpdate(shoot *gardener_api.Shoot) []*gardener_api.Shoot // processedShoot := processingShoot.DeepCopy() // will add specific data later - return []*gardener_api.Shoot{pendingShoot, processingShoot, readyShoot, readyShoot, readyShoot} + return []*gardener_api.Shoot{pendingShoot, processingShoot, readyShoot, readyShoot} } func fixShootsSequenceForDelete(shoot *gardener_api.Shoot) []*gardener_api.Shoot { @@ -270,7 +284,22 @@ func fixShootsSequenceForDelete(shoot *gardener_api.Shoot) []*gardener_api.Shoot return []*gardener_api.Shoot{currentShoot, currentShoot, currentShoot, currentShoot, pendingDeleteShoot, nil} } -func fixSeedsSequence() []*gardener_api.Seed { +func fixSeedsSequenceForProvisioning() []*gardener_api.Seed { + seed := &gardener_api.Seed{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-seed", + }, + Spec: gardener_api.SeedSpec{ + Provider: gardener_api.SeedProvider{ + Type: "aws", + }, + }, + } + + return []*gardener_api.Seed{seed, seed} +} + +func fixSeedsSequenceForUpdate() []*gardener_api.Seed { seed := &gardener_api.Seed{ ObjectMeta: metav1.ObjectMeta{ Name: "test-seed", @@ -325,3 +354,46 @@ func fixConverterConfigForTests() gardener_shoot.ConverterConfig { }, } } + +func addAuditLogConfigToShoot(shoot *gardener_api.Shoot) { + shoot.Spec.Kubernetes.KubeAPIServer.AuditConfig = &gardener_api.AuditConfig{ + AuditPolicy: &gardener_api.AuditPolicy{ + ConfigMapRef: &v12.ObjectReference{Name: "policy-config-map"}, + }, + } + + shoot.Spec.Resources = append(shoot.Spec.Resources, gardener_api.NamedResourceReference{ + Name: "auditlog-credentials", + ResourceRef: v1.CrossVersionObjectReference{ + Kind: "Secret", + Name: "auditlog-secret", + APIVersion: "v1", + }, + }) + + const ( + extensionKind = "AuditlogConfig" + extensionVersion = "service.auditlog.extensions.gardener.cloud/v1alpha1" + extensionType = "standard" + ) + + shoot.Spec.Extensions = append(shoot.Spec.Extensions, gardener_api.Extension{ + Type: "shoot-auditlog-service", + }) + + ext := &shoot.Spec.Extensions[len(shoot.Spec.Extensions)-1] + + cfg := auditlogging.AuditlogExtensionConfig{ + TypeMeta: metav1.TypeMeta{ + Kind: extensionKind, + APIVersion: extensionVersion, + }, + Type: extensionType, + TenantID: "79c64792-9c1e-4c1b-9941-ef7560dd3eae", + ServiceURL: "https://auditlog.example.com:3001", + SecretReferenceName: "auditlog-credentials", + } + + ext.ProviderConfig = &runtime.RawExtension{} + ext.ProviderConfig.Raw, _ = json.Marshal(cfg) +} diff --git a/internal/controller/runtime/testdata/auditConfig.json b/internal/controller/runtime/testdata/auditConfig.json index a7a163c3..608d06a2 100644 --- a/internal/controller/runtime/testdata/auditConfig.json +++ b/internal/controller/runtime/testdata/auditConfig.json @@ -3,7 +3,7 @@ "eu-central-1": { "tenantID": "79c64792-9c1e-4c1b-9941-ef7560dd3eae", "serviceURL": "https://auditlog.example.com:3001", - "secretName": "auditlog-secret2" + "secretName": "auditlog-secret" } } } diff --git a/internal/gardener/shoot/extender/oidc.go b/internal/gardener/shoot/extender/oidc.go index fdaadf46..8e8da762 100644 --- a/internal/gardener/shoot/extender/oidc.go +++ b/internal/gardener/shoot/extender/oidc.go @@ -3,11 +3,32 @@ package extender import ( gardener "github.com/gardener/gardener/pkg/apis/core/v1beta1" imv1 "github.com/kyma-project/infrastructure-manager/api/v1" + "k8s.io/utils/ptr" +) + +const ( + OidcExtensionType = "shoot-oidc-service" ) func ExtendWithOIDC(runtime imv1.Runtime, shoot *gardener.Shoot) error { oidcConfig := runtime.Spec.Shoot.Kubernetes.KubeAPIServer.OidcConfig + setOIDCExtension(shoot) + setKubeAPIServerOIDCConfig(shoot, oidcConfig) + + return nil +} + +func setOIDCExtension(shoot *gardener.Shoot) { + oidcService := gardener.Extension{ + Type: OidcExtensionType, + Disabled: ptr.To(false), + } + + shoot.Spec.Extensions = append(shoot.Spec.Extensions, oidcService) +} + +func setKubeAPIServerOIDCConfig(shoot *gardener.Shoot, oidcConfig gardener.OIDCConfig) { shoot.Spec.Kubernetes.KubeAPIServer = &gardener.KubeAPIServerConfig{ OIDCConfig: &gardener.OIDCConfig{ CABundle: oidcConfig.CABundle, @@ -21,6 +42,4 @@ func ExtendWithOIDC(runtime imv1.Runtime, shoot *gardener.Shoot) error { UsernamePrefix: oidcConfig.UsernamePrefix, }, } - - return nil } diff --git a/internal/gardener/shoot/extender/oidc_test.go b/internal/gardener/shoot/extender/oidc_test.go index 9303024e..2fda2db5 100644 --- a/internal/gardener/shoot/extender/oidc_test.go +++ b/internal/gardener/shoot/extender/oidc_test.go @@ -43,6 +43,9 @@ func TestOidcExtender(t *testing.T) { // then require.NoError(t, err) + assert.Equal(t, runtimeShoot.Spec.Shoot.Kubernetes.KubeAPIServer.OidcConfig, *shoot.Spec.Kubernetes.KubeAPIServer.OIDCConfig) + assert.Equal(t, false, *shoot.Spec.Extensions[0].Disabled) + assert.Equal(t, "shoot-oidc-service", shoot.Spec.Extensions[0].Type) }) }