diff --git a/.github/PULL_REQUEST_TEMPLATE/pr-template.md b/.github/PULL_REQUEST_TEMPLATE/pr-template.md index 52cd454bf2..753383a7f6 100644 --- a/.github/PULL_REQUEST_TEMPLATE/pr-template.md +++ b/.github/PULL_REQUEST_TEMPLATE/pr-template.md @@ -26,16 +26,15 @@ Please also provide information about any automatic tests that you added. - [ ] My PR fulfills the Definition of Done of the corresponding issue and not more (or parts if the issue is separated into multiple PRs) - [ ] I used descriptive commit messages to help reviewers understand my thought process -- [ ] I signed off all my commits according to the Developer Certificate of Origin (DCO) ( - see [Contribution Guide](https://github.com/keptn/lifecycle-toolkit/blob/main/CONTRIBUTING.md#submit-a-pull-request-)) +- [ ] I signed off all my commits according to the Developer Certificate of Origin (DCO) + see [Contribution Guide](https://lifecycle.keptn.sh/contribute/docs/contribution-guidelines) - [ ] My PR title is formatted according to the semantic PR conventions described in - the [Contribution Guide](https://github.com/keptn/lifecycle-toolkit/blob/main/CONTRIBUTING.md#submit-a-pull-request-) + the [Contribution Guide](https://lifecycle.keptn.sh/contribute/docs/contribution-guidelines) - [ ] My code follows the style guidelines of this project (golangci-lint passes, YAMLLint passes) - [ ] I regenerated the auto-generated docs for Helm and the CRD documentation (if applicable) - [ ] I have performed a self-review of my code - [ ] I have made corresponding changes to the documentation (if needed) -- [ ] My changes result in all-green PR checks (first-time contributors need to ask a maintainer to approve their test - runs) +- [ ] My changes result in all-green PR checks (first-time contributors need to ask a maintainer to approve their test runs) - [ ] New and existing unit and integration tests pass locally with my changes @@ -54,13 +53,12 @@ Fixes # (issue) - [ ] My PR fulfills the Definition of Done of the corresponding issue and not more (or parts if the issue is separated into multiple PRs) - [ ] I used descriptive commit messages to help reviewers understand my thought process -- [ ] I signed off all my commits according to the Developer Certificate of Origin (DCO) ( - see [Contribution Guide](https://github.com/keptn/lifecycle-toolkit/blob/main/docs/CONTRIBUTING.md#developer-certification-of-origin-dco)) +- [ ] I signed off all my commits according to the Developer Certificate of Origin (DCO)( + see [Contribution Guide](https://lifecycle.keptn.sh/contribute/docs/contribution-guidelines)) - [ ] My PR title is formatted according to the semantic PR conventions described in - the [Contribution Guide](https://github.com/keptn/lifecycle-toolkit/blob/main/CONTRIBUTING.md#submit-a-pull-request-) + the [Contribution Guide](https://lifecycle.keptn.sh/contribute/docs/contribution-guidelines) - [ ] My content follows the style guidelines of this project (YAMLLint, markdown-lint) - [ ] I regenerated the auto-generated docs for Helm and the CRD documentation (if applicable) - [ ] I have performed a self-review of my content including grammar and typo errors and also checked the rendered page from the Netlify deploy preview -- [ ] My changes result in all-green PR checks (first-time contributors need to ask a maintainer to approve their test - runs) +- [ ] My changes result in all-green PR checks (first-time contributors need to ask a maintainer to approve their test runs) diff --git a/.github/scripts/.helm-tests/default/result.yaml b/.github/scripts/.helm-tests/default/result.yaml index 6e9af18d64..1c15ebb717 100644 --- a/.github/scripts/.helm-tests/default/result.yaml +++ b/.github/scripts/.helm-tests/default/result.yaml @@ -8388,3 +8388,23 @@ webhooks: resources: - analyses sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: 'metrics-webhook-service' + namespace: 'helmtests' + path: /validate-metrics-keptn-sh-v1alpha3-analysisdefinition + failurePolicy: Fail + name: vanalysisdefinition.kb.io + rules: + - apiGroups: + - metrics.keptn.sh + apiVersions: + - v1alpha3 + operations: + - CREATE + - UPDATE + resources: + - analysisdefinitions + sideEffects: None diff --git a/.github/workflows/load-test.yml b/.github/workflows/load-test.yml index a0d8497d0a..cf579228c8 100644 --- a/.github/workflows/load-test.yml +++ b/.github/workflows/load-test.yml @@ -9,7 +9,7 @@ on: env: GO_VERSION: "~1.20" # renovate: datasource=github-tags depName=cloud-bulldozer/kube-burner - KUBE_BURNER_VERSION: "v1.7.7" + KUBE_BURNER_VERSION: "v1.7.8" defaults: run: shell: bash diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 091a39fcde..d41afb18b0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -114,7 +114,7 @@ jobs: return releaseMatrix.length > 0 ? { config: releaseMatrix } : {}; build-release: - if: needs.release-please.outputs.releases-created == 'true' + if: needs.release-please.outputs.releases-created == 'true' && needs.release-please.outputs.build-matrix != '{}' needs: - release-please strategy: diff --git a/.github/workflows/security-scans.yml b/.github/workflows/security-scans.yml index dbb9b7199c..e8a01dcf18 100644 --- a/.github/workflows/security-scans.yml +++ b/.github/workflows/security-scans.yml @@ -50,7 +50,7 @@ jobs: echo "RUN_ID=$RUN_ID" >> $GITHUB_OUTPUT - name: Download all artifacts from last successful build of main branch - uses: dawidd6/action-download-artifact@v2.27.0 + uses: dawidd6/action-download-artifact@v2.28.0 id: download_artifacts_push with: # Download last successful artifact from a CI build diff --git a/.github/workflows/validate-semantic-pr.yml b/.github/workflows/validate-semantic-pr.yml index 739ba57e53..cee87c08e5 100644 --- a/.github/workflows/validate-semantic-pr.yml +++ b/.github/workflows/validate-semantic-pr.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Validate Pull Request - uses: amannn/action-semantic-pull-request@v5.2.0 + uses: amannn/action-semantic-pull-request@v5.3.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/Makefile b/Makefile index 5e806c780f..df494bda3c 100644 --- a/Makefile +++ b/Makefile @@ -26,12 +26,14 @@ KUSTOMIZE ?= $(LOCALBIN)/kustomize integration-test: # to run a single test by name use --test eg. --test=expose-keptn-metric kubectl kuttl test --start-kind=false ./test/integration/ --config=kuttl-test.yaml kubectl kuttl test --start-kind=false ./test/testmetrics/ --config=kuttl-test.yaml + kubectl kuttl test --start-kind=false ./test/testanalysis/ --config=kuttl-test.yaml kubectl kuttl test --start-kind=false ./test/testcertificate/ --config=kuttl-test.yaml .PHONY: integration-test-local #these tests should run on a real cluster! integration-test-local: install-prometheus kubectl kuttl test --start-kind=false ./test/integration/ --config=kuttl-test-local.yaml kubectl kuttl test --start-kind=false ./test/testmetrics/ --config=kuttl-test-local.yaml + kubectl kuttl test --start-kind=false ./test/testanalysis/ --config=kuttl-test-local.yaml kubectl kuttl test --start-kind=false ./test/testcertificate/ --config=kuttl-test-local.yaml .PHONY: load-test diff --git a/docs/package-lock.json b/docs/package-lock.json index 321dbd3d46..bed77e0e34 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -76,9 +76,9 @@ } }, "node_modules/autoprefixer": { - "version": "10.4.15", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.15.tgz", - "integrity": "sha512-KCuPB8ZCIqFdA4HwKXsvz7j6gvSDNhDP7WnUjBleRkKjPdvCmHFuQ77ocavI8FT6NdvlBnE2UFr2H4Mycn8Vew==", + "version": "10.4.16", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz", + "integrity": "sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==", "funding": [ { "type": "opencollective", @@ -95,8 +95,8 @@ ], "dependencies": { "browserslist": "^4.21.10", - "caniuse-lite": "^1.0.30001520", - "fraction.js": "^4.2.0", + "caniuse-lite": "^1.0.30001538", + "fraction.js": "^4.3.6", "normalize-range": "^0.1.2", "picocolors": "^1.0.0", "postcss-value-parser": "^4.2.0" @@ -162,9 +162,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001522", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001522.tgz", - "integrity": "sha512-TKiyTVZxJGhsTszLuzb+6vUZSjVOAhClszBr2Ta2k9IwtNBT/4dzmL6aywt0HCgEZlmwJzXJd8yNiob6HgwTRg==", + "version": "1.0.30001538", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001538.tgz", + "integrity": "sha512-HWJnhnID+0YMtGlzcp3T9drmBJUVDchPJ08tpUGFLs9CYlwWPH2uLgpHn8fND5pCgXVtnGS3H4QR9XLMHVNkHw==", "funding": [ { "type": "opencollective", @@ -307,15 +307,15 @@ } }, "node_modules/fraction.js": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", - "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.6.tgz", + "integrity": "sha512-n2aZ9tNfYDwaHhvFTkhFErqOMIb8uyzSQ+vGJBjZyanAKZVbGUQ1sngfk9FdkBw7G26O7AgNjLcecLffD1c7eg==", "engines": { "node": "*" }, "funding": { "type": "patreon", - "url": "https://www.patreon.com/infusion" + "url": "https://github.com/sponsors/rawify" } }, "node_modules/fs-extra": { diff --git a/helm/chart/templates/metrics-validating-webhook-configuration.yaml b/helm/chart/templates/metrics-validating-webhook-configuration.yaml index 21281aef54..d188313680 100644 --- a/helm/chart/templates/metrics-validating-webhook-configuration.yaml +++ b/helm/chart/templates/metrics-validating-webhook-configuration.yaml @@ -47,4 +47,24 @@ webhooks: - UPDATE resources: - analyses - sideEffects: None \ No newline at end of file + sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: 'metrics-webhook-service' + namespace: '{{ .Release.Namespace }}' + path: /validate-metrics-keptn-sh-v1alpha3-analysisdefinition + failurePolicy: Fail + name: vanalysisdefinition.kb.io + rules: + - apiGroups: + - metrics.keptn.sh + apiVersions: + - v1alpha3 + operations: + - CREATE + - UPDATE + resources: + - analysisdefinitions + sideEffects: None diff --git a/klt-cert-manager/go.mod b/klt-cert-manager/go.mod index fad3e82232..365b3d1fc3 100644 --- a/klt-cert-manager/go.mod +++ b/klt-cert-manager/go.mod @@ -6,7 +6,7 @@ require ( github.com/go-logr/logr v1.2.4 github.com/kelseyhightower/envconfig v1.4.0 github.com/pkg/errors v0.9.1 - github.com/spf13/afero v1.9.5 + github.com/spf13/afero v1.10.0 github.com/stretchr/testify v1.8.4 k8s.io/api v0.28.2 k8s.io/apiextensions-apiserver v0.28.2 diff --git a/klt-cert-manager/go.sum b/klt-cert-manager/go.sum index e982eb8429..3eef9093b8 100644 --- a/klt-cert-manager/go.sum +++ b/klt-cert-manager/go.sum @@ -212,8 +212,8 @@ github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+Pymzi github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= -github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= +github.com/spf13/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY= +github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/lifecycle-operator/apis/lifecycle/v1alpha3/common/common.go b/lifecycle-operator/apis/lifecycle/v1alpha3/common/common.go index 52cf3d88cf..84dc0fe22d 100644 --- a/lifecycle-operator/apis/lifecycle/v1alpha3/common/common.go +++ b/lifecycle-operator/apis/lifecycle/v1alpha3/common/common.go @@ -23,6 +23,7 @@ const K8sRecommendedAppAnnotations = "app.kubernetes.io/part-of" const K8sRecommendedManagedByAnnotations = "app.kubernetes.io/managed-by" const PreDeploymentEvaluationAnnotation = "keptn.sh/pre-deployment-evaluations" const PostDeploymentEvaluationAnnotation = "keptn.sh/post-deployment-evaluations" +const SchedulingGateRemoved = "keptn.sh/scheduling-gate-removed" const TaskNameAnnotation = "keptn.sh/task-name" const NamespaceEnabledAnnotation = "keptn.sh/lifecycle-toolkit" const CreateAppTaskSpanName = "create_%s_app_task" @@ -30,8 +31,9 @@ const CreateWorkloadTaskSpanName = "create_%s_deployment_task" const CreateAppEvalSpanName = "create_%s_app_evaluation" const CreateWorkloadEvalSpanName = "create_%s_deployment_evaluation" const AppTypeAnnotation = "keptn.sh/app-type" +const KeptnGate = "keptn-prechecks-gate" -const MinKLTNameLen = 80 +const MinKeptnNameLen = 80 const MaxK8sObjectLength = 253 type AppType string @@ -176,17 +178,17 @@ const ( func GenerateTaskName(checkType CheckType, taskName string) string { randomId := rand.Intn(99_999-10_000) + 10000 - return operatorcommon.CreateResourceName(MaxK8sObjectLength, MinKLTNameLen, string(checkType), taskName, strconv.Itoa(randomId)) + return operatorcommon.CreateResourceName(MaxK8sObjectLength, MinKeptnNameLen, string(checkType), taskName, strconv.Itoa(randomId)) } func GenerateJobName(taskName string) string { randomId := rand.Intn(99_999-10_000) + 10000 - return operatorcommon.CreateResourceName(MaxK8sObjectLength, MinKLTNameLen, taskName, strconv.Itoa(randomId)) + return operatorcommon.CreateResourceName(MaxK8sObjectLength, MinKeptnNameLen, taskName, strconv.Itoa(randomId)) } func GenerateEvaluationName(checkType CheckType, evalName string) string { randomId := rand.Intn(99_999-10_000) + 10000 - return operatorcommon.CreateResourceName(MaxK8sObjectLength, MinKLTNameLen, string(checkType), evalName, strconv.Itoa(randomId)) + return operatorcommon.CreateResourceName(MaxK8sObjectLength, MinKeptnNameLen, string(checkType), evalName, strconv.Itoa(randomId)) } // MergeMaps merges two maps into a new map. If a key exists in both maps, the diff --git a/lifecycle-operator/apis/lifecycle/v1alpha3/keptnapp_types.go b/lifecycle-operator/apis/lifecycle/v1alpha3/keptnapp_types.go index 8e407c8ae1..0e224b7734 100644 --- a/lifecycle-operator/apis/lifecycle/v1alpha3/keptnapp_types.go +++ b/lifecycle-operator/apis/lifecycle/v1alpha3/keptnapp_types.go @@ -102,7 +102,7 @@ func init() { } func (a KeptnApp) GetAppVersionName() string { - return operatorcommon.CreateResourceName(common.MaxK8sObjectLength, common.MinKLTNameLen, a.Name, a.Spec.Version, common.Hash(a.Generation)) + return operatorcommon.CreateResourceName(common.MaxK8sObjectLength, common.MinKeptnNameLen, a.Name, a.Spec.Version, common.Hash(a.Generation)) } func (a KeptnApp) SetSpanAttributes(span trace.Span) { diff --git a/lifecycle-operator/apis/lifecycle/v1alpha3/keptnworkload_types.go b/lifecycle-operator/apis/lifecycle/v1alpha3/keptnworkload_types.go index 38e031457e..956debd855 100644 --- a/lifecycle-operator/apis/lifecycle/v1alpha3/keptnworkload_types.go +++ b/lifecycle-operator/apis/lifecycle/v1alpha3/keptnworkload_types.go @@ -103,7 +103,7 @@ func init() { } func (w KeptnWorkload) GetWorkloadInstanceName() string { - return operatorcommon.CreateResourceName(common.MaxK8sObjectLength, common.MinKLTNameLen, w.Name, w.Spec.Version) + return operatorcommon.CreateResourceName(common.MaxK8sObjectLength, common.MinKeptnNameLen, w.Name, w.Spec.Version) } func (w KeptnWorkload) SetSpanAttributes(span trace.Span) { diff --git a/lifecycle-operator/config/rbac/role.yaml b/lifecycle-operator/config/rbac/role.yaml index 265bef1dc8..bac0a3df79 100644 --- a/lifecycle-operator/config/rbac/role.yaml +++ b/lifecycle-operator/config/rbac/role.yaml @@ -85,6 +85,7 @@ rules: verbs: - get - list + - update - watch - apiGroups: - "" diff --git a/lifecycle-operator/controllers/common/fake/schedulinggateshandler_mock.go b/lifecycle-operator/controllers/common/fake/schedulinggateshandler_mock.go new file mode 100644 index 0000000000..a4c7ee86e0 --- /dev/null +++ b/lifecycle-operator/controllers/common/fake/schedulinggateshandler_mock.go @@ -0,0 +1,115 @@ +// Code generated by moq; DO NOT EDIT. +// github.com/matryer/moq + +package fake + +import ( + "context" + lfcv1alpha3 "github.com/keptn/lifecycle-toolkit/lifecycle-operator/apis/lifecycle/v1alpha3" + "sync" +) + +// ISchedulingGatesHandlerMock is a mock implementation of common.ISchedulingGatesHandler. +// +// func TestSomethingThatUsesISchedulingGatesHandler(t *testing.T) { +// +// // make and configure a mocked common.ISchedulingGatesHandler +// mockedISchedulingGatesHandler := &ISchedulingGatesHandlerMock{ +// EnabledFunc: func() bool { +// panic("mock out the Enabled method") +// }, +// RemoveGatesFunc: func(ctx context.Context, workloadInstance *lfcv1alpha3.KeptnWorkloadInstance) error { +// panic("mock out the RemoveGates method") +// }, +// } +// +// // use mockedISchedulingGatesHandler in code that requires common.ISchedulingGatesHandler +// // and then make assertions. +// +// } +type ISchedulingGatesHandlerMock struct { + // EnabledFunc mocks the Enabled method. + EnabledFunc func() bool + + // RemoveGatesFunc mocks the RemoveGates method. + RemoveGatesFunc func(ctx context.Context, workloadInstance *lfcv1alpha3.KeptnWorkloadInstance) error + + // calls tracks calls to the methods. + calls struct { + // Enabled holds details about calls to the Enabled method. + Enabled []struct { + } + // RemoveGates holds details about calls to the RemoveGates method. + RemoveGates []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // WorkloadInstance is the workloadInstance argument value. + WorkloadInstance *lfcv1alpha3.KeptnWorkloadInstance + } + } + lockEnabled sync.RWMutex + lockRemoveGates sync.RWMutex +} + +// Enabled calls EnabledFunc. +func (mock *ISchedulingGatesHandlerMock) Enabled() bool { + if mock.EnabledFunc == nil { + panic("ISchedulingGatesHandlerMock.EnabledFunc: method is nil but ISchedulingGatesHandler.Enabled was just called") + } + callInfo := struct { + }{} + mock.lockEnabled.Lock() + mock.calls.Enabled = append(mock.calls.Enabled, callInfo) + mock.lockEnabled.Unlock() + return mock.EnabledFunc() +} + +// EnabledCalls gets all the calls that were made to Enabled. +// Check the length with: +// +// len(mockedISchedulingGatesHandler.EnabledCalls()) +func (mock *ISchedulingGatesHandlerMock) EnabledCalls() []struct { +} { + var calls []struct { + } + mock.lockEnabled.RLock() + calls = mock.calls.Enabled + mock.lockEnabled.RUnlock() + return calls +} + +// RemoveGates calls RemoveGatesFunc. +func (mock *ISchedulingGatesHandlerMock) RemoveGates(ctx context.Context, workloadInstance *lfcv1alpha3.KeptnWorkloadInstance) error { + if mock.RemoveGatesFunc == nil { + panic("ISchedulingGatesHandlerMock.RemoveGatesFunc: method is nil but ISchedulingGatesHandler.RemoveGates was just called") + } + callInfo := struct { + Ctx context.Context + WorkloadInstance *lfcv1alpha3.KeptnWorkloadInstance + }{ + Ctx: ctx, + WorkloadInstance: workloadInstance, + } + mock.lockRemoveGates.Lock() + mock.calls.RemoveGates = append(mock.calls.RemoveGates, callInfo) + mock.lockRemoveGates.Unlock() + return mock.RemoveGatesFunc(ctx, workloadInstance) +} + +// RemoveGatesCalls gets all the calls that were made to RemoveGates. +// Check the length with: +// +// len(mockedISchedulingGatesHandler.RemoveGatesCalls()) +func (mock *ISchedulingGatesHandlerMock) RemoveGatesCalls() []struct { + Ctx context.Context + WorkloadInstance *lfcv1alpha3.KeptnWorkloadInstance +} { + var calls []struct { + Ctx context.Context + WorkloadInstance *lfcv1alpha3.KeptnWorkloadInstance + } + mock.lockRemoveGates.RLock() + calls = mock.calls.RemoveGates + mock.lockRemoveGates.RUnlock() + return calls +} diff --git a/lifecycle-operator/controllers/common/schedulinggateshandler.go b/lifecycle-operator/controllers/common/schedulinggateshandler.go new file mode 100644 index 0000000000..d6fabf4953 --- /dev/null +++ b/lifecycle-operator/controllers/common/schedulinggateshandler.go @@ -0,0 +1,108 @@ +package common + +import ( + "context" + + "github.com/go-logr/logr" + klcv1alpha3 "github.com/keptn/lifecycle-toolkit/lifecycle-operator/apis/lifecycle/v1alpha3" + apicommon "github.com/keptn/lifecycle-toolkit/lifecycle-operator/apis/lifecycle/v1alpha3/common" + controllererrors "github.com/keptn/lifecycle-toolkit/lifecycle-operator/controllers/errors" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +//go:generate moq -pkg fake -skip-ensure -out ./fake/schedulinggateshandler_mock.go . ISchedulingGatesHandler +type ISchedulingGatesHandler interface { + RemoveGates(ctx context.Context, workloadInstance *klcv1alpha3.KeptnWorkloadInstance) error + Enabled() bool +} + +type RemoveGatesFunc func(ctx context.Context, c client.Client, podName string, podNamespace string) error +type GetPodsFunc func(ctx context.Context, c client.Client, ownerUID types.UID, ownerKind string, namespace string) ([]string, error) + +type SchedulingGatesHandler struct { + client.Client + logr.Logger + enabled bool + removeGates RemoveGatesFunc + getPods GetPodsFunc +} + +func NewSchedulingGatesHandler(c client.Client, l logr.Logger, enabled bool) *SchedulingGatesHandler { + return &SchedulingGatesHandler{ + Client: c, + Logger: l, + enabled: enabled, + removeGates: removePodGates, + getPods: getPodsOfOwner, + } +} + +func (h *SchedulingGatesHandler) RemoveGates(ctx context.Context, workloadInstance *klcv1alpha3.KeptnWorkloadInstance) error { + switch workloadInstance.Spec.ResourceReference.Kind { + case "Pod": + return h.removeGates(ctx, h.Client, workloadInstance.Spec.ResourceReference.Name, workloadInstance.Namespace) + case "ReplicaSet", "StatefulSet", "DaemonSet": + podList, err := h.getPods(ctx, h.Client, workloadInstance.Spec.ResourceReference.UID, workloadInstance.Spec.ResourceReference.Kind, workloadInstance.Namespace) + if err != nil { + h.Logger.Error(err, "cannot get pods") + return err + } + for _, pod := range podList { + err := h.removeGates(ctx, h.Client, pod, workloadInstance.Namespace) + if err != nil { + h.Logger.Error(err, "cannot remove gates from pod") + return err + } + } + default: + return controllererrors.ErrUnsupportedWorkloadInstanceResourceReference + } + + return nil +} + +func (h *SchedulingGatesHandler) Enabled() bool { + return h.enabled +} + +func removePodGates(ctx context.Context, c client.Client, podName string, podNamespace string) error { + pod := &v1.Pod{} + err := c.Get(ctx, types.NamespacedName{Namespace: podNamespace, Name: podName}, pod) + if err != nil { + return err + } + + if pod.Annotations[apicommon.SchedulingGateRemoved] != "" { + return nil + } + + if len(pod.Annotations) == 0 { + pod.Annotations = make(map[string]string, 1) + } + pod.Annotations[apicommon.SchedulingGateRemoved] = "true" + pod.Spec.SchedulingGates = nil + return c.Update(ctx, pod) +} + +func getPodsOfOwner(ctx context.Context, c client.Client, ownerUID types.UID, ownerKind string, namespace string) ([]string, error) { + pods := &v1.PodList{} + err := c.List(ctx, pods, client.InNamespace(namespace)) + if err != nil { + return nil, err + } + + var resultPods []string + + for _, pod := range pods.Items { + for _, owner := range pod.OwnerReferences { + if owner.Kind == ownerKind && owner.UID == ownerUID { + resultPods = append(resultPods, pod.Name) + break + } + } + } + + return resultPods, nil +} diff --git a/lifecycle-operator/controllers/common/schedulinggateshandler_test.go b/lifecycle-operator/controllers/common/schedulinggateshandler_test.go new file mode 100644 index 0000000000..0ffe58ef8e --- /dev/null +++ b/lifecycle-operator/controllers/common/schedulinggateshandler_test.go @@ -0,0 +1,365 @@ +package common + +import ( + "context" + "fmt" + "testing" + + klcv1alpha3 "github.com/keptn/lifecycle-toolkit/lifecycle-operator/apis/lifecycle/v1alpha3" + apicommon "github.com/keptn/lifecycle-toolkit/lifecycle-operator/apis/lifecycle/v1alpha3/common" + controllererrors "github.com/keptn/lifecycle-toolkit/lifecycle-operator/controllers/errors" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func Test_RemovePodGates(t *testing.T) { + tests := []struct { + name string + podName string + pod *corev1.Pod + wantError bool + annotations map[string]string + }{ + { + name: "pod does not exist", + podName: "pod", + pod: &corev1.Pod{ + ObjectMeta: v1.ObjectMeta{ + Name: "pod2", + Namespace: "default", + }, + }, + wantError: true, + }, + { + name: "scheduling gates already removed", + podName: "pod", + pod: &corev1.Pod{ + ObjectMeta: v1.ObjectMeta{ + Name: "pod", + Namespace: "default", + Annotations: map[string]string{ + apicommon.SchedulingGateRemoved: "true", + }, + }, + }, + wantError: false, + annotations: map[string]string{ + apicommon.SchedulingGateRemoved: "true", + }, + }, + { + name: "scheduling gates removed - empty annotations", + podName: "pod", + pod: &corev1.Pod{ + ObjectMeta: v1.ObjectMeta{ + Name: "pod", + Namespace: "default", + }, + Spec: corev1.PodSpec{ + SchedulingGates: []corev1.PodSchedulingGate{ + { + Name: "gate", + }, + }, + }, + }, + wantError: false, + annotations: map[string]string{ + apicommon.SchedulingGateRemoved: "true", + }, + }, + { + name: "scheduling gates removed - not empty annotations", + podName: "pod", + pod: &corev1.Pod{ + ObjectMeta: v1.ObjectMeta{ + Name: "pod", + Namespace: "default", + Annotations: map[string]string{ + "test": "test", + }, + }, + Spec: corev1.PodSpec{ + SchedulingGates: []corev1.PodSchedulingGate{ + { + Name: "gate", + }, + }, + }, + }, + wantError: false, + annotations: map[string]string{ + apicommon.SchedulingGateRemoved: "true", + "test": "test", + }, + }, + } + + err := klcv1alpha3.AddToScheme(scheme.Scheme) + require.Nil(t, err) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client := fake.NewClientBuilder().WithObjects(tt.pod).WithStatusSubresource(tt.pod).Build() + err := removePodGates(context.TODO(), client, tt.podName, tt.pod.Namespace) + if tt.wantError != (err != nil) { + t.Errorf("want error: %t, got: %v", tt.wantError, err) + } + if !tt.wantError { + pod := &corev1.Pod{} + err := client.Get(context.TODO(), types.NamespacedName{Namespace: tt.pod.Namespace, Name: tt.podName}, pod) + require.Nil(t, err) + require.Equal(t, []corev1.PodSchedulingGate(nil), pod.Spec.SchedulingGates) + require.Equal(t, tt.annotations, pod.Annotations) + } + }) + } +} + +func Test_GetPodsOfOwner(t *testing.T) { + namespace := "default" + tests := []struct { + name string + uid types.UID + kind string + pods *corev1.PodList + result []string + }{ + { + name: "pod list empty", + pods: &corev1.PodList{}, + result: nil, + }, + { + name: "pod list not matching kind or uid", + pods: &corev1.PodList{ + Items: []corev1.Pod{ + { + ObjectMeta: v1.ObjectMeta{ + Name: "pod1", + Namespace: "default", + OwnerReferences: []v1.OwnerReference{ + { + Kind: "unknown", + UID: types.UID("uid"), + }, + }, + }, + }, + }, + }, + kind: "unknown2", + uid: types.UID("uid2"), + result: nil, + }, + { + name: "pod list matches one pod of list", + pods: &corev1.PodList{ + Items: []corev1.Pod{ + { + ObjectMeta: v1.ObjectMeta{ + Name: "pod1", + Namespace: "default", + OwnerReferences: []v1.OwnerReference{ + { + Kind: "unknown", + UID: types.UID("uid"), + }, + }, + }, + }, + { + ObjectMeta: v1.ObjectMeta{ + Name: "pod2", + Namespace: "default", + OwnerReferences: []v1.OwnerReference{ + { + Kind: "unknown2", + UID: types.UID("uid2"), + }, + }, + }, + }, + }, + }, + kind: "unknown", + uid: types.UID("uid"), + result: []string{"pod1"}, + }, + } + + err := klcv1alpha3.AddToScheme(scheme.Scheme) + require.Nil(t, err) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client := fake.NewClientBuilder().WithLists(tt.pods).Build() + res, err := getPodsOfOwner(context.TODO(), client, tt.uid, tt.kind, namespace) + require.Nil(t, err) + require.Equal(t, tt.result, res) + }) + } +} + +func Test_SchedulingGatesHandler_IsSchedulingGatesEnabled(t *testing.T) { + h := SchedulingGatesHandler{ + enabled: true, + } + + require.True(t, h.Enabled()) + + h.enabled = false + + require.False(t, h.Enabled()) +} + +func Test_SchedulingGatesHandler_IsSchedulingGatesEnabledRemoveGates(t *testing.T) { + tests := []struct { + name string + handler SchedulingGatesHandler + wi *klcv1alpha3.KeptnWorkloadInstance + wantErr error + }{ + { + name: "unsuported resource ref", + handler: SchedulingGatesHandler{}, + wi: &klcv1alpha3.KeptnWorkloadInstance{ + Spec: klcv1alpha3.KeptnWorkloadInstanceSpec{ + KeptnWorkloadSpec: klcv1alpha3.KeptnWorkloadSpec{ + ResourceReference: klcv1alpha3.ResourceReference{ + Kind: "unsupported", + }, + }, + }, + }, + wantErr: controllererrors.ErrUnsupportedWorkloadInstanceResourceReference, + }, + { + name: "pod - happy path", + handler: SchedulingGatesHandler{ + removeGates: func(ctx context.Context, c client.Client, podName, podNamespace string) error { + return nil + }, + }, + wi: &klcv1alpha3.KeptnWorkloadInstance{ + Spec: klcv1alpha3.KeptnWorkloadInstanceSpec{ + KeptnWorkloadSpec: klcv1alpha3.KeptnWorkloadSpec{ + ResourceReference: klcv1alpha3.ResourceReference{ + Kind: "Pod", + }, + }, + }, + }, + wantErr: nil, + }, + { + name: "pod - fail path", + handler: SchedulingGatesHandler{ + removeGates: func(ctx context.Context, c client.Client, podName, podNamespace string) error { + return fmt.Errorf("pod") + }, + }, + wi: &klcv1alpha3.KeptnWorkloadInstance{ + Spec: klcv1alpha3.KeptnWorkloadInstanceSpec{ + KeptnWorkloadSpec: klcv1alpha3.KeptnWorkloadSpec{ + ResourceReference: klcv1alpha3.ResourceReference{ + Kind: "Pod", + }, + }, + }, + }, + wantErr: fmt.Errorf("pod"), + }, + { + name: "ReplicaSet, StatefulSet, DaemonSet - happy path", + handler: SchedulingGatesHandler{ + removeGates: func(ctx context.Context, c client.Client, podName, podNamespace string) error { + return nil + }, + getPods: func(ctx context.Context, c client.Client, ownerUID types.UID, ownerKind, namespace string) ([]string, error) { + return []string{"podName"}, nil + }, + }, + wi: &klcv1alpha3.KeptnWorkloadInstance{ + Spec: klcv1alpha3.KeptnWorkloadInstanceSpec{ + KeptnWorkloadSpec: klcv1alpha3.KeptnWorkloadSpec{ + ResourceReference: klcv1alpha3.ResourceReference{ + Kind: "ReplicaSet", + }, + }, + }, + }, + wantErr: nil, + }, + { + name: "ReplicaSet, StatefulSet, DaemonSet - happy path - no pods found", + handler: SchedulingGatesHandler{ + getPods: func(ctx context.Context, c client.Client, ownerUID types.UID, ownerKind, namespace string) ([]string, error) { + return []string{}, nil + }, + }, + wi: &klcv1alpha3.KeptnWorkloadInstance{ + Spec: klcv1alpha3.KeptnWorkloadInstanceSpec{ + KeptnWorkloadSpec: klcv1alpha3.KeptnWorkloadSpec{ + ResourceReference: klcv1alpha3.ResourceReference{ + Kind: "ReplicaSet", + }, + }, + }, + }, + wantErr: nil, + }, + { + name: "ReplicaSet, StatefulSet, DaemonSet - happy path - err getPods", + handler: SchedulingGatesHandler{ + getPods: func(ctx context.Context, c client.Client, ownerUID types.UID, ownerKind, namespace string) ([]string, error) { + return []string{}, fmt.Errorf("err") + }, + }, + wi: &klcv1alpha3.KeptnWorkloadInstance{ + Spec: klcv1alpha3.KeptnWorkloadInstanceSpec{ + KeptnWorkloadSpec: klcv1alpha3.KeptnWorkloadSpec{ + ResourceReference: klcv1alpha3.ResourceReference{ + Kind: "ReplicaSet", + }, + }, + }, + }, + wantErr: fmt.Errorf("err"), + }, + { + name: "ReplicaSet, StatefulSet, DaemonSet - err removeGates", + handler: SchedulingGatesHandler{ + removeGates: func(ctx context.Context, c client.Client, podName, podNamespace string) error { + return fmt.Errorf("err") + }, + getPods: func(ctx context.Context, c client.Client, ownerUID types.UID, ownerKind, namespace string) ([]string, error) { + return []string{"podName"}, nil + }, + }, + wi: &klcv1alpha3.KeptnWorkloadInstance{ + Spec: klcv1alpha3.KeptnWorkloadInstanceSpec{ + KeptnWorkloadSpec: klcv1alpha3.KeptnWorkloadSpec{ + ResourceReference: klcv1alpha3.ResourceReference{ + Kind: "ReplicaSet", + }, + }, + }, + }, + wantErr: fmt.Errorf("err"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.handler.RemoveGates(context.TODO(), tt.wi) + require.Equal(t, tt.wantErr, err) + }) + } +} diff --git a/lifecycle-operator/controllers/lifecycle/keptnapp/controller.go b/lifecycle-operator/controllers/lifecycle/keptnapp/controller.go index f38a7a3a27..7804550c7d 100644 --- a/lifecycle-operator/controllers/lifecycle/keptnapp/controller.go +++ b/lifecycle-operator/controllers/lifecycle/keptnapp/controller.go @@ -184,7 +184,7 @@ func (r *KeptnAppReconciler) deprecateAppVersions(ctx context.Context, app *klcv lastResultErr = nil for i := app.Generation - 1; i > 0; i-- { deprecatedAppVersion := &klcv1alpha3.KeptnAppVersion{} - appVersionName := operatorcommon.CreateResourceName(common.MaxK8sObjectLength, common.MinKLTNameLen, app.Name, app.Spec.Version, common.Hash(i)) + appVersionName := operatorcommon.CreateResourceName(common.MaxK8sObjectLength, common.MinKeptnNameLen, app.Name, app.Spec.Version, common.Hash(i)) if err := r.Get(ctx, types.NamespacedName{Namespace: app.Namespace, Name: appVersionName}, deprecatedAppVersion); err != nil { if !errors.IsNotFound(err) { r.Log.Error(err, fmt.Sprintf("Could not get KeptnAppVersion: %s", appVersionName)) diff --git a/lifecycle-operator/controllers/lifecycle/keptnappversion/reconcile_workloadsstate.go b/lifecycle-operator/controllers/lifecycle/keptnappversion/reconcile_workloadsstate.go index 9ac81a6c46..e314bbebc1 100644 --- a/lifecycle-operator/controllers/lifecycle/keptnappversion/reconcile_workloadsstate.go +++ b/lifecycle-operator/controllers/lifecycle/keptnappversion/reconcile_workloadsstate.go @@ -85,5 +85,5 @@ func (r *KeptnAppVersionReconciler) handleUnaccessibleWorkloadInstanceList(ctx c } func getWorkloadInstanceName(appName string, workloadName string, version string) string { - return operatorcommon.CreateResourceName(apicommon.MaxK8sObjectLength, apicommon.MinKLTNameLen, appName, workloadName, version) + return operatorcommon.CreateResourceName(apicommon.MaxK8sObjectLength, apicommon.MinKeptnNameLen, appName, workloadName, version) } diff --git a/lifecycle-operator/controllers/lifecycle/keptnworkloadinstance/controller.go b/lifecycle-operator/controllers/lifecycle/keptnworkloadinstance/controller.go index ee887ec721..ce734b6c61 100644 --- a/lifecycle-operator/controllers/lifecycle/keptnworkloadinstance/controller.go +++ b/lifecycle-operator/controllers/lifecycle/keptnworkloadinstance/controller.go @@ -46,12 +46,13 @@ const traceComponentName = "keptn/lifecycle-operator/workloadinstance" // KeptnWorkloadInstanceReconciler reconciles a KeptnWorkloadInstance object type KeptnWorkloadInstanceReconciler struct { client.Client - Scheme *runtime.Scheme - EventSender controllercommon.IEvent - Log logr.Logger - Meters apicommon.KeptnMeters - SpanHandler *telemetry.SpanHandler - TracerFactory telemetry.TracerFactory + Scheme *runtime.Scheme + EventSender controllercommon.IEvent + Log logr.Logger + Meters apicommon.KeptnMeters + SpanHandler *telemetry.SpanHandler + TracerFactory telemetry.TracerFactory + SchedulingGatesHandler controllercommon.ISchedulingGatesHandler } // +kubebuilder:rbac:groups=lifecycle.keptn.sh,resources=keptnworkloadinstances,verbs=get;list;watch;create;update;patch;delete @@ -61,7 +62,7 @@ type KeptnWorkloadInstanceReconciler struct { // +kubebuilder:rbac:groups=lifecycle.keptn.sh,resources=keptntasks/status,verbs=get;update;patch // +kubebuilder:rbac:groups=lifecycle.keptn.sh,resources=keptntasks/finalizers,verbs=update // +kubebuilder:rbac:groups=core,resources=events,verbs=create;watch;patch -// +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch +// +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch;update // +kubebuilder:rbac:groups=apps,resources=replicasets;deployments;statefulsets;daemonsets,verbs=get;list;watch // +kubebuilder:rbac:groups=argoproj.io,resources=rollouts,verbs=get;list;watch @@ -71,7 +72,7 @@ type KeptnWorkloadInstanceReconciler struct { // For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.12.2/pkg/reconcile // -//nolint:gocyclo +//nolint:gocyclo,gocognit func (r *KeptnWorkloadInstanceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { r.Log.Info("Searching for KeptnWorkloadInstance") @@ -138,6 +139,14 @@ func (r *KeptnWorkloadInstanceReconciler) Reconcile(ctx context.Context, req ctr } } + if r.SchedulingGatesHandler.Enabled() { + // pre-evaluation checks done at this moment, we can remove the gate + if err := r.SchedulingGatesHandler.RemoveGates(ctx, workloadInstance); err != nil { + r.Log.Error(err, "could not remove SchedulingGates") + return ctrl.Result{Requeue: true, RequeueAfter: 10 * time.Second}, err + } + } + // Wait for deployment of Workload phase = apicommon.PhaseWorkloadDeployment if !workloadInstance.IsDeploymentSucceeded() { diff --git a/lifecycle-operator/controllers/lifecycle/keptnworkloadinstance/controller_test.go b/lifecycle-operator/controllers/lifecycle/keptnworkloadinstance/controller_test.go index 692c437bf8..582ac079f1 100644 --- a/lifecycle-operator/controllers/lifecycle/keptnworkloadinstance/controller_test.go +++ b/lifecycle-operator/controllers/lifecycle/keptnworkloadinstance/controller_test.go @@ -778,6 +778,11 @@ func TestKeptnWorkloadInstanceReconciler_ReconcileReachCompletion(t *testing.T) }, ) r, eventChannel, _ := setupReconciler(wi, app) + r.SchedulingGatesHandler = &fake.ISchedulingGatesHandlerMock{ + EnabledFunc: func() bool { + return false + }, + } req := ctrl.Request{ NamespacedName: types.NamespacedName{ @@ -807,6 +812,168 @@ func TestKeptnWorkloadInstanceReconciler_ReconcileReachCompletion(t *testing.T) } } +func TestKeptnWorkloadInstanceReconciler_ReconcileReachCompletion_SchedulingGates(t *testing.T) { + + testNamespace := "some-ns" + + wi := &klcv1alpha3.KeptnWorkloadInstance{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Name: "some-wi", + Namespace: testNamespace, + }, + Spec: klcv1alpha3.KeptnWorkloadInstanceSpec{ + KeptnWorkloadSpec: klcv1alpha3.KeptnWorkloadSpec{ + AppName: "some-app", + Version: "1.0.0", + }, + WorkloadName: "some-app-some-workload", + PreviousVersion: "", + TraceId: nil, + }, + Status: klcv1alpha3.KeptnWorkloadInstanceStatus{ + DeploymentStatus: apicommon.StateSucceeded, + PreDeploymentStatus: apicommon.StateSucceeded, + PostDeploymentStatus: apicommon.StateSucceeded, + PreDeploymentEvaluationStatus: apicommon.StateSucceeded, + PostDeploymentEvaluationStatus: apicommon.StateSucceeded, + CurrentPhase: apicommon.PhaseWorkloadPostEvaluation.ShortName, + Status: apicommon.StateSucceeded, + StartTime: metav1.Time{}, + EndTime: metav1.Time{}, + }, + } + + app := controllercommon.ReturnAppVersion( + testNamespace, + "some-app", + "1.0.0", + []klcv1alpha3.KeptnWorkloadRef{ + { + Name: "some-workload", + Version: "1.0.0", + }, + }, + klcv1alpha3.KeptnAppVersionStatus{ + PreDeploymentEvaluationStatus: apicommon.StateSucceeded, + }, + ) + + schedulingGatesMock := &fake.ISchedulingGatesHandlerMock{ + RemoveGatesFunc: func(ctx context.Context, workloadInstance *klcv1alpha3.KeptnWorkloadInstance) error { + return nil + }, + EnabledFunc: func() bool { + return true + }, + } + r, eventChannel, _ := setupReconciler(wi, app) + r.SchedulingGatesHandler = schedulingGatesMock + + req := ctrl.Request{ + NamespacedName: types.NamespacedName{ + Namespace: testNamespace, + Name: "some-wi", + }, + } + + result, err := r.Reconcile(context.TODO(), req) + + require.Len(t, schedulingGatesMock.RemoveGatesCalls(), 1) + require.Nil(t, err) + + // do not requeue since we reached completion + require.False(t, result.Requeue) + + // here we do not expect an event about the application preEvaluation being finished since that will have been sent in + // one of the previous reconciliation loops that lead to the first phase being reached + expectedEvents := []string{ + "CompletedFinished", + } + + for _, e := range expectedEvents { + select { + case event := <-eventChannel: + assert.Equal(t, strings.Contains(event, req.Name), true, "wrong workloadinstance") + assert.Equal(t, strings.Contains(event, req.Namespace), true, "wrong namespace") + assert.Equal(t, strings.Contains(event, e), true, fmt.Sprintf("no %s found in %s", e, event)) + case <-time.After(5 * time.Second): + t.Error("Didn't receive the cloud event") + } + } +} + +func TestKeptnWorkloadInstanceReconciler_RemoveGates_fail(t *testing.T) { + + testNamespace := "some-ns" + + wi := &klcv1alpha3.KeptnWorkloadInstance{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Name: "some-wi", + Namespace: testNamespace, + }, + Spec: klcv1alpha3.KeptnWorkloadInstanceSpec{ + KeptnWorkloadSpec: klcv1alpha3.KeptnWorkloadSpec{ + AppName: "some-app", + Version: "1.0.0", + }, + WorkloadName: "some-app-some-workload", + PreviousVersion: "", + TraceId: nil, + }, + Status: klcv1alpha3.KeptnWorkloadInstanceStatus{ + DeploymentStatus: apicommon.StateSucceeded, + PreDeploymentStatus: apicommon.StateSucceeded, + PostDeploymentStatus: apicommon.StateSucceeded, + PreDeploymentEvaluationStatus: apicommon.StateSucceeded, + PostDeploymentEvaluationStatus: apicommon.StateSucceeded, + CurrentPhase: apicommon.PhaseWorkloadPostEvaluation.ShortName, + Status: apicommon.StateSucceeded, + StartTime: metav1.Time{}, + EndTime: metav1.Time{}, + }, + } + + app := controllercommon.ReturnAppVersion( + testNamespace, + "some-app", + "1.0.0", + []klcv1alpha3.KeptnWorkloadRef{ + { + Name: "some-workload", + Version: "1.0.0", + }, + }, + klcv1alpha3.KeptnAppVersionStatus{ + PreDeploymentEvaluationStatus: apicommon.StateSucceeded, + }, + ) + r, _, _ := setupReconciler(wi, app) + r.SchedulingGatesHandler = &fake.ISchedulingGatesHandlerMock{ + RemoveGatesFunc: func(ctx context.Context, workloadInstance *klcv1alpha3.KeptnWorkloadInstance) error { + return fmt.Errorf("err") + }, + EnabledFunc: func() bool { + return true + }, + } + + req := ctrl.Request{ + NamespacedName: types.NamespacedName{ + Namespace: testNamespace, + Name: "some-wi", + }, + } + + result, err := r.Reconcile(context.TODO(), req) + + require.NotNil(t, err) + + // do not requeue since we reached completion + require.True(t, result.Requeue) +} + func TestKeptnWorkloadInstanceReconciler_ReconcileFailed(t *testing.T) { testNamespace := "some-ns" diff --git a/lifecycle-operator/go.mod b/lifecycle-operator/go.mod index f2dc6149f8..a45ca58a89 100644 --- a/lifecycle-operator/go.mod +++ b/lifecycle-operator/go.mod @@ -9,9 +9,9 @@ require ( github.com/cloudevents/sdk-go/v2 v2.14.0 github.com/go-logr/logr v1.2.4 github.com/kelseyhightower/envconfig v1.4.0 - github.com/keptn/lifecycle-toolkit/klt-cert-manager v0.0.0-20230920065432-f2f3a0c1e09c + github.com/keptn/lifecycle-toolkit/klt-cert-manager v0.0.0-20230926092913-099a4573b273 github.com/magiconair/properties v1.8.7 - github.com/onsi/ginkgo/v2 v2.12.0 + github.com/onsi/ginkgo/v2 v2.12.1 github.com/onsi/gomega v1.27.10 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.16.0 @@ -25,7 +25,8 @@ require ( go.opentelemetry.io/otel/sdk/metric v0.41.0 go.opentelemetry.io/otel/trace v1.18.0 golang.org/x/net v0.15.0 - google.golang.org/grpc v1.58.1 + gomodules.xyz/jsonpatch/v2 v2.4.0 + google.golang.org/grpc v1.58.2 k8s.io/api v0.28.2 k8s.io/apiextensions-apiserver v0.28.2 k8s.io/apimachinery v0.28.2 @@ -70,7 +71,7 @@ require ( github.com/prometheus/client_model v0.4.0 // indirect github.com/prometheus/common v0.44.0 // indirect github.com/prometheus/procfs v0.10.1 // indirect - github.com/spf13/afero v1.9.5 // indirect + github.com/spf13/afero v1.10.0 // indirect github.com/spf13/pflag v1.0.5 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.18.0 // indirect go.opentelemetry.io/proto/otlp v1.0.0 // indirect @@ -83,7 +84,6 @@ require ( golang.org/x/text v0.13.0 // indirect golang.org/x/time v0.3.0 // indirect golang.org/x/tools v0.12.0 // indirect - gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect diff --git a/lifecycle-operator/go.sum b/lifecycle-operator/go.sum index c8453e8c6f..9b063ccfb2 100644 --- a/lifecycle-operator/go.sum +++ b/lifecycle-operator/go.sum @@ -189,8 +189,8 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= -github.com/keptn/lifecycle-toolkit/klt-cert-manager v0.0.0-20230920065432-f2f3a0c1e09c h1:w+Q49s031wwWEdh6fvgzELQw7aOsoEsqIdNaJepIRn0= -github.com/keptn/lifecycle-toolkit/klt-cert-manager v0.0.0-20230920065432-f2f3a0c1e09c/go.mod h1:IMs51ScEWzHa5k0L7leYRNc0C/TghTHNRp+Go7o58Ko= +github.com/keptn/lifecycle-toolkit/klt-cert-manager v0.0.0-20230926092913-099a4573b273 h1:wytmV4pUahfCvjNfsYTpHAjRentNjMOwaOpcAz/6qbc= +github.com/keptn/lifecycle-toolkit/klt-cert-manager v0.0.0-20230926092913-099a4573b273/go.mod h1:GjJFB+g1DuBYrOXSgF4kzR9Phb+W5t3obFt4UHqz9OM= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= @@ -214,8 +214,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/onsi/ginkgo/v2 v2.12.0 h1:UIVDowFPwpg6yMUpPjGkYvf06K3RAiJXUhCxEwQVHRI= -github.com/onsi/ginkgo/v2 v2.12.0/go.mod h1:ZNEzXISYlqpb8S36iN71ifqLi3vVD1rVJGvWRCJOUpQ= +github.com/onsi/ginkgo/v2 v2.12.1 h1:uHNEO1RP2SpuZApSkel9nEh1/Mu+hmQe7Q+Pepg5OYA= +github.com/onsi/ginkgo/v2 v2.12.1/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -235,8 +235,8 @@ github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+Pymzi github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= -github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= +github.com/spf13/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY= +github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -566,8 +566,8 @@ google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.58.1 h1:OL+Vz23DTtrrldqHK49FUOPHyY75rvFqJfXC84NYW58= -google.golang.org/grpc v1.58.1/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= +google.golang.org/grpc v1.58.2 h1:SXUpjxeVF3FKrTYQI4f4KvbGD5u2xccdYdurwowix5I= +google.golang.org/grpc v1.58.2/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= diff --git a/lifecycle-operator/main.go b/lifecycle-operator/main.go index 2696070a62..82a8298edd 100644 --- a/lifecycle-operator/main.go +++ b/lifecycle-operator/main.go @@ -92,6 +92,8 @@ type envConfig struct { KeptnWorkloadInstanceControllerLogLevel int `envconfig:"KEPTN_WORKLOAD_INSTANCE_CONTROLLER_LOG_LEVEL" default:"0"` KeptnOptionsControllerLogLevel int `envconfig:"OPTIONS_CONTROLLER_LOG_LEVEL" default:"0"` + SchedulingGatesEnabled bool `envconfig:"SCHEDULING_GATES_ENABLED" default:"false"` + KeptnOptionsCollectorURL string `envconfig:"OTEL_COLLECTOR_URL" default:""` } @@ -259,13 +261,14 @@ func main() { workloadInstanceLogger := ctrl.Log.WithName("KeptnWorkloadInstance Controller").V(env.KeptnWorkloadInstanceControllerLogLevel) workloadInstanceRecorder := mgr.GetEventRecorderFor("keptnworkloadinstance-controller") workloadInstanceReconciler := &keptnworkloadinstance.KeptnWorkloadInstanceReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - Log: workloadInstanceLogger, - EventSender: controllercommon.NewEventMultiplexer(workloadInstanceLogger, workloadInstanceRecorder, ceClient), - Meters: keptnMeters, - TracerFactory: telemetry.GetOtelInstance(), - SpanHandler: spanHandler, + SchedulingGatesHandler: controllercommon.NewSchedulingGatesHandler(mgr.GetClient(), workloadInstanceLogger, env.SchedulingGatesEnabled), + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Log: workloadInstanceLogger, + EventSender: controllercommon.NewEventMultiplexer(workloadInstanceLogger, workloadInstanceRecorder, ceClient), + Meters: keptnMeters, + TracerFactory: telemetry.GetOtelInstance(), + SpanHandler: spanHandler, } if err = (workloadInstanceReconciler).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "KeptnWorkloadInstance") @@ -363,11 +366,12 @@ func main() { webhookBuilder.Register(mgr, map[string]*ctrlWebhook.Admission{ "/mutate-v1-pod": { Handler: &pod_mutator.PodMutatingWebhook{ - Client: mgr.GetClient(), - Tracer: otel.Tracer("keptn/webhook"), - EventSender: controllercommon.NewEventMultiplexer(webhookLogger, webhookRecorder, ceClient), - Decoder: admission.NewDecoder(mgr.GetScheme()), - Log: webhookLogger, + SchedulingGatesEnabled: env.SchedulingGatesEnabled, + Client: mgr.GetClient(), + Tracer: otel.Tracer("keptn/webhook"), + EventSender: controllercommon.NewEventMultiplexer(webhookLogger, webhookRecorder, ceClient), + Decoder: admission.NewDecoder(mgr.GetScheme()), + Log: webhookLogger, }, }, }) diff --git a/lifecycle-operator/test/component/workloadinstance/workloadinstance_suite_test.go b/lifecycle-operator/test/component/workloadinstance/workloadinstance_suite_test.go index 2d3bb0678b..111e38ddca 100644 --- a/lifecycle-operator/test/component/workloadinstance/workloadinstance_suite_test.go +++ b/lifecycle-operator/test/component/workloadinstance/workloadinstance_suite_test.go @@ -43,13 +43,14 @@ var _ = BeforeSuite(func() { // //setup controllers here config.Instance().SetDefaultNamespace(KeptnNamespace) controller := &keptnworkloadinstance.KeptnWorkloadInstanceReconciler{ - Client: k8sManager.GetClient(), - Scheme: k8sManager.GetScheme(), - EventSender: controllercommon.NewK8sSender(k8sManager.GetEventRecorderFor("test-workloadinstance-controller")), - Log: GinkgoLogr, - Meters: common.InitKeptnMeters(), - SpanHandler: &telemetry.SpanHandler{}, - TracerFactory: &common.TracerFactory{Tracer: tracer}, + SchedulingGatesHandler: controllercommon.NewSchedulingGatesHandler(nil, GinkgoLogr, false), + Client: k8sManager.GetClient(), + Scheme: k8sManager.GetScheme(), + EventSender: controllercommon.NewK8sSender(k8sManager.GetEventRecorderFor("test-workloadinstance-controller")), + Log: GinkgoLogr, + Meters: common.InitKeptnMeters(), + SpanHandler: &telemetry.SpanHandler{}, + TracerFactory: &common.TracerFactory{Tracer: tracer}, } Eventually(controller.SetupWithManager(k8sManager)).WithTimeout(30 * time.Second).WithPolling(time.Second).Should(Succeed()) close(readyToStart) diff --git a/lifecycle-operator/webhooks/pod_mutator/pod_mutating_webhook.go b/lifecycle-operator/webhooks/pod_mutator/pod_mutating_webhook.go index b43a3deae4..ab87375e3a 100644 --- a/lifecycle-operator/webhooks/pod_mutator/pod_mutating_webhook.go +++ b/lifecycle-operator/webhooks/pod_mutator/pod_mutating_webhook.go @@ -36,11 +36,12 @@ import ( // PodMutatingWebhook annotates Pods type PodMutatingWebhook struct { - Client client.Client - Tracer trace.Tracer - Decoder *admission.Decoder - EventSender controllercommon.IEvent - Log logr.Logger + Client client.Client + Tracer trace.Tracer + Decoder *admission.Decoder + EventSender controllercommon.IEvent + Log logr.Logger + SchedulingGatesEnabled bool } const InvalidAnnotationMessage = "Invalid annotations" @@ -81,7 +82,7 @@ func (a *PodMutatingWebhook) Handle(ctx context.Context, req admission.Request) return admission.Allowed("namespace is not enabled for lifecycle operator") } - // check the OwnerReference of the pod to see if it is supported and intended to be managed by KLT + // check the OwnerReference of the pod to see if it is supported and intended to be managed by Keptn ownerRef := a.getOwnerReference(pod.ObjectMeta) if ownerRef.Kind == "" { @@ -93,16 +94,28 @@ func (a *PodMutatingWebhook) Handle(ctx context.Context, req admission.Request) logger.Info(fmt.Sprintf("Pod annotations: %v", pod.Annotations)) podIsAnnotated := a.isPodAnnotated(pod) - logger.Info("Checked if pod is annotated.") - if !podIsAnnotated { logger.Info("Pod is not annotated, check for parent annotations...") podIsAnnotated = a.copyAnnotationsIfParentAnnotated(ctx, &req, pod) } if podIsAnnotated { - logger.Info("Resource is annotated with Keptn annotations, using Keptn scheduler") - pod.Spec.SchedulerName = "keptn-scheduler" + logger.Info("Resource is annotated with Keptn annotations") + if a.SchedulingGatesEnabled { + logger.Info("SchedulingGates enabled") + _, gateRemoved := getLabelOrAnnotation(&pod.ObjectMeta, apicommon.SchedulingGateRemoved, "") + if gateRemoved { + return admission.Allowed("gate of the pod already removed") + } + pod.Spec.SchedulingGates = []corev1.PodSchedulingGate{ + { + Name: apicommon.KeptnGate, + }, + } + } else { + logger.Info("SchedulingGates disabled, using keptn-scheduler") + pod.Spec.SchedulerName = "keptn-scheduler" + } logger.Info("Annotations", "annotations", pod.Annotations) isAppAnnotationPresent := a.isAppAnnotationPresent(pod) @@ -438,7 +451,7 @@ func (a *PodMutatingWebhook) generateAppCreationRequest(ctx context.Context, pod func (a *PodMutatingWebhook) getWorkloadName(pod *corev1.Pod) string { workloadName, _ := getLabelOrAnnotation(&pod.ObjectMeta, apicommon.WorkloadAnnotation, apicommon.K8sRecommendedWorkloadAnnotations) applicationName, _ := getLabelOrAnnotation(&pod.ObjectMeta, apicommon.AppAnnotation, apicommon.K8sRecommendedAppAnnotations) - return operatorcommon.CreateResourceName(apicommon.MaxK8sObjectLength, apicommon.MinKLTNameLen, applicationName, workloadName) + return operatorcommon.CreateResourceName(apicommon.MaxK8sObjectLength, apicommon.MinKeptnNameLen, applicationName, workloadName) } func (a *PodMutatingWebhook) getAppName(pod *corev1.Pod) string { diff --git a/lifecycle-operator/webhooks/pod_mutator/pod_mutating_webhook_test.go b/lifecycle-operator/webhooks/pod_mutator/pod_mutating_webhook_test.go index 433b5e0130..bd9a8cbdf8 100644 --- a/lifecycle-operator/webhooks/pod_mutator/pod_mutating_webhook_test.go +++ b/lifecycle-operator/webhooks/pod_mutator/pod_mutating_webhook_test.go @@ -14,6 +14,7 @@ import ( fakeclient "github.com/keptn/lifecycle-toolkit/lifecycle-operator/controllers/common/fake" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/trace" + "gomodules.xyz/jsonpatch/v2" admissionv1 "k8s.io/api/admission/v1" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -1207,6 +1208,204 @@ func TestPodMutatingWebhook_Handle_SingleService(t *testing.T) { }, workload.Spec) } +func TestPodMutatingWebhook_Handle_SchedulingGates_GateRemoved(t *testing.T) { + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "example-pod", + Namespace: "default", + Annotations: map[string]string{ + apicommon.WorkloadAnnotation: "my-workload", + apicommon.VersionAnnotation: "0.1", + apicommon.SchedulingGateRemoved: "true", + }, + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: "v1", + Kind: "Deployment", + Name: "my-deployment", + UID: "1234", + }, + }, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "example-container", + Image: "nginx", + }, + }, + }, + } + ns := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "default", + Annotations: map[string]string{ + apicommon.NamespaceEnabledAnnotation: "enabled", + }, + }, + } + fakeClient := fakeclient.NewClient(ns, pod) + + tr := &fakeclient.ITracerMock{StartFunc: func(ctx context.Context, spanName string, opts ...trace.SpanStartOption) (context.Context, trace.Span) { + return ctx, trace.SpanFromContext(ctx) + }} + + decoder := admission.NewDecoder(runtime.NewScheme()) + + wh := &PodMutatingWebhook{ + SchedulingGatesEnabled: true, + Client: fakeClient, + Tracer: tr, + Decoder: decoder, + EventSender: controllercommon.NewK8sSender(record.NewFakeRecorder(100)), + Log: testr.New(t), + } + + // Convert the Pod object to a byte array + podBytes, err := json.Marshal(pod) + require.Nil(t, err) + + // Create an AdmissionRequest object + request := admissionv1.AdmissionRequest{ + UID: "12345", + Kind: metav1.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"}, + Operation: admissionv1.Create, + Object: runtime.RawExtension{ + Raw: podBytes, + }, + Namespace: "default", + } + + resp := wh.Handle(context.TODO(), admission.Request{ + AdmissionRequest: request, + }) + + require.NotNil(t, resp) + require.True(t, resp.Allowed) + + // no changes to the pod are expected + require.Len(t, resp.Patches, 0) +} + +func TestPodMutatingWebhook_Handle_SchedulingGates(t *testing.T) { + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "example-pod", + Namespace: "default", + Annotations: map[string]string{ + apicommon.WorkloadAnnotation: "my-workload", + apicommon.VersionAnnotation: "0.1", + }, + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: "v1", + Kind: "Deployment", + Name: "my-deployment", + UID: "1234", + }, + }, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "example-container", + Image: "nginx", + }, + }, + }, + } + ns := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "default", + Annotations: map[string]string{ + apicommon.NamespaceEnabledAnnotation: "enabled", + }, + }, + } + fakeClient := fakeclient.NewClient(ns, pod) + + tr := &fakeclient.ITracerMock{StartFunc: func(ctx context.Context, spanName string, opts ...trace.SpanStartOption) (context.Context, trace.Span) { + return ctx, trace.SpanFromContext(ctx) + }} + + decoder := admission.NewDecoder(runtime.NewScheme()) + + wh := &PodMutatingWebhook{ + SchedulingGatesEnabled: true, + Client: fakeClient, + Tracer: tr, + Decoder: decoder, + EventSender: controllercommon.NewK8sSender(record.NewFakeRecorder(100)), + Log: testr.New(t), + } + + // Convert the Pod object to a byte array + podBytes, err := json.Marshal(pod) + require.Nil(t, err) + + // Create an AdmissionRequest object + request := admissionv1.AdmissionRequest{ + UID: "12345", + Kind: metav1.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"}, + Operation: admissionv1.Create, + Object: runtime.RawExtension{ + Raw: podBytes, + }, + Namespace: "default", + } + + resp := wh.Handle(context.TODO(), admission.Request{ + AdmissionRequest: request, + }) + + require.NotNil(t, resp) + require.True(t, resp.Allowed) + + op := jsonpatch.Operation{ + Operation: "add", + Path: "/spec/schedulingGates", + Value: []interface{}{map[string]interface{}{"name": apicommon.KeptnGate}}, + } + + require.Len(t, resp.Patches, 2) + if resp.Patches[0].Path == "/spec/schedulingGates" { + require.Equal(t, op, resp.Patches[0]) + } else { + require.Equal(t, op, resp.Patches[1]) + } + + kacr := &klcv1alpha3.KeptnAppCreationRequest{} + + err = fakeClient.Get(context.Background(), types.NamespacedName{ + Namespace: "default", + Name: "my-workload", + }, kacr) + + require.Nil(t, err) + + require.Equal(t, "my-workload", kacr.Spec.AppName) + require.Equal(t, string(apicommon.AppTypeSingleService), kacr.Annotations[apicommon.AppTypeAnnotation]) + + workload := &klcv1alpha3.KeptnWorkload{} + + err = fakeClient.Get(context.TODO(), types.NamespacedName{ + Namespace: "default", + Name: "my-workload-my-workload", + }, workload) + + require.Nil(t, err) + + require.Equal(t, klcv1alpha3.KeptnWorkloadSpec{ + AppName: kacr.Spec.AppName, + Version: "0.1", + ResourceReference: klcv1alpha3.ResourceReference{ + UID: "1234", + Kind: "Deployment", + Name: "my-deployment", + }, + }, workload.Spec) +} + func TestPodMutatingWebhook_Handle_SingleService_AppCreationRequestAlreadyPresent(t *testing.T) { fakeClient := fakeclient.NewClient(&corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ diff --git a/metrics-operator/go.mod b/metrics-operator/go.mod index 9b070128c9..da9fe3afc7 100644 --- a/metrics-operator/go.mod +++ b/metrics-operator/go.mod @@ -8,10 +8,11 @@ require ( github.com/go-logr/logr v1.2.4 github.com/gorilla/mux v1.8.0 github.com/kelseyhightower/envconfig v1.4.0 - github.com/keptn/lifecycle-toolkit/klt-cert-manager v0.0.0-20230920065432-f2f3a0c1e09c + github.com/keptn/lifecycle-toolkit/klt-cert-manager v0.0.0-20230926092913-099a4573b273 github.com/open-feature/go-sdk v1.7.0 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.16.0 + github.com/prometheus/client_model v0.4.0 github.com/prometheus/common v0.44.0 github.com/stretchr/testify v1.8.4 golang.org/x/exp v0.0.0-20230905200255-921286631fa9 @@ -73,9 +74,8 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_model v0.4.0 // indirect github.com/prometheus/procfs v0.10.1 // indirect - github.com/spf13/afero v1.9.5 // indirect + github.com/spf13/afero v1.10.0 // indirect github.com/spf13/cobra v1.7.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stoewer/go-strcase v1.2.0 // indirect diff --git a/metrics-operator/go.sum b/metrics-operator/go.sum index 2280dfaa3c..15135711cf 100644 --- a/metrics-operator/go.sum +++ b/metrics-operator/go.sum @@ -239,8 +239,8 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= -github.com/keptn/lifecycle-toolkit/klt-cert-manager v0.0.0-20230920065432-f2f3a0c1e09c h1:w+Q49s031wwWEdh6fvgzELQw7aOsoEsqIdNaJepIRn0= -github.com/keptn/lifecycle-toolkit/klt-cert-manager v0.0.0-20230920065432-f2f3a0c1e09c/go.mod h1:IMs51ScEWzHa5k0L7leYRNc0C/TghTHNRp+Go7o58Ko= +github.com/keptn/lifecycle-toolkit/klt-cert-manager v0.0.0-20230926092913-099a4573b273 h1:wytmV4pUahfCvjNfsYTpHAjRentNjMOwaOpcAz/6qbc= +github.com/keptn/lifecycle-toolkit/klt-cert-manager v0.0.0-20230926092913-099a4573b273/go.mod h1:GjJFB+g1DuBYrOXSgF4kzR9Phb+W5t3obFt4UHqz9OM= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= @@ -289,8 +289,8 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= -github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= +github.com/spf13/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY= +github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= diff --git a/runtimes/deno-runtime/Dockerfile b/runtimes/deno-runtime/Dockerfile index a1e37311c2..ee81e8295f 100644 --- a/runtimes/deno-runtime/Dockerfile +++ b/runtimes/deno-runtime/Dockerfile @@ -1,4 +1,4 @@ -FROM denoland/deno:alpine-1.36.4 AS production +FROM denoland/deno:alpine-1.37.0 AS production LABEL org.opencontainers.image.source="https://github.com/keptn/lifecycle-toolkit" \ org.opencontainers.image.url="https://keptn.sh" \ diff --git a/scheduler/go.mod b/scheduler/go.mod index fec868b1a3..ab2215d6b1 100644 --- a/scheduler/go.mod +++ b/scheduler/go.mod @@ -4,7 +4,7 @@ go 1.20 require ( github.com/kelseyhightower/envconfig v1.4.0 - github.com/onsi/ginkgo/v2 v2.12.0 + github.com/onsi/ginkgo/v2 v2.12.1 github.com/onsi/gomega v1.27.10 github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.8.4 @@ -13,7 +13,7 @@ require ( go.opentelemetry.io/otel/exporters/stdout v0.20.0 go.opentelemetry.io/otel/sdk v0.20.0 go.opentelemetry.io/otel/trace v0.20.0 - google.golang.org/grpc v1.58.1 + google.golang.org/grpc v1.58.2 k8s.io/api v0.25.14 k8s.io/apimachinery v0.25.14 k8s.io/apiserver v0.25.14 diff --git a/scheduler/go.sum b/scheduler/go.sum index 16145381e6..27661f0bd5 100644 --- a/scheduler/go.sum +++ b/scheduler/go.sum @@ -272,8 +272,8 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWb github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= -github.com/onsi/ginkgo/v2 v2.12.0 h1:UIVDowFPwpg6yMUpPjGkYvf06K3RAiJXUhCxEwQVHRI= -github.com/onsi/ginkgo/v2 v2.12.0/go.mod h1:ZNEzXISYlqpb8S36iN71ifqLi3vVD1rVJGvWRCJOUpQ= +github.com/onsi/ginkgo/v2 v2.12.1 h1:uHNEO1RP2SpuZApSkel9nEh1/Mu+hmQe7Q+Pepg5OYA= +github.com/onsi/ginkgo/v2 v2.12.1/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= @@ -658,8 +658,8 @@ google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTp google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.58.1 h1:OL+Vz23DTtrrldqHK49FUOPHyY75rvFqJfXC84NYW58= -google.golang.org/grpc v1.58.1/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= +google.golang.org/grpc v1.58.2 h1:SXUpjxeVF3FKrTYQI4f4KvbGD5u2xccdYdurwowix5I= +google.golang.org/grpc v1.58.2/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= diff --git a/test/integration/analysis-controller-existing-status/00-assert.yaml b/test/testanalysis/analysis-controller-existing-status/00-assert.yaml similarity index 100% rename from test/integration/analysis-controller-existing-status/00-assert.yaml rename to test/testanalysis/analysis-controller-existing-status/00-assert.yaml diff --git a/test/integration/analysis-controller-existing-status/00-install.yaml b/test/testanalysis/analysis-controller-existing-status/00-install.yaml similarity index 100% rename from test/integration/analysis-controller-existing-status/00-install.yaml rename to test/testanalysis/analysis-controller-existing-status/00-install.yaml diff --git a/test/integration/analysis-controller-existing-status/01-assert.yaml b/test/testanalysis/analysis-controller-existing-status/01-assert.yaml similarity index 100% rename from test/integration/analysis-controller-existing-status/01-assert.yaml rename to test/testanalysis/analysis-controller-existing-status/01-assert.yaml diff --git a/test/integration/analysis-controller-existing-status/01-install.yaml b/test/testanalysis/analysis-controller-existing-status/01-install.yaml similarity index 100% rename from test/integration/analysis-controller-existing-status/01-install.yaml rename to test/testanalysis/analysis-controller-existing-status/01-install.yaml diff --git a/test/integration/analysis-controller-existing-status/02-namespacedelete.yaml b/test/testanalysis/analysis-controller-existing-status/02-namespacedelete.yaml similarity index 100% rename from test/integration/analysis-controller-existing-status/02-namespacedelete.yaml rename to test/testanalysis/analysis-controller-existing-status/02-namespacedelete.yaml diff --git a/test/integration/analysis-controller-multiple-providers/00-teststep-template.yaml b/test/testanalysis/analysis-controller-multiple-providers/00-teststep-template.yaml similarity index 100% rename from test/integration/analysis-controller-multiple-providers/00-teststep-template.yaml rename to test/testanalysis/analysis-controller-multiple-providers/00-teststep-template.yaml diff --git a/test/integration/analysis-controller-multiple-providers/01-assert.yaml b/test/testanalysis/analysis-controller-multiple-providers/01-assert.yaml similarity index 100% rename from test/integration/analysis-controller-multiple-providers/01-assert.yaml rename to test/testanalysis/analysis-controller-multiple-providers/01-assert.yaml diff --git a/test/integration/analysis-controller-multiple-providers/install.yaml b/test/testanalysis/analysis-controller-multiple-providers/install.yaml similarity index 100% rename from test/integration/analysis-controller-multiple-providers/install.yaml rename to test/testanalysis/analysis-controller-multiple-providers/install.yaml diff --git a/test/integration/analysis-controller-multiple-providers/mock-server.yaml b/test/testanalysis/analysis-controller-multiple-providers/mock-server.yaml similarity index 100% rename from test/integration/analysis-controller-multiple-providers/mock-server.yaml rename to test/testanalysis/analysis-controller-multiple-providers/mock-server.yaml diff --git a/test/integration/analysis-controller-with-duration-timeframe/00-teststep-template.yaml b/test/testanalysis/analysis-controller-with-duration-timeframe/00-teststep-template.yaml similarity index 100% rename from test/integration/analysis-controller-with-duration-timeframe/00-teststep-template.yaml rename to test/testanalysis/analysis-controller-with-duration-timeframe/00-teststep-template.yaml diff --git a/test/integration/analysis-controller-with-duration-timeframe/01-assert.yaml b/test/testanalysis/analysis-controller-with-duration-timeframe/01-assert.yaml similarity index 100% rename from test/integration/analysis-controller-with-duration-timeframe/01-assert.yaml rename to test/testanalysis/analysis-controller-with-duration-timeframe/01-assert.yaml diff --git a/test/integration/analysis-controller-with-duration-timeframe/install.yaml b/test/testanalysis/analysis-controller-with-duration-timeframe/install.yaml similarity index 100% rename from test/integration/analysis-controller-with-duration-timeframe/install.yaml rename to test/testanalysis/analysis-controller-with-duration-timeframe/install.yaml diff --git a/test/integration/analysis-controller-with-duration-timeframe/mock-server.yaml b/test/testanalysis/analysis-controller-with-duration-timeframe/mock-server.yaml similarity index 100% rename from test/integration/analysis-controller-with-duration-timeframe/mock-server.yaml rename to test/testanalysis/analysis-controller-with-duration-timeframe/mock-server.yaml diff --git a/test/integration/analysis-controller/00-teststep-template.yaml b/test/testanalysis/analysis-controller/00-teststep-template.yaml similarity index 100% rename from test/integration/analysis-controller/00-teststep-template.yaml rename to test/testanalysis/analysis-controller/00-teststep-template.yaml diff --git a/test/integration/analysis-controller/01-assert.yaml b/test/testanalysis/analysis-controller/01-assert.yaml similarity index 100% rename from test/integration/analysis-controller/01-assert.yaml rename to test/testanalysis/analysis-controller/01-assert.yaml diff --git a/test/integration/analysis-controller/install.yaml b/test/testanalysis/analysis-controller/install.yaml similarity index 100% rename from test/integration/analysis-controller/install.yaml rename to test/testanalysis/analysis-controller/install.yaml diff --git a/test/integration/analysis-controller/mock-server.yaml b/test/testanalysis/analysis-controller/mock-server.yaml similarity index 100% rename from test/integration/analysis-controller/mock-server.yaml rename to test/testanalysis/analysis-controller/mock-server.yaml diff --git a/test/integration/analysis-resources/00-teststep-install.yaml b/test/testanalysis/analysis-resources/00-teststep-install.yaml similarity index 100% rename from test/integration/analysis-resources/00-teststep-install.yaml rename to test/testanalysis/analysis-resources/00-teststep-install.yaml diff --git a/test/integration/analysis-resources/01-teststep-assert.yaml b/test/testanalysis/analysis-resources/01-teststep-assert.yaml similarity index 100% rename from test/integration/analysis-resources/01-teststep-assert.yaml rename to test/testanalysis/analysis-resources/01-teststep-assert.yaml diff --git a/test/integration/analysis-resources/invalid-analysis-1.yaml b/test/testanalysis/analysis-resources/invalid-analysis-1.yaml similarity index 100% rename from test/integration/analysis-resources/invalid-analysis-1.yaml rename to test/testanalysis/analysis-resources/invalid-analysis-1.yaml diff --git a/test/integration/analysis-resources/invalid-analysis-2.yaml b/test/testanalysis/analysis-resources/invalid-analysis-2.yaml similarity index 100% rename from test/integration/analysis-resources/invalid-analysis-2.yaml rename to test/testanalysis/analysis-resources/invalid-analysis-2.yaml diff --git a/test/integration/analysis-resources/invalid-analysis-3.yaml b/test/testanalysis/analysis-resources/invalid-analysis-3.yaml similarity index 100% rename from test/integration/analysis-resources/invalid-analysis-3.yaml rename to test/testanalysis/analysis-resources/invalid-analysis-3.yaml diff --git a/test/integration/analysis-resources/valid-analysis-1.yaml b/test/testanalysis/analysis-resources/valid-analysis-1.yaml similarity index 100% rename from test/integration/analysis-resources/valid-analysis-1.yaml rename to test/testanalysis/analysis-resources/valid-analysis-1.yaml diff --git a/test/integration/analysis-resources/valid-analysis-2.yaml b/test/testanalysis/analysis-resources/valid-analysis-2.yaml similarity index 100% rename from test/integration/analysis-resources/valid-analysis-2.yaml rename to test/testanalysis/analysis-resources/valid-analysis-2.yaml diff --git a/test/integration/analysis-value-template/00-assert.yaml b/test/testanalysis/analysis-value-template/00-assert.yaml similarity index 100% rename from test/integration/analysis-value-template/00-assert.yaml rename to test/testanalysis/analysis-value-template/00-assert.yaml diff --git a/test/integration/analysis-value-template/00-install.yaml b/test/testanalysis/analysis-value-template/00-install.yaml similarity index 100% rename from test/integration/analysis-value-template/00-install.yaml rename to test/testanalysis/analysis-value-template/00-install.yaml diff --git a/test/testanalysis/analysisdefinition-validate/00-teststep-install.yaml b/test/testanalysis/analysisdefinition-validate/00-teststep-install.yaml new file mode 100644 index 0000000000..394ecab36c --- /dev/null +++ b/test/testanalysis/analysisdefinition-validate/00-teststep-install.yaml @@ -0,0 +1,7 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +apply: + - goodanalysis.yaml +commands: + - command: kubectl apply -f badanalysis.yaml + ignoreFailure: true # we must install ignoring the validating webhook error to proceed with the test diff --git a/test/testanalysis/analysisdefinition-validate/01-teststep-assert.yaml b/test/testanalysis/analysisdefinition-validate/01-teststep-assert.yaml new file mode 100644 index 0000000000..d18161645e --- /dev/null +++ b/test/testanalysis/analysisdefinition-validate/01-teststep-assert.yaml @@ -0,0 +1,6 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +error: # this checks that kubectl get resource fails, AKA bad CRD not added + - badanalysis.yaml +assert: # this checks that kubectl get resource succeeds + - goodanalysis.yaml diff --git a/test/testanalysis/analysisdefinition-validate/badanalysis.yaml b/test/testanalysis/analysisdefinition-validate/badanalysis.yaml new file mode 100644 index 0000000000..66678efdf0 --- /dev/null +++ b/test/testanalysis/analysisdefinition-validate/badanalysis.yaml @@ -0,0 +1,20 @@ +apiVersion: metrics.keptn.sh/v1alpha3 +kind: AnalysisDefinition +metadata: + name: ed-my-proj-dev-svc1 +spec: + objectives: + - analysisValueTemplateRef: + name: ready + target: + failure: + lessThan: + fixedValue: 2 + warning: + lessThan: + fixedValue: 3 + weight: 1 + keyObjective: false + totalScore: + passPercentage: 50 + warningPercentage: 75 diff --git a/test/testanalysis/analysisdefinition-validate/goodanalysis.yaml b/test/testanalysis/analysisdefinition-validate/goodanalysis.yaml new file mode 100644 index 0000000000..f1a51dae53 --- /dev/null +++ b/test/testanalysis/analysisdefinition-validate/goodanalysis.yaml @@ -0,0 +1,20 @@ +apiVersion: metrics.keptn.sh/v1alpha3 +kind: AnalysisDefinition +metadata: + name: ed-my-proj-dev-svc2 +spec: + objectives: + - analysisValueTemplateRef: + name: ready + target: + failure: + lessThan: + fixedValue: 2 + warning: + lessThan: + fixedValue: 3 + weight: 1 + keyObjective: false + totalScore: + passPercentage: 90 + warningPercentage: 75