diff --git a/CHANGELOG.md b/CHANGELOG.md index c9270d6c..35048531 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,17 +8,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 *Breaking Change ahead!* +### Added +- [#121] Added use case to check if dogus actually use the desired version and config before completing the blueprint + ### Changed - [#119] *breaking* sensitive dogu config can now only be referenced with secrets - - it was not safe to have these values in clear text in the blueprint + - it was not safe to have these values in clear text in the blueprint - [#119] we now support blueprint v2 CRs -- [#121] health checks before and after applying the blueprint are now non-blocking +- [#121] all health checks are now non-blocking +- [#121] there are in general no steps anymore, which will block the reconciliation loop beyond some HTTP-Requests +- [#121] *breaking* blueprints will now be executed as a continuous process + - the operator will now detect changes and will enforce the content of the blueprint +- [#121] *breaking* the current state will now be reflected via conditions instead of the `statusPhase` field +- [#121] *breaking* events were reworked, some events are now more general, some events got removed completely + - Note, that events are for humans. You should not compute them for automation as they have no consistency guarantees. +- [#121] Upgrade to Golang v1.25.1 +- [#121] Upgrade Makefiles to v10.4.0 +- [#121] *breaking* merge proxy config dogu action into one to simplify the status ### Removed - [#119] *breaking* no support for v1 blueprint CRs anymore - - make sure to persist your blueprints before upgrading - - you need to transform your blueprints to the new v2 format yourself + - make sure to persist your blueprints before upgrading + - you need to transform your blueprints to the new v2 format yourself - [#121] remove maintenance mode + - remove dependency to k8s-service-discovery (maintenance-mode was the reason for this dependency) +- [#121] *breaking* dogus will not be restarted by the blueprint operator anymore + - this is now the responsibility of the dogu operator +- [#121] *breaking* remove the ability to apply components (including self upgrade) + - Ecosystem-Core-Chart has now the resonsibility of components +- [#121] *breaking* remove watching the health state of components in the ecosystem + +## [v2.8.0] - 2025-09-15 +### Changed +- [#125] ignore nginx dependencies + +### Removed +- [#125] component-dependency for component-oprator-crd + - it is replaced by a helm capabilities-check for the component CRD ## [v2.7.0] - 2025-07-17 ### Fixed diff --git a/Dockerfile b/Dockerfile index 4304dedb..912006b1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build the manager binary -FROM golang:1.24.3 AS builder +FROM golang:1.25.1 AS builder WORKDIR /workspace @@ -34,7 +34,7 @@ RUN make compile-generic FROM gcr.io/distroless/static:nonroot LABEL maintainer="hello@cloudogu.com" \ NAME="k8s-blueprint-operator" \ - VERSION="2.7.0" + VERSION="2.8.0" WORKDIR / COPY --from=builder /workspace/target/k8s-blueprint-operator . diff --git a/Jenkinsfile b/Jenkinsfile index 95a40315..289a80f1 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -12,9 +12,11 @@ github = new GitHub(this, git) changelog = new Changelog(this) Docker docker = new Docker(this) gpg = new Gpg(this, docker) -goVersion = "1.24.3" +goVersion = "1.25.1" Makefile makefile = new Makefile(this) +componentOperatorVersion="1.10.0" + // Configuration of repository repositoryOwner = "cloudogu" repositoryName = "k8s-blueprint-operator" @@ -109,6 +111,11 @@ node('docker') { } stage('Deploy Manager') { + withCredentials([[$class: 'UsernamePasswordMultiBinding', credentialsId: 'harborhelmchartpush', usernameVariable: 'HARBOR_USERNAME', passwordVariable: 'HARBOR_PASSWORD']]) { + k3d.helm("registry login ${registry} --username '${HARBOR_USERNAME}' --password '${HARBOR_PASSWORD}'") + k3d.helm("install k8s-component-operator-crd oci://${registry}/k8s/k8s-component-operator-crd --version ${componentOperatorVersion}") + k3d.helm("registry logout ${registry}") + } k3d.helm("install ${repositoryName} ${helmChartDir}") } diff --git a/Makefile b/Makefile index 02b5d871..7358960a 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,9 @@ # Set these to the desired values ARTIFACT_ID=k8s-blueprint-operator -VERSION=2.7.0 +VERSION=2.8.0 IMAGE=cloudogu/${ARTIFACT_ID}:${VERSION} -GOTAG=1.24.3 -MAKEFILES_VERSION=10.2.0 +GOTAG=1.25.1 +MAKEFILES_VERSION=10.4.0 STAGE?=production diff --git a/build/make/bats.mk b/build/make/bats.mk index 7e73553d..23680444 100644 --- a/build/make/bats.mk +++ b/build/make/bats.mk @@ -9,7 +9,7 @@ BATS_SUPPORT=$(BATS_LIBRARY_DIR)/bats-support BATS_FILE=$(BATS_LIBRARY_DIR)/bats-file BATS_BASE_IMAGE?=bats/bats BATS_CUSTOM_IMAGE?=cloudogu/bats -BATS_TAG?=1.11.0 +BATS_TAG?=1.12.0 BATS_DIR=build/make/bats BATS_WORKDIR="${WORKDIR}"/"${BATS_DIR}" @@ -18,15 +18,19 @@ unit-test-shell: unit-test-shell-$(ENVIRONMENT) $(BATS_ASSERT): @git clone --depth 1 https://github.com/bats-core/bats-assert $@ + @rm -rf $@/.git $(BATS_MOCK): @git clone --depth 1 https://github.com/grayhemp/bats-mock $@ + @rm -rf $@/.git $(BATS_SUPPORT): @git clone --depth 1 https://github.com/bats-core/bats-support $@ + @rm -rf $@/.git $(BATS_FILE): @git clone --depth 1 https://github.com/bats-core/bats-file $@ + @rm -rf $@/.git $(BASH_SRC): BASH_SRC:=$(shell find "${WORKDIR}" -type f -name "*.sh") @@ -49,10 +53,10 @@ unit-test-shell-local: $(BASH_SRC) $(PASSWD) $(ETCGROUP) $(HOME_DIR) buildTestIm "${BATS_DIR}"/customBatsEntrypoint.sh make unit-test-shell-generic-no-junit unit-test-shell-generic: - @bats --formatter junit --output ${BASH_TEST_REPORT_DIR} ${TESTS_DIR} + @bats --report-formatter junit --formatter junit --output ${BASH_TEST_REPORT_DIR} ${TESTS_DIR} unit-test-shell-generic-no-junit: - @bats ${TESTS_DIR} + @bats --report-formatter junit --output ${BASH_TEST_REPORT_DIR} ${TESTS_DIR} .PHONY buildTestImage: buildTestImage: diff --git a/build/make/bats/Dockerfile b/build/make/bats/Dockerfile index 7167a941..95a27875 100644 --- a/build/make/bats/Dockerfile +++ b/build/make/bats/Dockerfile @@ -1,7 +1,7 @@ ARG BATS_BASE_IMAGE ARG BATS_TAG -FROM ${BATS_BASE_IMAGE:-bats/bats}:${BATS_TAG:-1.11.0} +FROM ${BATS_BASE_IMAGE:-bats/bats}:${BATS_TAG:-1.12.0} # Make bash more findable by scripts and tests RUN apk add make git bash diff --git a/build/make/bats/customBatsEntrypoint.sh b/build/make/bats/customBatsEntrypoint.sh index 58856fe3..bfc192ba 100755 --- a/build/make/bats/customBatsEntrypoint.sh +++ b/build/make/bats/customBatsEntrypoint.sh @@ -3,4 +3,11 @@ set -o errexit set -o nounset set -o pipefail -"$@" \ No newline at end of file +targetReportDir="${PWD}"/target/shell_test_reports +uidgid=1000:1000 +exitcode=0 +"$@" || exitcode=$? +echo "Resetting file ownership to ${uidgid} in ${targetReportDir}/" +chown -R ${uidgid} "${targetReportDir}"/* +echo "exiting with code ${exitcode}" +exit ${exitcode} \ No newline at end of file diff --git a/build/make/build.mk b/build/make/build.mk index a29f2c5b..c2f512ce 100644 --- a/build/make/build.mk +++ b/build/make/build.mk @@ -3,7 +3,7 @@ ADDITIONAL_LDFLAGS?=-extldflags -static LDFLAGS?=-ldflags "$(ADDITIONAL_LDFLAGS) -X main.Version=$(VERSION) -X main.CommitID=$(COMMIT_ID)" GOIMAGE?=golang -GOTAG?=1.24 +GOTAG?=1.25 GOOS?=linux GOARCH?=amd64 PRE_COMPILE?= diff --git a/build/make/k8s.mk b/build/make/k8s.mk index a46ff79c..162abb69 100644 --- a/build/make/k8s.mk +++ b/build/make/k8s.mk @@ -11,7 +11,7 @@ BINARY_YQ_4_VERSION?=v4.40.3 BINARY_HELM = $(UTILITY_BIN_PATH)/helm BINARY_HELM_VERSION?=v3.13.0 CONTROLLER_GEN = $(UTILITY_BIN_PATH)/controller-gen -CONTROLLER_GEN_VERSION?=v0.14.0 +CONTROLLER_GEN_VERSION?=v0.19.0 # Setting SHELL to bash allows bash commands to be executed by recipes. # Options are set to exit when a recipe line exits non-zero or a piped command fails. diff --git a/build/make/static-analysis.mk b/build/make/static-analysis.mk index 0989b74d..262bc0d2 100644 --- a/build/make/static-analysis.mk +++ b/build/make/static-analysis.mk @@ -2,12 +2,12 @@ STATIC_ANALYSIS_DIR=$(TARGET_DIR)/static-analysis GOIMAGE?=golang -GOTAG?=1.24 +GOTAG?=1.25 CUSTOM_GO_MOUNT?=-v /tmp:/tmp REVIEW_DOG=$(TMP_DIR)/bin/reviewdog LINT=$(TMP_DIR)/bin/golangci-lint -LINT_VERSION?=v2.1.6 +LINT_VERSION?=v2.5.0 # ignore tests and mocks LINTFLAGS=--tests=false --timeout 10m --issues-exit-code 0 ADDITIONAL_LINTER=-E bodyclose -E containedctx -E contextcheck -E decorder -E dupl -E errname -E forcetypeassert -E funlen -E unparam diff --git a/docs/operations/apply_blueprints_de.md b/docs/operations/apply_blueprints_de.md index 165972ef..c7088e2b 100644 --- a/docs/operations/apply_blueprints_de.md +++ b/docs/operations/apply_blueprints_de.md @@ -3,35 +3,31 @@ Sie können einen Blueprint anwenden, indem Sie eine `Blueprint`-Ressource auf den Cluster-Namespace anwenden, in dem das Cloudogu MultiNode EcoSystem läuft: ```yaml -apiVersion: k8s.cloudogu.com/v1 +apiVersion: k8s.cloudogu.com/v2 kind: Blueprint metadata: + labels: + app: ces + app.kubernetes.io/name: k8s-blueprint-lib name: my-blueprint spec: - # fügen Sie die blueprint.json hier ein - blueprint: | - { - "blueprintApi": "v2", - "dogus": [ ... ], - "components": [ ... ], - "config": { - "global": { ... }, - "dogus": { ... } - } - } - # fügen Sie hier die blueprint-mask.json ein - blueprintMask: | - { - "blueprintMaskApi": "v1", - "blueprintMaskId": "my-blueprint-mask", - "dogus": [ ... ] - } + displayName: "Blueprint Sample v6.834" + # fügen Sie die blueprint hier ein + blueprint: + dogus: ... + config: + global: ... + dogus: ... + # fügen Sie hier die blueprint-mask ein + blueprintMask: + dogus: ... ``` Das Dokument [Blueprint-Format](https://github.com/cloudogu/k8s-blueprint-lib/blob/develop/docs/operations/blueprintV2_format_de.md) beschreibt die Struktur des Blueprint im Detail. Blueprint-CR-Beispiele können dem [Sample-Repository](https://github.com/cloudogu/k8s-ecosystem-samples/tree/main/blueprints) entnommen werden. Wenn `k8s-blueprint-operator` korrekt installiert wurde, lässt sich dies z. B. so auf den Cluster anwenden: ```bash -kubectl apply -n ecosystem -f k8s_v1_blueprint.yaml +kubectl apply -n ecosystem -f k8s_v2_blueprint.yaml ``` +**Hinweis:** Pro Namespace ist nur ein Blueprint zulässig. Ändern Sie entweder das vorhandene Blueprint oder wenden Sie erneut ein `kubectl apply` mit demselben Blueprint-Namen an, um es zu aktualisieren. diff --git a/docs/operations/apply_blueprints_en.md b/docs/operations/apply_blueprints_en.md index 77ef453f..0e08b906 100644 --- a/docs/operations/apply_blueprints_en.md +++ b/docs/operations/apply_blueprints_en.md @@ -3,33 +3,30 @@ You can apply a blueprint by applying a `Blueprint` resource to the cluster namespace where the Cloudogu MultiNode EcoSystem is running in: ```yaml -apiVersion: k8s.cloudogu.com/v1 +apiVersion: k8s.cloudogu.com/v2 kind: Blueprint metadata: + labels: + app: ces + app.kubernetes.io/name: k8s-blueprint-lib name: my-blueprint spec: - # put your blueprint.json here - blueprint: | - { - "blueprintApi": "v2", - "dogus": [ ... ], - "components": [ ... ], - "config": { - "global": { ... }, - "dogus": { ... } - } - } - # put your blueprint-mask.json here - blueprintMask: | - { - "blueprintMaskApi": "v1", - "blueprintMaskId": "my-blueprint-mask", - "dogus": [ ... ] - } + displayName: "Blueprint Sample v6.834" + # put your blueprint here + blueprint: + dogus: ... + config: + global: ... + dogus: ... + # put your blueprint-mask here + blueprintMask: + dogus: ... ``` The document [blueprint format](https://github.com/cloudogu/k8s-blueprint-lib/blob/develop/docs/operations/blueprintV2_format_en.md) describes the structure of the Blueprint in detail. You may see examples of Blueprint-CRs in the [sample repository](https://github.com/cloudogu/k8s-ecosystem-samples/tree/main/blueprints). With `k8s-blueprint-operator` properly being installed, you can apply it to the cluster like this: ```bash -kubectl apply -n ecosystem -f k8s_v1_blueprint.yaml +kubectl apply -n ecosystem -f k8s_v2_blueprint.yaml ``` + +**Note:** Only one blueprint is permitted per namespace. Either change the existing one or apply with the same name to update it. diff --git a/docs/operations/health_checks_de.md b/docs/operations/health_checks_de.md index 990c2af8..28c1d06b 100644 --- a/docs/operations/health_checks_de.md +++ b/docs/operations/health_checks_de.md @@ -3,36 +3,13 @@ Vor und nach dem Anwenden des Blueprints wird gewartet, dass das Ecosystem healthy ist. Dabei wird folgendes geprüft: - Health aller Dogus anhand der Dogu-CRs -- Health aller Components anhand der Component-CRs -- Überprüfung, ob alle notwendigen Components installiert sind, die für das Blueprint gebraucht werden - -Die Health-Checks verwenden einen eingebauten Retry. -Timeout und Check-Interval lassen sich dafür in der [Health-Config](#health-config) festlegen. +- Überprüfung, ob alle Dogus bereits die neueste Version und Konfiguration verwenden ## Health ignorieren Die Health-Checks vor der Ausführung des Blueprints können deaktiviert werden: - für Dogus, wenn `spec.ignoreDoguHealth` auf `true` gesetzt wird, -- für Components, wenn `spec.ignoreComponentHealth` auf `true` gesetzt wird. So ist es möglich, per Blueprint Fehler an Dogus und Komponenten zu beheben. Für ein Dogu-Upgrade muss ein Dogu allerdings healthy sein, um Pre-Upgrade-Skripte ausführen zu können. Das Ignorieren der Dogu-Health kann also zu Folgefehlern während der Ausführung des Blueprints führen. - -## Health-Config - -Die Health-Konfiguration kann im Feld `valuesYamlOverwrite` der Komponenten-CR des Blueprint-Operators überschrieben werden. -Folgendes Beispiel zeigt die möglichen Einstellungen mit ihrer Default-Konfiguration: - -```yaml -valuesYamlOverwrite: | - healthConfig: - components: - required: # These components are required for health checks to succeed. - - name: k8s-dogu-operator - - name: k8s-service-discovery - - name: k8s-component-operator - wait: # Define timeout and check-interval for the ecosystem to become healthy. - timeout: 10m - interval: 10s -``` \ No newline at end of file diff --git a/docs/operations/health_checks_en.md b/docs/operations/health_checks_en.md index 7ee804f5..597876a7 100644 --- a/docs/operations/health_checks_en.md +++ b/docs/operations/health_checks_en.md @@ -3,36 +3,13 @@ Before and after applying the blueprint, the ecosystem is checked to ensure that it is healthy. The following is checked: - Health of all Dogus based on the Dogu-CRs -- Health of all components based on the component CRs -- Check whether all necessary components required for the blueprint are installed - -The health checks use a built-in retry. -The timeout and check interval can be defined in the [Health-Config](#health-config). +- Check whether all Dogus already use the latest version and configuration ## Ignoring health Upfront health checks can be deactivated: - for Dogus, if `spec.ignoreDoguHealth` is set to `true`, -- for components, if `spec.ignoreComponentHealth` is set to `true`. This makes it possible to fix errors on Dogus and components via Blueprint. For a Dogu upgrade, however, a Dogu must be healthy in order to be able to execute pre-upgrade scripts. Ignoring the dogu health can therefore lead to subsequent errors during the execution of the blueprint. - -## Health-Config - -The health configuration can be overwritten in the `valuesYamlOverwrite` field of the component CR of the blueprint operator. -The following example shows the possible settings with their default configuration: - -```yaml -valuesYamlOverwrite: | - healthConfig: - components: - required: # These components are required for health checks to succeed. - - name: k8s-dogu-operator - - name: k8s-service-discovery - - name: k8s-component-operator - wait: # Define timeout and check-interval for the ecosystem to become healthy. - timeout: 10m - interval: 10s -``` \ No newline at end of file diff --git a/go.mod b/go.mod index fa52b419..d3945c1d 100644 --- a/go.mod +++ b/go.mod @@ -1,172 +1,104 @@ module github.com/cloudogu/k8s-blueprint-operator/v2 -go 1.24.3 +go 1.25.1 require ( - github.com/Masterminds/semver/v3 v3.3.1 + github.com/Masterminds/semver/v3 v3.4.0 github.com/cloudogu/ces-commons-lib v0.2.0 github.com/cloudogu/cesapp-lib v0.18.1 - github.com/cloudogu/k8s-blueprint-lib/v2 v2.0.0-20250731163609-f388bd8c571c - github.com/cloudogu/k8s-component-operator v1.9.0 - github.com/cloudogu/k8s-dogu-lib/v2 v2.8.0 - github.com/cloudogu/k8s-registry-lib v0.5.1 + github.com/cloudogu/k8s-blueprint-lib/v2 v2.0.0-20250925091741-275e54da827b + github.com/cloudogu/k8s-dogu-lib/v2 v2.0.0-20250924122244-01339f784cd0 + github.com/cloudogu/k8s-registry-lib v0.2.2-0.20250818112109-cfd93e57e9f6 github.com/cloudogu/remote-dogu-descriptor-lib v0.1.1 - github.com/cloudogu/retry-lib v0.1.0 - github.com/go-logr/logr v1.4.2 - github.com/stretchr/testify v1.10.0 + github.com/go-logr/logr v1.4.3 + github.com/google/go-cmp v0.7.0 + github.com/stretchr/testify v1.11.1 go.uber.org/zap v1.27.0 golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 - golang.org/x/net v0.40.0 - gopkg.in/yaml.v2 v2.4.0 - gopkg.in/yaml.v3 v3.0.1 - k8s.io/api v0.33.3 - k8s.io/apimachinery v0.33.3 - k8s.io/client-go v0.33.3 - sigs.k8s.io/controller-runtime v0.21.0 + golang.org/x/net v0.44.0 + k8s.io/api v0.34.1 + k8s.io/apimachinery v0.34.1 + k8s.io/client-go v0.34.1 + k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d + sigs.k8s.io/controller-runtime v0.22.1 ) require ( - dario.cat/mergo v1.0.1 // indirect - github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect - github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect - github.com/BurntSushi/toml v1.4.0 // indirect - github.com/MakeNowJust/heredoc v1.0.0 // indirect - github.com/Masterminds/goutils v1.1.1 // indirect - github.com/Masterminds/sprig/v3 v3.3.0 // indirect - github.com/Masterminds/squirrel v1.5.4 // indirect - github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect + dario.cat/mergo v1.0.2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/chai2010/gettext-go v1.0.3 // indirect - github.com/containerd/cgroups/v3 v3.0.5 // indirect - github.com/containerd/containerd v1.7.27 // indirect - github.com/containerd/errdefs v1.0.0 // indirect - github.com/containerd/errdefs/pkg v0.3.0 // indirect - github.com/containerd/log v0.1.0 // indirect - github.com/containerd/platforms v0.2.1 // indirect - github.com/cyphar/filepath-securejoin v0.3.6 // indirect + github.com/cloudogu/retry-lib v0.1.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/distribution/reference v0.6.0 // indirect - github.com/docker/cli v27.5.0+incompatible // indirect - github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/docker v27.4.1+incompatible // indirect - github.com/docker/docker-credential-helpers v0.8.2 // indirect - github.com/docker/go-connections v0.5.0 // indirect - github.com/docker/go-metrics v0.0.1 // indirect github.com/eapache/go-resiliency v1.7.0 // indirect - github.com/emicklei/go-restful/v3 v3.12.1 // indirect - github.com/evanphx/json-patch v5.9.0+incompatible // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect + github.com/evanphx/json-patch v5.9.11+incompatible // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect - github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect - github.com/fatih/color v1.18.0 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/fsnotify/fsnotify v1.8.0 // indirect - github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/gammazero/toposort v0.1.1 // indirect - github.com/go-errors/errors v1.5.1 // indirect - github.com/go-gorp/gorp/v3 v3.1.0 // indirect - github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/zapr v1.3.0 // indirect - github.com/go-openapi/jsonpointer v0.21.0 // indirect - github.com/go-openapi/jsonreference v0.21.0 // indirect - github.com/go-openapi/swag v0.23.0 // indirect - github.com/gobwas/glob v0.2.3 // indirect + github.com/go-openapi/jsonpointer v0.22.0 // indirect + github.com/go-openapi/jsonreference v0.21.1 // indirect + github.com/go-openapi/swag v0.24.1 // indirect + github.com/go-openapi/swag/cmdutils v0.24.0 // indirect + github.com/go-openapi/swag/conv v0.24.0 // indirect + github.com/go-openapi/swag/fileutils v0.24.0 // indirect + github.com/go-openapi/swag/jsonname v0.24.0 // indirect + github.com/go-openapi/swag/jsonutils v0.24.0 // indirect + github.com/go-openapi/swag/loading v0.24.0 // indirect + github.com/go-openapi/swag/mangling v0.24.0 // indirect + github.com/go-openapi/swag/netutils v0.24.0 // indirect + github.com/go-openapi/swag/stringutils v0.24.0 // indirect + github.com/go-openapi/swag/typeutils v0.24.0 // indirect + github.com/go-openapi/swag/yamlutils v0.24.0 // indirect + github.com/gobuffalo/flect v1.0.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/btree v1.1.3 // indirect - github.com/google/gnostic-models v0.6.9 // indirect - github.com/google/go-cmp v0.7.0 // indirect - github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect - github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + github.com/google/gnostic-models v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/gorilla/mux v1.8.1 // indirect - github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect - github.com/gosuri/uitable v0.0.4 // indirect - github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect - github.com/hashicorp/errwrap v1.1.0 // indirect - github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/huandu/xstrings v1.5.0 // indirect - github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/jmoiron/sqlx v1.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.18.0 // indirect - github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect - github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect - github.com/lib/pq v1.10.9 // indirect - github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect - github.com/mailru/easyjson v0.9.0 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.16 // indirect - github.com/mitchellh/copystructure v1.2.0 // indirect - github.com/mitchellh/go-wordwrap v1.0.1 // indirect - github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/moby/locker v1.0.1 // indirect - github.com/moby/spdystream v0.5.0 // indirect - github.com/moby/term v0.5.0 // indirect + github.com/mailru/easyjson v0.9.1 // indirect + github.com/moby/term v0.5.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect - github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.1.0 // indirect - github.com/peterbourgon/diskv v2.0.1+incompatible // indirect + github.com/onsi/gomega v1.38.2 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/client_golang v1.22.0 // indirect - github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.62.0 // indirect - github.com/prometheus/procfs v0.15.1 // indirect - github.com/rivo/uniseg v0.4.7 // indirect - github.com/rubenv/sql-migrate v1.7.1 // indirect - github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/shopspring/decimal v1.4.0 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.17.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect - github.com/spf13/cast v1.7.1 // indirect - github.com/spf13/cobra v1.8.1 // indirect - github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/pflag v1.0.10 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/x448/float16 v0.8.4 // indirect - github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect - github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect - github.com/xeipuuv/gojsonschema v1.2.0 // indirect - github.com/xlab/treeprint v1.2.0 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect - go.opentelemetry.io/otel v1.33.0 // indirect - go.opentelemetry.io/otel/metric v1.33.0 // indirect - go.opentelemetry.io/otel/trace v1.33.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect + go.opentelemetry.io/otel/trace v1.37.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.38.0 // indirect - golang.org/x/oauth2 v0.27.0 // indirect - golang.org/x/sync v0.14.0 // indirect - golang.org/x/sys v0.33.0 // indirect - golang.org/x/term v0.32.0 // indirect - golang.org/x/text v0.25.0 // indirect - golang.org/x/time v0.9.0 // indirect - gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241216192217-9240e9c98484 // indirect - google.golang.org/grpc v1.69.2 // indirect - google.golang.org/protobuf v1.36.5 // indirect - gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/oauth2 v0.31.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.36.0 // indirect + golang.org/x/term v0.35.0 // indirect + golang.org/x/text v0.29.0 // indirect + golang.org/x/time v0.13.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect + google.golang.org/protobuf v1.36.9 // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gotest.tools/v3 v3.5.1 // indirect - helm.sh/helm/v3 v3.17.3 // indirect - k8s.io/apiextensions-apiserver v0.33.0 // indirect - k8s.io/apiserver v0.33.0 // indirect - k8s.io/cli-runtime v0.32.2 // indirect - k8s.io/component-base v0.33.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/apiextensions-apiserver v0.34.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect - k8s.io/kubectl v0.32.2 // indirect - k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect - oras.land/oras-go v1.2.6 // indirect - sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect - sigs.k8s.io/kustomize/api v0.18.0 // indirect - sigs.k8s.io/kustomize/kyaml v0.18.1 // indirect + k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect + sigs.k8s.io/cluster-api v1.11.1 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/go.sum b/go.sum index ac6c1a5b..653640af 100644 --- a/go.sum +++ b/go.sum @@ -1,331 +1,172 @@ -dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= -dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= -filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= -github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= -github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= -github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= -github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= -github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= -github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= -github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= -github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= -github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= -github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= -github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= -github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/Microsoft/hcsshim v0.12.9 h1:2zJy5KA+l0loz1HzEGqyNnjd3fyZA31ZBCGKacp6lLg= -github.com/Microsoft/hcsshim v0.12.9/go.mod h1:fJ0gkFAna6ukt0bLdKB8djt4XIJhF/vEPuoIWYVvZ8Y= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= -github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= -github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= -github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70= -github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chai2010/gettext-go v1.0.3 h1:9liNh8t+u26xl5ddmWLmsOsdNLwkdRTg5AG+JnTiM80= -github.com/chai2010/gettext-go v1.0.3/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= github.com/cloudogu/ces-commons-lib v0.2.0 h1:yOEZWFl4W9N3J/6fok4svE3UufK5GQQtyxvwtIF5AdM= github.com/cloudogu/ces-commons-lib v0.2.0/go.mod h1:4rvR2RTDDaz5a6OZ1fW27G0MOnl5I3ackeiHxt4gn3o= github.com/cloudogu/cesapp-lib v0.18.1 h1:LMdGktIefm/PuhdPqpLTPvjY1smO06EEGBbRSAaYi7U= github.com/cloudogu/cesapp-lib v0.18.1/go.mod h1:J05eXFxnz4enZblABlmiVTZaUtJ+LIhlJ2UF6l9jpDw= -github.com/cloudogu/k8s-blueprint-lib/v2 v2.0.0-20250723132542-b0472a456ff0 h1:b3vbIIV1J8YtRCMkJC0EKB10DcTPDqVNda/FNX11E80= -github.com/cloudogu/k8s-blueprint-lib/v2 v2.0.0-20250723132542-b0472a456ff0/go.mod h1:Qyi8M+HJMHJfhXN6Zotey/tXjFuJDM9RIXn+FjQaJAU= -github.com/cloudogu/k8s-blueprint-lib/v2 v2.0.0-20250731094124-310a388d0c7f h1:05BZKhsUJrv4oHkmK4CbNd2/0egXqXup/sfHOOCWC/0= -github.com/cloudogu/k8s-blueprint-lib/v2 v2.0.0-20250731094124-310a388d0c7f/go.mod h1:Qyi8M+HJMHJfhXN6Zotey/tXjFuJDM9RIXn+FjQaJAU= -github.com/cloudogu/k8s-blueprint-lib/v2 v2.0.0-20250731163609-f388bd8c571c h1:o2hARW5ifab56Yjzxedc95wCBnjsJuF1k0bOfqUNXB0= -github.com/cloudogu/k8s-blueprint-lib/v2 v2.0.0-20250731163609-f388bd8c571c/go.mod h1:Qyi8M+HJMHJfhXN6Zotey/tXjFuJDM9RIXn+FjQaJAU= -github.com/cloudogu/k8s-component-operator v1.9.0 h1:b1/gMcAPQBP93EIVTE1ctmAKFdVOqnTST+x6LOqu08g= -github.com/cloudogu/k8s-component-operator v1.9.0/go.mod h1:BdWqwpZWHoSFOduQ3FzcVV4uGJNb+t0DUYlLBHQ9wwQ= -github.com/cloudogu/k8s-dogu-lib/v2 v2.8.0 h1:ae+LtiY+J2/qIX1AUJLcVxx/FQvlstq9ViVMwlJD26Y= -github.com/cloudogu/k8s-dogu-lib/v2 v2.8.0/go.mod h1:XvnrQKqCh+ksGc+tFkv+EXfKeZC7P1jPInidSElh5CM= -github.com/cloudogu/k8s-registry-lib v0.5.1 h1:gbdrhETUm53GP65LoljrS1kekDDl/onBPfrOQTQpt1s= -github.com/cloudogu/k8s-registry-lib v0.5.1/go.mod h1:mdMOgknEOrGQH1zc/3K859iPhwpqwtzigK9QrjM3Vk0= +github.com/cloudogu/k8s-blueprint-lib/v2 v2.0.0-20250925091741-275e54da827b h1:1VRisF6h7o/B8fOtPIRuMApMe4PQKiCgiYDLD2tyScw= +github.com/cloudogu/k8s-blueprint-lib/v2 v2.0.0-20250925091741-275e54da827b/go.mod h1:yNtYKIfJkJyMCQ5srtWspl4CDIu3pCvfNC4Yci1XVtQ= +github.com/cloudogu/k8s-dogu-lib/v2 v2.0.0-20250924122244-01339f784cd0 h1:ohitXnpL655pmZOO0LVjW/weVi1ftFKSXG7jllbRQag= +github.com/cloudogu/k8s-dogu-lib/v2 v2.0.0-20250924122244-01339f784cd0/go.mod h1:2xkJ1TI7fuBRcqIpHUlmQ6CJAtzlOvyUbwPktIVq6K8= +github.com/cloudogu/k8s-registry-lib v0.2.2-0.20250818112109-cfd93e57e9f6 h1:OYMO6DYX6rtzo2a88hXXTzwf8+aXt7uV+BTaEmvlls4= +github.com/cloudogu/k8s-registry-lib v0.2.2-0.20250818112109-cfd93e57e9f6/go.mod h1:NytbB2XBjc+gr5gOJFoPjCzyxs/TuhWpKSnDJzCLQ14= github.com/cloudogu/remote-dogu-descriptor-lib v0.1.1 h1:NWoBvBwiE8yO8HqSId0+nOTBMGSjzX0yM8m6fC7Ll6E= github.com/cloudogu/remote-dogu-descriptor-lib v0.1.1/go.mod h1:AGZ3bihl/sUUJ6iEtcxtALIt/UlkfUdK7HXJ6DtBivE= github.com/cloudogu/retry-lib v0.1.0 h1:gaAmtyjUqgHbxfCWMeUn0qnGbDH4TtZVSQkbZ1Nq6eI= github.com/cloudogu/retry-lib v0.1.0/go.mod h1:iG9y6zx8oJZT5ULtl9koZkYJLRsqam/2mTU+rgjxQ0g= -github.com/containerd/cgroups/v3 v3.0.5 h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJHo6Bzo= -github.com/containerd/cgroups/v3 v3.0.5/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins= -github.com/containerd/containerd v1.7.27 h1:yFyEyojddO3MIGVER2xJLWoCIn+Up4GaHFquP7hsFII= -github.com/containerd/containerd v1.7.27/go.mod h1:xZmPnl75Vc+BLGt4MIfu6bp+fy03gdHAn9bz+FreFR0= -github.com/containerd/continuity v0.4.4 h1:/fNVfTJ7wIl/YPMHjf+5H32uFhl63JucB34PlCpMKII= -github.com/containerd/continuity v0.4.4/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= -github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= -github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= -github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= -github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/containerd/typeurl v1.0.2 h1:Chlt8zIieDbzQFzXzAeBEF92KhExuE4p9p92/QmY7aY= -github.com/containerd/typeurl/v2 v2.2.0 h1:6NBDbQzr7I5LHgp34xAXYF5DOTQDn05X58lsPEmzLso= -github.com/containerd/typeurl/v2 v2.2.0/go.mod h1:8XOOxnyatxSWuG8OfsZXVnAF4iZfedjS/8UHSPJnX4g= -github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= -github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= -github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= -github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM= -github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/distribution/distribution/v3 v3.0.0-beta.1 h1:X+ELTxPuZ1Xe5MsD3kp2wfGUhc8I+MPfRis8dZ818Ic= -github.com/distribution/distribution/v3 v3.0.0-beta.1/go.mod h1:O9O8uamhHzWWQVTjuQpyYUVm/ShPHPUDgvQMpHGVBDs= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/cli v27.5.0+incompatible h1:aMphQkcGtpHixwwhAXJT1rrK/detk2JIvDaFkLctbGM= -github.com/docker/cli v27.5.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= -github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v27.4.1+incompatible h1:ZJvcY7gfwHn1JF48PfbyXg7Jyt9ZCWDW+GGXOIxEwp4= github.com/docker/docker v27.4.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= -github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= -github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= -github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= -github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= -github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 h1:ZClxb8laGDf5arXfYcAtECDFgAgHklGI8CxgjHnXKJ4= -github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= github.com/eapache/go-resiliency v1.7.0 h1:n3NRTnBn5N0Cbi/IeOHuQn9s2UwVUH7Ga0ZWcP+9JTA= github.com/eapache/go-resiliency v1.7.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho= -github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU= -github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls= -github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= +github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= -github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= -github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= -github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= -github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI= -github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= -github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= -github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gammazero/toposort v0.1.1 h1:OivGxsWxF3U3+U80VoLJ+f50HcPU1MIqE1JlKzoJ2Eg= github.com/gammazero/toposort v0.1.1/go.mod h1:H2cozTnNpMw0hg2VHAYsAxmkHXBYroNangj2NTBQDvw= -github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= -github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= -github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= -github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= -github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= -github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= -github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= -github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= -github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= -github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= -github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= -github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-openapi/jsonpointer v0.22.0 h1:TmMhghgNef9YXxTu1tOopo+0BGEytxA+okbry0HjZsM= +github.com/go-openapi/jsonpointer v0.22.0/go.mod h1:xt3jV88UtExdIkkL7NloURjRQjbeUgcxFblMjq2iaiU= +github.com/go-openapi/jsonreference v0.21.1 h1:bSKrcl8819zKiOgxkbVNRUBIr6Wwj9KYrDbMjRs0cDA= +github.com/go-openapi/jsonreference v0.21.1/go.mod h1:PWs8rO4xxTUqKGu+lEvvCxD5k2X7QYkKAepJyCmSTT8= +github.com/go-openapi/swag v0.24.1 h1:DPdYTZKo6AQCRqzwr/kGkxJzHhpKxZ9i/oX0zag+MF8= +github.com/go-openapi/swag v0.24.1/go.mod h1:sm8I3lCPlspsBBwUm1t5oZeWZS0s7m/A+Psg0ooRU0A= +github.com/go-openapi/swag/cmdutils v0.24.0 h1:KlRCffHwXFI6E5MV9n8o8zBRElpY4uK4yWyAMWETo9I= +github.com/go-openapi/swag/cmdutils v0.24.0/go.mod h1:uxib2FAeQMByyHomTlsP8h1TtPd54Msu2ZDU/H5Vuf8= +github.com/go-openapi/swag/conv v0.24.0 h1:ejB9+7yogkWly6pnruRX45D1/6J+ZxRu92YFivx54ik= +github.com/go-openapi/swag/conv v0.24.0/go.mod h1:jbn140mZd7EW2g8a8Y5bwm8/Wy1slLySQQ0ND6DPc2c= +github.com/go-openapi/swag/fileutils v0.24.0 h1:U9pCpqp4RUytnD689Ek/N1d2N/a//XCeqoH508H5oak= +github.com/go-openapi/swag/fileutils v0.24.0/go.mod h1:3SCrCSBHyP1/N+3oErQ1gP+OX1GV2QYFSnrTbzwli90= +github.com/go-openapi/swag/jsonname v0.24.0 h1:2wKS9bgRV/xB8c62Qg16w4AUiIrqqiniJFtZGi3dg5k= +github.com/go-openapi/swag/jsonname v0.24.0/go.mod h1:GXqrPzGJe611P7LG4QB9JKPtUZ7flE4DOVechNaDd7Q= +github.com/go-openapi/swag/jsonutils v0.24.0 h1:F1vE1q4pg1xtO3HTyJYRmEuJ4jmIp2iZ30bzW5XgZts= +github.com/go-openapi/swag/jsonutils v0.24.0/go.mod h1:vBowZtF5Z4DDApIoxcIVfR8v0l9oq5PpYRUuteVu6f0= +github.com/go-openapi/swag/loading v0.24.0 h1:ln/fWTwJp2Zkj5DdaX4JPiddFC5CHQpvaBKycOlceYc= +github.com/go-openapi/swag/loading v0.24.0/go.mod h1:gShCN4woKZYIxPxbfbyHgjXAhO61m88tmjy0lp/LkJk= +github.com/go-openapi/swag/mangling v0.24.0 h1:PGOQpViCOUroIeak/Uj/sjGAq9LADS3mOyjznmHy2pk= +github.com/go-openapi/swag/mangling v0.24.0/go.mod h1:Jm5Go9LHkycsz0wfoaBDkdc4CkpuSnIEf62brzyCbhc= +github.com/go-openapi/swag/netutils v0.24.0 h1:Bz02HRjYv8046Ycg/w80q3g9QCWeIqTvlyOjQPDjD8w= +github.com/go-openapi/swag/netutils v0.24.0/go.mod h1:WRgiHcYTnx+IqfMCtu0hy9oOaPR0HnPbmArSRN1SkZM= +github.com/go-openapi/swag/stringutils v0.24.0 h1:i4Z/Jawf9EvXOLUbT97O0HbPUja18VdBxeadyAqS1FM= +github.com/go-openapi/swag/stringutils v0.24.0/go.mod h1:5nUXB4xA0kw2df5PRipZDslPJgJut+NjL7D25zPZ/4w= +github.com/go-openapi/swag/typeutils v0.24.0 h1:d3szEGzGDf4L2y1gYOSSLeK6h46F+zibnEas2Jm/wIw= +github.com/go-openapi/swag/typeutils v0.24.0/go.mod h1:q8C3Kmk/vh2VhpCLaoR2MVWOGP8y7Jc8l82qCTd1DYI= +github.com/go-openapi/swag/yamlutils v0.24.0 h1:bhw4894A7Iw6ne+639hsBNRHg9iZg/ISrOVr+sJGp4c= +github.com/go-openapi/swag/yamlutils v0.24.0/go.mod h1:DpKv5aYuaGm/sULePoeiG8uwMpZSfReo1HR3Ik0yaG8= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= -github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4= +github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= -github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= -github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= -github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= -github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= -github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= -github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= -github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= -github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= -github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= -github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= -github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= -github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= -github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= -github.com/hashicorp/golang-lru/arc/v2 v2.0.5 h1:l2zaLDubNhW4XO3LnliVj0GXO3+/CGNJAg1dcN2Fpfw= -github.com/hashicorp/golang-lru/arc/v2 v2.0.5/go.mod h1:ny6zBSQZi2JxIeYcv7kt2sH2PXJtirBN7RDhRpxPkxU= -github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4= -github.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= -github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= -github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 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/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= -github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= -github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= -github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= -github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= -github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= -github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= github.com/lufia/plan9stats v0.0.0-20240819163618-b1d8f4d146e7 h1:5RK988zAqB3/AN3opGfRpoQgAVqr6/A5+qRTi67VUZY= github.com/lufia/plan9stats v0.0.0-20240819163618-b1d8f4d146e7/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= -github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= -github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= -github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= -github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= -github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= -github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= -github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= -github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= -github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= -github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= +github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= -github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= -github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= -github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= -github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= -github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= -github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= -github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= -github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 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/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= -github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= -github.com/onsi/ginkgo/v2 v2.22.1 h1:QW7tbJAUDyVDVOM5dFa7qaybo+CRfR7bemlQUN6Z8aM= -github.com/onsi/ginkgo/v2 v2.22.1/go.mod h1:S6aTpoRsSq2cZOd+pssHAlKW/Q/jZt6cPrPlnj4a1xM= -github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= -github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= +github.com/onsi/ginkgo/v2 v2.25.1 h1:Fwp6crTREKM+oA6Cz4MsO8RhKQzs2/gOIVOUscMAfZY= +github.com/onsi/ginkgo/v2 v2.25.1/go.mod h1:ppTWQ1dh9KM/F1XgpeRqelR+zHVwV81DGRSDnFxK7Sk= +github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= +github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= -github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= -github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= -github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= -github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= -github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -333,68 +174,36 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= -github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY= -github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= -github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= -github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= -github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= -github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= -github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= -github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= -github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= -github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 h1:EaDatTxkdHG+U3Bk4EUr+DZ7fOGwTfezUiUJMaIcaho= -github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5/go.mod h1:fyalQWdtzDBECAQFBJuQe5bzQ02jGd5Qcbgb97Flm7U= -github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb27yVE+gIAfeqp8LUCc= -github.com/redis/go-redis/extra/redisotel/v9 v9.0.5/go.mod h1:WZjPDy7VNzn77AAfnAfVjZNvfJTYfPetfZk5yoSTLaQ= -github.com/redis/go-redis/v9 v9.1.0 h1:137FnGdk+EQdCbye1FW+qOEcY5S+SpY9T0NiuqvtfMY= -github.com/redis/go-redis/v9 v9.1.0/go.mod h1:urWj3He21Dj5k4TK1y59xH8Uj6ATueP8AH1cY3lZl4c= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= -github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= +github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= -github.com/rubenv/sql-migrate v1.7.1 h1:f/o0WgfO/GqNuVg+6801K/KW3WdDSupzSjDYODmiUq4= -github.com/rubenv/sql-migrate v1.7.1/go.mod h1:Ob2Psprc0/3ggbM6wCzyYVFFuc6FyZrb2AS+ezLDFb4= -github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= -github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= -github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= -github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= -github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= -github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= -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/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/testcontainers/testcontainers-go v0.33.0 h1:zJS9PfXYT5O0ZFXM2xxXfk4J5UMw/kRiISng037Gxdw= github.com/testcontainers/testcontainers-go v0.33.0/go.mod h1:W80YpTa8D5C3Yy16icheD01UTDu+LmXIA2Keo+jWtT8= github.com/testcontainers/testcontainers-go/modules/k3s v0.33.0 h1:vKz46Z+vClMc6MNnwpcw8tNjOOprJxWzsn4J+1noTM4= @@ -405,185 +214,115 @@ github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYg github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= -github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= -github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= -github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= -github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= -github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/exporters/autoexport v0.46.1 h1:ysCfPZB9AjUlMa1UHYup3c9dAOCMQX/6sxSfPBUoxHw= -go.opentelemetry.io/contrib/exporters/autoexport v0.46.1/go.mod h1:ha0aiYm+DOPsLHjh0zoQ8W8sLT+LJ58J3j47lGpSLrU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= -go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= -go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 h1:jd0+5t/YynESZqsSyPz+7PAFdEop0dlN0+PkyHYo8oI= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0/go.mod h1:U707O40ee1FpQGyhvqnzmCJm1Wh6OX6GGBVn0E6Uyyk= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.44.0 h1:bflGWrfYyuulcdxf14V6n9+CoQcu5SAAdHmDPAJnlps= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.44.0/go.mod h1:qcTO4xHAxZLaLxPd60TdE88rxtItPHgHWqOhOGRr0as= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 h1:5pojmb1U1AogINhN3SurB+zm/nIcusopeBNp42f45QM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0 h1:digkEZCJWobwBqMwC0cwCq8/wkkRy/OowZg5OArWZrM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0/go.mod h1:/OpE/y70qVkndM0TrxT4KBoN3RsFZP0QaofcfYrj76I= -go.opentelemetry.io/otel/exporters/prometheus v0.44.0 h1:08qeJgaPC0YEBu2PQMbqU3rogTlyzpjhCI2b58Yn00w= -go.opentelemetry.io/otel/exporters/prometheus v0.44.0/go.mod h1:ERL2uIeBtg4TxZdojHUwzZfIFlUIjZtxubT5p4h1Gjg= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v0.44.0 h1:dEZWPjVN22urgYCza3PXRUGEyCB++y1sAqm6guWFesk= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v0.44.0/go.mod h1:sTt30Evb7hJB/gEk27qLb1+l9n4Tb8HvHkR0Wx3S6CU= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.21.0 h1:VhlEQAPp9R1ktYfrPk5SOryw1e9LDDTZCbIPFrho0ec= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.21.0/go.mod h1:kB3ufRbfU+CQ4MlUcqtW8Z7YEOBeK2DJ6CmR5rYYF3E= -go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ= -go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M= -go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM= -go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM= -go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc= -go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8= -go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s= -go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= -go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg= -go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= -golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI= golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= -golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= -golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= -golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= -golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= +golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo= +golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= -golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= -golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= +golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= -golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= -golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= -golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= +golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= -golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= -gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= -google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 h1:KAeGQVN3M9nD0/bQXnr/ClcEMJ968gUXJQ9pwfSynuQ= -google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q= -google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241216192217-9240e9c98484 h1:Z7FRVJPSMaHQxD0uXU8WdgFh8PseLM8Q8NzhnpMrBhQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241216192217-9240e9c98484/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA= -google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU= -google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= -google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0= +gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= -gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= -gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= -helm.sh/helm/v3 v3.17.3 h1:3n5rW3D0ArjFl0p4/oWO8IbY/HKaNNwJtOQFdH2AZHg= -helm.sh/helm/v3 v3.17.3/go.mod h1:+uJKMH/UiMzZQOALR3XUf3BLIoczI2RKKD6bMhPh4G8= -k8s.io/api v0.33.3 h1:SRd5t//hhkI1buzxb288fy2xvjubstenEKL9K51KBI8= -k8s.io/api v0.33.3/go.mod h1:01Y/iLUjNBM3TAvypct7DIj0M0NIZc+PzAHCIo0CYGE= -k8s.io/apiextensions-apiserver v0.33.0 h1:d2qpYL7Mngbsc1taA4IjJPRJ9ilnsXIrndH+r9IimOs= -k8s.io/apiextensions-apiserver v0.33.0/go.mod h1:VeJ8u9dEEN+tbETo+lFkwaaZPg6uFKLGj5vyNEwwSzc= -k8s.io/apimachinery v0.33.3 h1:4ZSrmNa0c/ZpZJhAgRdcsFcZOw1PQU1bALVQ0B3I5LA= -k8s.io/apimachinery v0.33.3/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= -k8s.io/apiserver v0.33.0 h1:QqcM6c+qEEjkOODHppFXRiw/cE2zP85704YrQ9YaBbc= -k8s.io/apiserver v0.33.0/go.mod h1:EixYOit0YTxt8zrO2kBU7ixAtxFce9gKGq367nFmqI8= -k8s.io/cli-runtime v0.32.2 h1:aKQR4foh9qeyckKRkNXUccP9moxzffyndZAvr+IXMks= -k8s.io/cli-runtime v0.32.2/go.mod h1:a/JpeMztz3xDa7GCyyShcwe55p8pbcCVQxvqZnIwXN8= -k8s.io/client-go v0.33.3 h1:M5AfDnKfYmVJif92ngN532gFqakcGi6RvaOF16efrpA= -k8s.io/client-go v0.33.3/go.mod h1:luqKBQggEf3shbxHY4uVENAxrDISLOarxpTKMiUuujg= -k8s.io/component-base v0.33.0 h1:Ot4PyJI+0JAD9covDhwLp9UNkUja209OzsJ4FzScBNk= -k8s.io/component-base v0.33.0/go.mod h1:aXYZLbw3kihdkOPMDhWbjGCO6sg+luw554KP51t8qCU= +k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM= +k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= +k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI= +k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc= +k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= +k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY= +k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= -k8s.io/kubectl v0.32.2 h1:TAkag6+XfSBgkqK9I7ZvwtF0WVtUAvK8ZqTt+5zi1Us= -k8s.io/kubectl v0.32.2/go.mod h1:+h/NQFSPxiDZYX/WZaWw9fwYezGLISP0ud8nQKg+3g8= -k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0= -k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -oras.land/oras-go v1.2.6 h1:z8cmxQXBU8yZ4mkytWqXfo6tZcamPwjsuxYU81xJ8Lk= -oras.land/oras-go v1.2.6/go.mod h1:OVPc1PegSEe/K8YiLfosrlqlqTN9PUyFvOw5Y9gwrT8= -sigs.k8s.io/controller-runtime v0.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytIGcJS8= -sigs.k8s.io/controller-runtime v0.21.0/go.mod h1:OSg14+F65eWqIu4DceX7k/+QRAbTTvxeQSNSOQpukWM= -sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= -sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= -sigs.k8s.io/kustomize/api v0.18.0 h1:hTzp67k+3NEVInwz5BHyzc9rGxIauoXferXyjv5lWPo= -sigs.k8s.io/kustomize/api v0.18.0/go.mod h1:f8isXnX+8b+SGLHQ6yO4JG1rdkZlvhaCf/uZbLVMb0U= -sigs.k8s.io/kustomize/kyaml v0.18.1 h1:WvBo56Wzw3fjS+7vBjN6TeivvpbW9GmRaWZ9CIVmt4E= -sigs.k8s.io/kustomize/kyaml v0.18.1/go.mod h1:C3L2BFVU1jgcddNBE1TxuVLgS46TjObMwW5FT9FcjYo= -sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d h1:wAhiDyZ4Tdtt7e46e9M5ZSAJ/MnPGPs+Ki1gHw4w1R0= +k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/cluster-api v1.11.1 h1:7CyGCTxv1p3Y2kRe1ljTj/w4TcdIdWNj0CTBc4i1aBo= +sigs.k8s.io/cluster-api v1.11.1/go.mod h1:zyrjgJ5RbXhwKcAdUlGPNK5YOHpcmxXvur+5I8lkMUQ= +sigs.k8s.io/controller-runtime v0.22.1 h1:Ah1T7I+0A7ize291nJZdS1CabF/lB4E++WizgV24Eqg= +sigs.k8s.io/controller-runtime v0.22.1/go.mod h1:FwiwRjkRPbiN+zp2QRp7wlTCzbUXxZ/D4OzuQUDwBHY= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= -sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/k8s/helm/Chart.yaml b/k8s/helm/Chart.yaml index c0518d4b..9cb8402a 100644 --- a/k8s/helm/Chart.yaml +++ b/k8s/helm/Chart.yaml @@ -26,6 +26,4 @@ appVersion: "0.0.0-replaceme" annotations: # TODO: need to update Dogu-CRD and blueprint-CRD dependencies "k8s.cloudogu.com/ces-dependency/k8s-blueprint-operator-crd": ">=1.3.0-0" - "k8s.cloudogu.com/ces-dependency/k8s-dogu-operator-crd": ">=2.8.0-0, <3.0.0-0" - "k8s.cloudogu.com/ces-dependency/k8s-component-operator-crd": "1.x.x-0" - "k8s.cloudogu.com/ces-dependency/k8s-service-discovery": ">=0.15.0-0" + "k8s.cloudogu.com/ces-dependency/k8s-dogu-operator-crd": ">=2.8.0-0, <3.0.0-0" \ No newline at end of file diff --git a/k8s/helm/component-patch-tpl.yaml b/k8s/helm/component-patch-tpl.yaml index 396f53d0..7cd34e69 100644 --- a/k8s/helm/component-patch-tpl.yaml +++ b/k8s/helm/component-patch-tpl.yaml @@ -1,7 +1,7 @@ apiVersion: v1 values: images: - blueprintOperator: cloudogu/k8s-blueprint-operator:2.7.0 + blueprintOperator: cloudogu/k8s-blueprint-operator:2.8.0 patches: values.yaml: manager: diff --git a/k8s/helm/templates/blueprint-editor-role.yaml b/k8s/helm/templates/blueprint-editor-role.yaml index 0f3b7fb2..0ce46fe9 100644 --- a/k8s/helm/templates/blueprint-editor-role.yaml +++ b/k8s/helm/templates/blueprint-editor-role.yaml @@ -1,25 +1,31 @@ +# Issue RBAC permissions to the operator to fulfill CR handling which includes reading and updating customer-created +# Blueprint CRs. The operator does not create or delete Blueprints by itself, though. + apiVersion: rbac.authorization.k8s.io/v1 +# the blueprint operator should only handle Blueprint CRs within its own namespace, not Blueprints in other namespaces kind: Role metadata: labels: {{- include "k8s-blueprint-operator.labels" . | nindent 4 }} name: {{ include "k8s-blueprint-operator.name" . }}-blueprint-editor-role rules: + # issue permissions to read/update fields beyond the status - apiGroups: - k8s.cloudogu.com resources: - blueprints verbs: - - create - - delete - get - list - patch - update - watch + # issue permissions to update the status which contains blueprint processing data - apiGroups: - k8s.cloudogu.com resources: - blueprints/status verbs: - - get \ No newline at end of file + - get + - patch + - update \ No newline at end of file diff --git a/k8s/helm/templates/component-editor-role.yaml b/k8s/helm/templates/component-editor-role.yaml deleted file mode 100644 index 9474e38f..00000000 --- a/k8s/helm/templates/component-editor-role.yaml +++ /dev/null @@ -1,25 +0,0 @@ -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - labels: - {{- include "k8s-blueprint-operator.labels" . | nindent 4 }} - name: {{ include "k8s-blueprint-operator.name" . }}-component-editor-role -rules: - - apiGroups: - - k8s.cloudogu.com - resources: - - components - verbs: - - create - - delete - - get - - list - - patch - - update - - watch - - apiGroups: - - k8s.cloudogu.com - resources: - - components/status - verbs: - - get \ No newline at end of file diff --git a/k8s/helm/templates/component-editor-rolebinding.yaml b/k8s/helm/templates/component-editor-rolebinding.yaml deleted file mode 100644 index 98ea20aa..00000000 --- a/k8s/helm/templates/component-editor-rolebinding.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - labels: - {{- include "k8s-blueprint-operator.labels" . | nindent 4 }} - name: {{ include "k8s-blueprint-operator.name" . }}-component-editor-rolebinding -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: {{ include "k8s-blueprint-operator.name" . }}-component-editor-role -subjects: - - kind: ServiceAccount - name: {{ include "k8s-blueprint-operator.name" . }}-controller-manager \ No newline at end of file diff --git a/k8s/helm/templates/dogu-restart-role.yaml b/k8s/helm/templates/dogu-restart-role.yaml deleted file mode 100644 index 61b2d92e..00000000 --- a/k8s/helm/templates/dogu-restart-role.yaml +++ /dev/null @@ -1,19 +0,0 @@ -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - labels: - {{- include "k8s-blueprint-operator.labels" . | nindent 4 }} - name: {{ include "k8s-blueprint-operator.name" . }}-dogu-restart-role -rules: - - apiGroups: - - k8s.cloudogu.com - resources: - - dogurestarts - verbs: - - create - - apiGroups: - - k8s.cloudogu.com - resources: - - dogurestarts/status - verbs: - - get \ No newline at end of file diff --git a/k8s/helm/templates/dogu-restart-rolebinding.yaml b/k8s/helm/templates/dogu-restart-rolebinding.yaml deleted file mode 100644 index f01e3992..00000000 --- a/k8s/helm/templates/dogu-restart-rolebinding.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - labels: - {{- include "k8s-blueprint-operator.labels" . | nindent 4 }} - name: {{ include "k8s-blueprint-operator.name" . }}-dogu-restart-rolebinding -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: {{ include "k8s-blueprint-operator.name" . }}-dogu-restart-role -subjects: - - kind: ServiceAccount - name: {{ include "k8s-blueprint-operator.name" . }}-controller-manager \ No newline at end of file diff --git a/k8s/helm/templates/health-configmap.yaml b/k8s/helm/templates/health-configmap.yaml deleted file mode 100644 index 466c3bb5..00000000 --- a/k8s/helm/templates/health-configmap.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - labels: - {{- include "k8s-blueprint-operator.labels" . | nindent 4 }} - name: {{ include "k8s-blueprint-operator.name" . }}-health-config -data: - components: | - {{- toYaml .Values.healthConfig.components | nindent 4 }} - wait: | - {{- toYaml .Values.healthConfig.wait | nindent 4 }} \ No newline at end of file diff --git a/k8s/helm/templates/manager-role.yaml b/k8s/helm/templates/manager-role.yaml index 95316428..8967a42d 100644 --- a/k8s/helm/templates/manager-role.yaml +++ b/k8s/helm/templates/manager-role.yaml @@ -9,34 +9,6 @@ metadata: {{- include "k8s-blueprint-operator.labels" . | nindent 4 }} name: {{ include "k8s-blueprint-operator.name" . }}-manager-role rules: - -# issue permissions to read/update fields beyond the status or finalizer fields - - apiGroups: - - k8s.cloudogu.com - resources: - - blueprints - verbs: - - get - - list - - patch - - update - - watch -# issue permissions to update the finalizer field that may control CR deletion - - apiGroups: - - k8s.cloudogu.com - resources: - - blueprints/finalizers - verbs: - - update -# issue permissions to update the status which contains blueprint processing data - - apiGroups: - - k8s.cloudogu.com - resources: - - blueprints/status - verbs: - - get - - patch - - update # issue secret handling for Dogus - apiGroups: - "" diff --git a/k8s/helm/templates/validate.yaml b/k8s/helm/templates/validate.yaml new file mode 100644 index 00000000..9626893a --- /dev/null +++ b/k8s/helm/templates/validate.yaml @@ -0,0 +1,3 @@ +{{- if not (.Capabilities.APIVersions.Has "k8s.cloudogu.com/v1/Component") -}} + {{- fail "CRD k8s.cloudogu.com/v1/Component is not installed." -}} +{{- end -}} \ No newline at end of file diff --git a/k8s/helm/values.yaml b/k8s/helm/values.yaml index f1ef7866..91353a5c 100644 --- a/k8s/helm/values.yaml +++ b/k8s/helm/values.yaml @@ -6,7 +6,7 @@ manager: image: registry: docker.io repository: cloudogu/k8s-blueprint-operator - tag: 2.7.0 + tag: 2.8.0 imagePullPolicy: IfNotPresent env: logLevel: info @@ -18,15 +18,6 @@ manager: memory: 105M networkPolicies: enabled: true -healthConfig: - components: - required: - - name: k8s-dogu-operator - - name: k8s-service-discovery - - name: k8s-component-operator - wait: - timeout: 10m - interval: 10s doguRegistry: certificate: secret: dogu-registry-cert diff --git a/pkg/adapter/config/kubernetes/doguConfigRepository.go b/pkg/adapter/config/kubernetes/doguConfigRepository.go index dbdad768..2bbeb6f9 100644 --- a/pkg/adapter/config/kubernetes/doguConfigRepository.go +++ b/pkg/adapter/config/kubernetes/doguConfigRepository.go @@ -3,6 +3,7 @@ package kubernetes import ( "context" "fmt" + cescommons "github.com/cloudogu/ces-commons-lib/dogu" liberrors "github.com/cloudogu/ces-commons-lib/errors" "github.com/cloudogu/k8s-registry-lib/config" diff --git a/pkg/adapter/doguregistry/doguDescriptorRepository.go b/pkg/adapter/doguregistry/doguDescriptorRepository.go new file mode 100644 index 00000000..617fa2c6 --- /dev/null +++ b/pkg/adapter/doguregistry/doguDescriptorRepository.go @@ -0,0 +1,96 @@ +package doguregistry + +import ( + "context" + "errors" + + cescommons "github.com/cloudogu/ces-commons-lib/dogu" + cloudoguerrors "github.com/cloudogu/ces-commons-lib/errors" + "github.com/cloudogu/cesapp-lib/core" + "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domainservice" + "github.com/go-logr/logr" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +type DoguDescriptorRepository struct { + remoteRepository remoteDoguDescriptorRepository + localRepository localDoguDescriptorRepository +} + +func NewDoguDescriptorRepository(remoteRepository remoteDoguDescriptorRepository, localRepository localDoguDescriptorRepository) *DoguDescriptorRepository { + return &DoguDescriptorRepository{remoteRepository: remoteRepository, localRepository: localRepository} +} + +func (r *DoguDescriptorRepository) GetDogu(ctx context.Context, qualifiedDoguVersion cescommons.QualifiedVersion) (*core.Dogu, error) { + logger := log.FromContext(ctx). + WithName("DoguDescriptorRepository.GetDogu"). + WithValues("dogu", qualifiedDoguVersion.Name.SimpleName) + + // Try to get the dogu from the local repository first. + dogu := r.getLocalDogu(ctx, qualifiedDoguVersion, logger) + if dogu != nil { + return dogu, nil + } + + dogu, err := r.getRemoteDogu(ctx, qualifiedDoguVersion) + if err != nil { + return nil, err + } + + err = r.localRepository.Add(ctx, qualifiedDoguVersion.Name.SimpleName, dogu) + if err != nil { + // just log the error, no need to fail the reconcilation + logger.Info("failed to add dogu to local repository", + "error", err, + "dogu", qualifiedDoguVersion.Name.SimpleName, + "version", qualifiedDoguVersion.Version.Raw) + } + return dogu, nil +} + +func (r *DoguDescriptorRepository) getRemoteDogu(ctx context.Context, qualifiedDoguVersion cescommons.QualifiedVersion) (*core.Dogu, error) { + // do not retry here. If any error happens, just reconcile later. We only do retries in application level. + // This makes the code way easier and non-blocking. + dogu, err := r.remoteRepository.Get(ctx, qualifiedDoguVersion) + if err != nil { + if cloudoguerrors.IsNotFoundError(err) { + return nil, domainservice.NewNotFoundError( + err, + "dogu %q with version %q could not be found", + qualifiedDoguVersion.Name, qualifiedDoguVersion.Version.Raw, + ) + } else { + return nil, domainservice.NewInternalError( + err, + "failed to get dogu %q with version %q", + qualifiedDoguVersion.Name, qualifiedDoguVersion.Version.Raw, + ) + } + } + return dogu, nil +} + +func (r *DoguDescriptorRepository) getLocalDogu(ctx context.Context, qualifiedDoguVersion cescommons.QualifiedVersion, logger logr.Logger) *core.Dogu { + dogu, err := r.localRepository.Get(ctx, cescommons.NewSimpleNameVersion(qualifiedDoguVersion.Name.SimpleName, qualifiedDoguVersion.Version)) + if err == nil { + logger.V(2).Info("local dogu descriptor hit", "dogu", qualifiedDoguVersion.Name.SimpleName) + return dogu + } else { + logger.V(2).Info("local dogu descriptor miss", "error", err) + return nil + } +} + +func (r *DoguDescriptorRepository) GetDogus(ctx context.Context, dogusToLoad []cescommons.QualifiedVersion) (map[cescommons.QualifiedName]*core.Dogu, error) { + dogus := make(map[cescommons.QualifiedName]*core.Dogu) + + var errs []error + for _, doguRef := range dogusToLoad { + dogu, err := r.GetDogu(ctx, doguRef) + errs = append(errs, err) + + dogus[doguRef.Name] = dogu + } + + return dogus, errors.Join(errs...) +} diff --git a/pkg/adapter/doguregistry/doguDescriptorRepository_test.go b/pkg/adapter/doguregistry/doguDescriptorRepository_test.go new file mode 100644 index 00000000..d450be9e --- /dev/null +++ b/pkg/adapter/doguregistry/doguDescriptorRepository_test.go @@ -0,0 +1,227 @@ +package doguregistry + +import ( + "context" + "fmt" + "testing" + + cescommons "github.com/cloudogu/ces-commons-lib/dogu" + cloudoguerrors "github.com/cloudogu/ces-commons-lib/errors" + "github.com/cloudogu/cesapp-lib/core" + "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domainservice" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestNewRemote(t *testing.T) { + // given + remoteRepoMock := newMockRemoteDoguDescriptorRepository(t) + localRepoMock := newMockLocalDoguDescriptorRepository(t) + + // when + actual := NewDoguDescriptorRepository(remoteRepoMock, localRepoMock) + + // then + assert.NotEmpty(t, actual) +} + +func TestRemote_GetDogu(t *testing.T) { + t.Run("should return not found error on remote miss", func(t *testing.T) { + // given + remoteRepoMock := newMockRemoteDoguDescriptorRepository(t) + localRepoMock := newMockLocalDoguDescriptorRepository(t) + version, err := core.ParseVersion("1.2.3") + require.NoError(t, err) + qSimpleDoguVersion := cescommons.SimpleNameVersion{Name: "my-dogu", Version: version} + // no local hit + localRepoMock.EXPECT().Get(context.TODO(), qSimpleDoguVersion).Return(nil, assert.AnError) + qDoguVersion := cescommons.QualifiedVersion{ + Name: cescommons.QualifiedName{Namespace: "testing", SimpleName: "my-dogu"}, + Version: version, + } + remoteRepoMock.EXPECT().Get(context.TODO(), qDoguVersion).Return(nil, cloudoguerrors.NewNotFoundError(cloudoguerrors.Error{})) + + sut := &DoguDescriptorRepository{remoteRepoMock, localRepoMock} + + // when + actual, err := sut.GetDogu(context.TODO(), qDoguVersion) + + // then + require.Error(t, err) + assert.Nil(t, actual) + assert.ErrorContains(t, err, "dogu \"testing/my-dogu\" with version \"1.2.3\" could not be found") + expectedErr := &domainservice.NotFoundError{} + assert.ErrorAs(t, err, &expectedErr) + }) + t.Run("should return internal error on remote error", func(t *testing.T) { + // given + remoteRepoMock := newMockRemoteDoguDescriptorRepository(t) + localRepoMock := newMockLocalDoguDescriptorRepository(t) + + version, err := core.ParseVersion("1.2.3") + require.NoError(t, err) + // no local hit + qSimpleDoguVersion := cescommons.SimpleNameVersion{Name: "my-dogu", Version: version} + localRepoMock.EXPECT().Get(context.TODO(), qSimpleDoguVersion).Return(nil, assert.AnError) + qDoguVersion := cescommons.QualifiedVersion{ + Name: cescommons.QualifiedName{Namespace: "testing", SimpleName: "my-dogu"}, + Version: version, + } + remoteRepoMock.EXPECT().Get(context.TODO(), qDoguVersion).Return(nil, assert.AnError) + + sut := &DoguDescriptorRepository{remoteRepoMock, localRepoMock} + + // when + actual, err := sut.GetDogu(context.TODO(), qDoguVersion) + + // then + require.Error(t, err) + assert.Nil(t, actual) + assert.ErrorContains(t, err, "failed to get dogu \"testing/my-dogu\" with version \"1.2.3\"") + assert.ErrorIs(t, err, assert.AnError) + expectedErr := &domainservice.InternalError{} + assert.ErrorAs(t, err, &expectedErr) + }) + t.Run("should return dogu on remote hit", func(t *testing.T) { + // given + expectedDogu := core.Dogu{Name: "testing/my-dogu", Version: "1.2.3"} + + remoteRepoMock := newMockRemoteDoguDescriptorRepository(t) + localRepoMock := newMockLocalDoguDescriptorRepository(t) + version, err := core.ParseVersion("1.2.3") + require.NoError(t, err) + // no local hit + qSimpleDoguVersion := cescommons.SimpleNameVersion{Name: "my-dogu", Version: version} + localRepoMock.EXPECT().Get(context.TODO(), qSimpleDoguVersion).Return(nil, assert.AnError) + qDoguVersion := cescommons.QualifiedVersion{ + Name: cescommons.QualifiedName{Namespace: "testing", SimpleName: "my-dogu"}, + Version: version, + } + remoteRepoMock.EXPECT().Get(context.TODO(), qDoguVersion).Return(&expectedDogu, nil) + localRepoMock.EXPECT().Add(context.TODO(), qDoguVersion.Name.SimpleName, &expectedDogu).Return(nil) + + sut := &DoguDescriptorRepository{remoteRepoMock, localRepoMock} + + // when + actual, err := sut.GetDogu(context.TODO(), qDoguVersion) + + // then + require.NoError(t, err) + assert.Equal(t, &expectedDogu, actual) + }) + t.Run("should return no error on local dogu description addition error", func(t *testing.T) { + // given + expectedDogu := core.Dogu{Name: "testing/my-dogu", Version: "1.2.3"} + + remoteRepoMock := newMockRemoteDoguDescriptorRepository(t) + localRepoMock := newMockLocalDoguDescriptorRepository(t) + version, err := core.ParseVersion("1.2.3") + require.NoError(t, err) + // no local hit + qSimpleDoguVersion := cescommons.SimpleNameVersion{Name: "my-dogu", Version: version} + localRepoMock.EXPECT().Get(context.TODO(), qSimpleDoguVersion).Return(nil, assert.AnError) + qDoguVersion := cescommons.QualifiedVersion{ + Name: cescommons.QualifiedName{Namespace: "testing", SimpleName: "my-dogu"}, + Version: version, + } + remoteRepoMock.EXPECT().Get(context.TODO(), qDoguVersion).Return(&expectedDogu, nil) + localRepoMock.EXPECT().Add(context.TODO(), qDoguVersion.Name.SimpleName, &expectedDogu).Return(assert.AnError) + + sut := &DoguDescriptorRepository{remoteRepoMock, localRepoMock} + + // when + actual, err := sut.GetDogu(context.TODO(), qDoguVersion) + + // then + require.NoError(t, err) + assert.Equal(t, &expectedDogu, actual) + }) + t.Run("should return dogu on local hit", func(t *testing.T) { + // given + expectedDogu := core.Dogu{Name: "testing/my-dogu", Version: "1.2.3"} + + localRepoMock := newMockLocalDoguDescriptorRepository(t) + version, err := core.ParseVersion("1.2.3") + require.NoError(t, err) + // no local hit + qSimpleDoguVersion := cescommons.SimpleNameVersion{Name: "my-dogu", Version: version} + localRepoMock.EXPECT().Get(context.TODO(), qSimpleDoguVersion).Return(&expectedDogu, nil) + + sut := &DoguDescriptorRepository{nil, localRepoMock} + + // when + qDoguVersion := cescommons.QualifiedVersion{ + Name: cescommons.QualifiedName{Namespace: "testing", SimpleName: "my-dogu"}, + Version: version, + } + actual, err := sut.GetDogu(context.TODO(), qDoguVersion) + + // then + require.NoError(t, err) + assert.Equal(t, &expectedDogu, actual) + }) +} + +func TestRemote_GetDogus(t *testing.T) { + t.Run("should return collected errors on remote calls", func(t *testing.T) { + // given + remoteRepoMock := newMockRemoteDoguDescriptorRepository(t) + localRepoMock := newMockLocalDoguDescriptorRepository(t) + expectedDogu := core.Dogu{Name: "testing/good-dogu", Version: "0.1.2"} + goodVersion, err := core.ParseVersion("0.1.2") + require.NoError(t, err) + NotFoundVersion, err := core.ParseVersion("1.2.3") + require.NoError(t, err) + OtherVersion, err := core.ParseVersion("2.3.4") + require.NoError(t, err) + qGoodDoguVersion := cescommons.QualifiedVersion{ + Name: cescommons.QualifiedName{Namespace: "testing", SimpleName: "good-dogu"}, + Version: goodVersion, + } + qNotFoundDoguVersion := cescommons.QualifiedVersion{ + Name: cescommons.QualifiedName{Namespace: "testing", SimpleName: "not-found"}, + Version: NotFoundVersion, + } + qOtherErrorDoguVersion := cescommons.QualifiedVersion{ + Name: cescommons.QualifiedName{Namespace: "testing", SimpleName: "other-error"}, + Version: OtherVersion, + } + // no local hits + localRepoMock.EXPECT().Get(context.TODO(), mock.Anything).Return(nil, assert.AnError) + remoteRepoMock.EXPECT().Get(context.TODO(), qGoodDoguVersion).Return(&expectedDogu, nil) + notFoundError := fmt.Errorf("404 not found") + remoteRepoMock.EXPECT().Get(context.TODO(), qNotFoundDoguVersion).Return(nil, notFoundError) + remoteRepoMock.EXPECT().Get(context.TODO(), qOtherErrorDoguVersion).Return(nil, assert.AnError) + localRepoMock.EXPECT().Add(context.TODO(), qGoodDoguVersion.Name.SimpleName, &expectedDogu).Return(nil) + + sut := &DoguDescriptorRepository{remoteRepoMock, localRepoMock} + dogusToLoad := []cescommons.QualifiedVersion{ + qGoodDoguVersion, + qOtherErrorDoguVersion, + qNotFoundDoguVersion, + } + + expectedDogus := map[cescommons.QualifiedName]*core.Dogu{ + cescommons.QualifiedName{Namespace: "testing", SimpleName: "good-dogu"}: &expectedDogu, + cescommons.QualifiedName{Namespace: "testing", SimpleName: "not-found"}: nil, + cescommons.QualifiedName{Namespace: "testing", SimpleName: "other-error"}: nil, + } + + // when + actual, err := sut.GetDogus(context.TODO(), dogusToLoad) + + // then + require.Error(t, err) + + assert.ErrorContains(t, err, "dogu \"testing/not-found\" with version \"1.2.3\": 404 not found") + assert.ErrorIs(t, err, notFoundError) + + assert.ErrorContains(t, err, "failed to get dogu \"testing/other-error\" with version \"2.3.4\"") + assert.ErrorIs(t, err, assert.AnError) + expectedInternal := &domainservice.InternalError{} + assert.ErrorAs(t, err, &expectedInternal) + + assert.Equal(t, expectedDogus, actual) + }) +} diff --git a/pkg/adapter/doguregistry/interfaces.go b/pkg/adapter/doguregistry/interfaces.go index 1930cf6a..4a814c0c 100644 --- a/pkg/adapter/doguregistry/interfaces.go +++ b/pkg/adapter/doguregistry/interfaces.go @@ -1,7 +1,17 @@ package doguregistry -import cescommons "github.com/cloudogu/ces-commons-lib/dogu" +import ( + "context" + + cescommons "github.com/cloudogu/ces-commons-lib/dogu" + "github.com/cloudogu/cesapp-lib/core" +) type remoteDoguDescriptorRepository interface { cescommons.RemoteDoguDescriptorRepository } + +type localDoguDescriptorRepository interface { + Get(ctx context.Context, doguVersion cescommons.SimpleNameVersion) (*core.Dogu, error) + Add(ctx context.Context, name cescommons.SimpleName, dogu *core.Dogu) error +} diff --git a/pkg/adapter/doguregistry/mock_localDoguDescriptorRepository_test.go b/pkg/adapter/doguregistry/mock_localDoguDescriptorRepository_test.go new file mode 100644 index 00000000..63dc1faa --- /dev/null +++ b/pkg/adapter/doguregistry/mock_localDoguDescriptorRepository_test.go @@ -0,0 +1,146 @@ +// Code generated by mockery v2.53.3. DO NOT EDIT. + +package doguregistry + +import ( + context "context" + + dogu "github.com/cloudogu/ces-commons-lib/dogu" + core "github.com/cloudogu/cesapp-lib/core" + + mock "github.com/stretchr/testify/mock" +) + +// mockLocalDoguDescriptorRepository is an autogenerated mock type for the localDoguDescriptorRepository type +type mockLocalDoguDescriptorRepository struct { + mock.Mock +} + +type mockLocalDoguDescriptorRepository_Expecter struct { + mock *mock.Mock +} + +func (_m *mockLocalDoguDescriptorRepository) EXPECT() *mockLocalDoguDescriptorRepository_Expecter { + return &mockLocalDoguDescriptorRepository_Expecter{mock: &_m.Mock} +} + +// Add provides a mock function with given fields: ctx, name, _a2 +func (_m *mockLocalDoguDescriptorRepository) Add(ctx context.Context, name dogu.SimpleName, _a2 *core.Dogu) error { + ret := _m.Called(ctx, name, _a2) + + if len(ret) == 0 { + panic("no return value specified for Add") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, dogu.SimpleName, *core.Dogu) error); ok { + r0 = rf(ctx, name, _a2) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// mockLocalDoguDescriptorRepository_Add_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Add' +type mockLocalDoguDescriptorRepository_Add_Call struct { + *mock.Call +} + +// Add is a helper method to define mock.On call +// - ctx context.Context +// - name dogu.SimpleName +// - _a2 *core.Dogu +func (_e *mockLocalDoguDescriptorRepository_Expecter) Add(ctx interface{}, name interface{}, _a2 interface{}) *mockLocalDoguDescriptorRepository_Add_Call { + return &mockLocalDoguDescriptorRepository_Add_Call{Call: _e.mock.On("Add", ctx, name, _a2)} +} + +func (_c *mockLocalDoguDescriptorRepository_Add_Call) Run(run func(ctx context.Context, name dogu.SimpleName, _a2 *core.Dogu)) *mockLocalDoguDescriptorRepository_Add_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(dogu.SimpleName), args[2].(*core.Dogu)) + }) + return _c +} + +func (_c *mockLocalDoguDescriptorRepository_Add_Call) Return(_a0 error) *mockLocalDoguDescriptorRepository_Add_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *mockLocalDoguDescriptorRepository_Add_Call) RunAndReturn(run func(context.Context, dogu.SimpleName, *core.Dogu) error) *mockLocalDoguDescriptorRepository_Add_Call { + _c.Call.Return(run) + return _c +} + +// Get provides a mock function with given fields: ctx, doguVersion +func (_m *mockLocalDoguDescriptorRepository) Get(ctx context.Context, doguVersion dogu.SimpleNameVersion) (*core.Dogu, error) { + ret := _m.Called(ctx, doguVersion) + + if len(ret) == 0 { + panic("no return value specified for Get") + } + + var r0 *core.Dogu + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, dogu.SimpleNameVersion) (*core.Dogu, error)); ok { + return rf(ctx, doguVersion) + } + if rf, ok := ret.Get(0).(func(context.Context, dogu.SimpleNameVersion) *core.Dogu); ok { + r0 = rf(ctx, doguVersion) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*core.Dogu) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, dogu.SimpleNameVersion) error); ok { + r1 = rf(ctx, doguVersion) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// mockLocalDoguDescriptorRepository_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get' +type mockLocalDoguDescriptorRepository_Get_Call struct { + *mock.Call +} + +// Get is a helper method to define mock.On call +// - ctx context.Context +// - doguVersion dogu.SimpleNameVersion +func (_e *mockLocalDoguDescriptorRepository_Expecter) Get(ctx interface{}, doguVersion interface{}) *mockLocalDoguDescriptorRepository_Get_Call { + return &mockLocalDoguDescriptorRepository_Get_Call{Call: _e.mock.On("Get", ctx, doguVersion)} +} + +func (_c *mockLocalDoguDescriptorRepository_Get_Call) Run(run func(ctx context.Context, doguVersion dogu.SimpleNameVersion)) *mockLocalDoguDescriptorRepository_Get_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(dogu.SimpleNameVersion)) + }) + return _c +} + +func (_c *mockLocalDoguDescriptorRepository_Get_Call) Return(_a0 *core.Dogu, _a1 error) *mockLocalDoguDescriptorRepository_Get_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *mockLocalDoguDescriptorRepository_Get_Call) RunAndReturn(run func(context.Context, dogu.SimpleNameVersion) (*core.Dogu, error)) *mockLocalDoguDescriptorRepository_Get_Call { + _c.Call.Return(run) + return _c +} + +// newMockLocalDoguDescriptorRepository creates a new instance of mockLocalDoguDescriptorRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func newMockLocalDoguDescriptorRepository(t interface { + mock.TestingT + Cleanup(func()) +}) *mockLocalDoguDescriptorRepository { + mock := &mockLocalDoguDescriptorRepository{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/adapter/doguregistry/remote.go b/pkg/adapter/doguregistry/remote.go deleted file mode 100644 index b482ff1e..00000000 --- a/pkg/adapter/doguregistry/remote.go +++ /dev/null @@ -1,62 +0,0 @@ -package doguregistry - -import ( - "context" - "errors" - "fmt" - cescommons "github.com/cloudogu/ces-commons-lib/dogu" - cloudoguerrors "github.com/cloudogu/ces-commons-lib/errors" - "github.com/cloudogu/cesapp-lib/core" - "github.com/cloudogu/retry-lib/retry" - - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domainservice" -) - -var maxTries = 20 - -type Remote struct { - repository remoteDoguDescriptorRepository -} - -func NewRemote(repository remoteDoguDescriptorRepository) *Remote { - return &Remote{repository: repository} -} - -func (r *Remote) GetDogu(ctx context.Context, qualifiedDoguVersion cescommons.QualifiedVersion) (*core.Dogu, error) { - dogu := &core.Dogu{} - err := retry.OnError(maxTries, cloudoguerrors.IsConnectionError, func() error { - var err error - dogu, err = r.repository.Get(ctx, qualifiedDoguVersion) - return err - }) - if err != nil { - // this is ugly, maybe do it better in cesapp-lib? - if cloudoguerrors.IsNotFoundError(err) { - return nil, &domainservice.NotFoundError{ - WrappedError: err, - Message: fmt.Sprintf("dogu %q with version %q could not be found", qualifiedDoguVersion.Name, qualifiedDoguVersion.Version.Raw), - } - } - - return nil, &domainservice.InternalError{ - WrappedError: err, - Message: fmt.Sprintf("failed to get dogu %q with version %q", qualifiedDoguVersion.Name, qualifiedDoguVersion.Version.Raw), - } - } - - return dogu, nil -} - -func (r *Remote) GetDogus(ctx context.Context, dogusToLoad []cescommons.QualifiedVersion) (map[cescommons.QualifiedName]*core.Dogu, error) { - dogus := make(map[cescommons.QualifiedName]*core.Dogu) - - var errs []error - for _, doguRef := range dogusToLoad { - dogu, err := r.GetDogu(ctx, doguRef) - errs = append(errs, err) - - dogus[doguRef.Name] = dogu - } - - return dogus, errors.Join(errs...) -} diff --git a/pkg/adapter/doguregistry/remote_test.go b/pkg/adapter/doguregistry/remote_test.go deleted file mode 100644 index e8e46b53..00000000 --- a/pkg/adapter/doguregistry/remote_test.go +++ /dev/null @@ -1,156 +0,0 @@ -package doguregistry - -import ( - "context" - "fmt" - cescommons "github.com/cloudogu/ces-commons-lib/dogu" - cloudoguerrors "github.com/cloudogu/ces-commons-lib/errors" - "github.com/cloudogu/cesapp-lib/core" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domainservice" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "testing" -) - -func TestNewRemote(t *testing.T) { - // given - repoMock := newMockRemoteDoguDescriptorRepository(t) - - // when - actual := NewRemote(repoMock) - - // then - assert.NotEmpty(t, actual) -} - -func TestRemote_GetDogu(t *testing.T) { - t.Run("should return not found error", func(t *testing.T) { - // given - repoMock := newMockRemoteDoguDescriptorRepository(t) - version, err := core.ParseVersion("1.2.3") - require.NoError(t, err) - qDoguVersion := cescommons.QualifiedVersion{ - Name: cescommons.QualifiedName{Namespace: "testing", SimpleName: "my-dogu"}, - Version: version, - } - repoMock.EXPECT().Get(context.TODO(), qDoguVersion).Return(nil, cloudoguerrors.NewNotFoundError(cloudoguerrors.Error{})) - - sut := &Remote{repoMock} - - // when - actual, err := sut.GetDogu(context.TODO(), qDoguVersion) - - // then - require.Error(t, err) - assert.Nil(t, actual) - assert.ErrorContains(t, err, "dogu \"testing/my-dogu\" with version \"1.2.3\" could not be found") - expectedErr := &domainservice.NotFoundError{} - assert.ErrorAs(t, err, &expectedErr) - }) - t.Run("should return internal error", func(t *testing.T) { - // given - repoMock := newMockRemoteDoguDescriptorRepository(t) - - version, err := core.ParseVersion("1.2.3") - require.NoError(t, err) - qDoguVersion := cescommons.QualifiedVersion{ - Name: cescommons.QualifiedName{Namespace: "testing", SimpleName: "my-dogu"}, - Version: version, - } - repoMock.EXPECT().Get(context.TODO(), qDoguVersion).Return(nil, assert.AnError) - - sut := &Remote{repoMock} - - // when - actual, err := sut.GetDogu(context.TODO(), qDoguVersion) - - // then - require.Error(t, err) - assert.Nil(t, actual) - assert.ErrorContains(t, err, "failed to get dogu \"testing/my-dogu\" with version \"1.2.3\"") - assert.ErrorIs(t, err, assert.AnError) - expectedErr := &domainservice.InternalError{} - assert.ErrorAs(t, err, &expectedErr) - }) - t.Run("should return dogu", func(t *testing.T) { - // given - expectedDogu := core.Dogu{Name: "testing/my-dogu", Version: "1.2.3"} - - repoMock := newMockRemoteDoguDescriptorRepository(t) - version, err := core.ParseVersion("1.2.3") - require.NoError(t, err) - qDoguVersion := cescommons.QualifiedVersion{ - Name: cescommons.QualifiedName{Namespace: "testing", SimpleName: "my-dogu"}, - Version: version, - } - repoMock.EXPECT().Get(context.TODO(), qDoguVersion).Return(&expectedDogu, nil) - - sut := &Remote{repoMock} - - // when - actual, err := sut.GetDogu(context.TODO(), qDoguVersion) - - // then - require.NoError(t, err) - assert.Equal(t, &expectedDogu, actual) - }) -} - -func TestRemote_GetDogus(t *testing.T) { - t.Run("should return collected errors", func(t *testing.T) { - // given - repoMock := newMockRemoteDoguDescriptorRepository(t) - expectedDogu := core.Dogu{Name: "testing/good-dogu", Version: "0.1.2"} - goodVersion, err := core.ParseVersion("0.1.2") - require.NoError(t, err) - NotFoundVersion, err := core.ParseVersion("1.2.3") - require.NoError(t, err) - OtherVersion, err := core.ParseVersion("2.3.4") - require.NoError(t, err) - qGoodDoguVersion := cescommons.QualifiedVersion{ - Name: cescommons.QualifiedName{Namespace: "testing", SimpleName: "good-dogu"}, - Version: goodVersion, - } - qNotFoundDoguVersion := cescommons.QualifiedVersion{ - Name: cescommons.QualifiedName{Namespace: "testing", SimpleName: "not-found"}, - Version: NotFoundVersion, - } - qOtherErrorDoguVersion := cescommons.QualifiedVersion{ - Name: cescommons.QualifiedName{Namespace: "testing", SimpleName: "other-error"}, - Version: OtherVersion, - } - repoMock.EXPECT().Get(context.TODO(), qGoodDoguVersion).Return(&expectedDogu, nil) - notFoundError := fmt.Errorf("404 not found") - repoMock.EXPECT().Get(context.TODO(), qNotFoundDoguVersion).Return(nil, notFoundError) - repoMock.EXPECT().Get(context.TODO(), qOtherErrorDoguVersion).Return(nil, assert.AnError) - - sut := &Remote{repoMock} - dogusToLoad := []cescommons.QualifiedVersion{ - qGoodDoguVersion, - qOtherErrorDoguVersion, - qNotFoundDoguVersion, - } - - expectedDogus := map[cescommons.QualifiedName]*core.Dogu{ - cescommons.QualifiedName{Namespace: "testing", SimpleName: "good-dogu"}: &expectedDogu, - cescommons.QualifiedName{Namespace: "testing", SimpleName: "not-found"}: nil, - cescommons.QualifiedName{Namespace: "testing", SimpleName: "other-error"}: nil, - } - - // when - actual, err := sut.GetDogus(context.TODO(), dogusToLoad) - - // then - require.Error(t, err) - - assert.ErrorContains(t, err, "dogu \"testing/not-found\" with version \"1.2.3\": 404 not found") - assert.ErrorIs(t, err, notFoundError) - - assert.ErrorContains(t, err, "failed to get dogu \"testing/other-error\" with version \"2.3.4\"") - assert.ErrorIs(t, err, assert.AnError) - expectedInternal := &domainservice.InternalError{} - assert.ErrorAs(t, err, &expectedInternal) - - assert.Equal(t, expectedDogus, actual) - }) -} diff --git a/pkg/adapter/kubernetes/blueprintcr/v2/blueprintSpecCRRepository.go b/pkg/adapter/kubernetes/blueprintcr/v2/blueprintSpecCRRepository.go index e8f3780c..dafe7bb1 100644 --- a/pkg/adapter/kubernetes/blueprintcr/v2/blueprintSpecCRRepository.go +++ b/pkg/adapter/kubernetes/blueprintcr/v2/blueprintSpecCRRepository.go @@ -4,11 +4,13 @@ import ( "context" "errors" "fmt" + v2 "github.com/cloudogu/k8s-blueprint-lib/v2/api/v2" serializerv2 "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/adapter/kubernetes/blueprintcr/v2/serializer" corev1 "k8s.io/api/core/v1" k8sErrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/log" bpv2client "github.com/cloudogu/k8s-blueprint-lib/v2/client" @@ -46,6 +48,7 @@ func (repo *blueprintSpecRepo) GetById(ctx context.Context, blueprintId string) return nil, &domainservice.NotFoundError{ WrappedError: err, Message: fmt.Sprintf("cannot load blueprint CR %q as it does not exist", blueprintId), + DoNotRetry: true, } } return nil, &domainservice.InternalError{ @@ -54,29 +57,62 @@ func (repo *blueprintSpecRepo) GetById(ctx context.Context, blueprintId string) } } - effectiveBlueprint, err := serializerv2.ConvertToEffectiveBlueprintDomain(blueprintCR.Status.EffectiveBlueprint) + effectiveBlueprint, err := convertBlueprintStatus(blueprintCR) if err != nil { return nil, err } - stateDiff, err := serializerv2.ConvertToStateDiffDomain(blueprintCR.Status.StateDiff) - if err != nil { - return nil, err + var conditions []domain.Condition + if blueprintCR.Status != nil && blueprintCR.Status.Conditions != nil { + conditions = blueprintCR.Status.Conditions } blueprintSpec := &domain.BlueprintSpec{ Id: blueprintId, + DisplayName: blueprintCR.Spec.DisplayName, EffectiveBlueprint: effectiveBlueprint, - StateDiff: stateDiff, + Conditions: conditions, Config: domain.BlueprintConfiguration{ - IgnoreDoguHealth: blueprintCR.Spec.IgnoreDoguHealth, - IgnoreComponentHealth: blueprintCR.Spec.IgnoreComponentHealth, - AllowDoguNamespaceSwitch: blueprintCR.Spec.AllowDoguNamespaceSwitch, - DryRun: blueprintCR.Spec.DryRun, + IgnoreDoguHealth: ptr.Deref(blueprintCR.Spec.IgnoreDoguHealth, false), + AllowDoguNamespaceSwitch: ptr.Deref(blueprintCR.Spec.AllowDoguNamespaceSwitch, false), + Stopped: ptr.Deref(blueprintCR.Spec.Stopped, false), }, - Status: domain.StatusPhase(blueprintCR.Status.Phase), } + err = repo.serializeBlueprintAndMask(blueprintSpec, blueprintCR, blueprintId) + if err != nil { + return nil, err + } + + setPersistenceContext(blueprintCR, blueprintSpec) + return blueprintSpec, nil +} + +func (repo *blueprintSpecRepo) CheckSingleton(ctx context.Context) error { + // Ask for just 2 items: enough to detect "more than one" + limit := int64(2) + + list, err := repo.blueprintClient.List(ctx, metav1.ListOptions{Limit: limit}) + if err != nil { + return &domainservice.InternalError{ + WrappedError: err, + Message: "error while listing blueprint resources", + } + } + + if list == nil { + return nil + } + + switch len(list.Items) { + case 0, 1: + return nil + default: + return &domain.MultipleBlueprintsError{Message: "more than one blueprint CR found"} + } +} + +func (repo *blueprintSpecRepo) serializeBlueprintAndMask(blueprintSpec *domain.BlueprintSpec, blueprintCR *v2.Blueprint, blueprintId string) error { blueprint, blueprintErr := serializerv2.ConvertToBlueprintDomain(blueprintCR.Spec.Blueprint) if blueprintErr != nil { blueprintErrorEvent := domain.BlueprintSpecInvalidEvent{ValidationError: blueprintErr} @@ -91,13 +127,24 @@ func (repo *blueprintSpecRepo) GetById(ctx context.Context, blueprintId string) serializationErr := errors.Join(blueprintErr, maskErr) if serializationErr != nil { - return nil, fmt.Errorf("could not deserialize blueprint CR %q: %w", blueprintId, serializationErr) + return fmt.Errorf("could not deserialize blueprint CR %q: %w", blueprintId, serializationErr) } - setPersistenceContext(blueprintCR, blueprintSpec) blueprintSpec.Blueprint = blueprint blueprintSpec.BlueprintMask = blueprintMask - return blueprintSpec, nil + return nil +} + +func convertBlueprintStatus(blueprintCR *v2.Blueprint) (domain.EffectiveBlueprint, error) { + var effectiveBlueprint domain.EffectiveBlueprint + var err error + if blueprintCR.Status != nil { + effectiveBlueprint, err = serializerv2.ConvertToEffectiveBlueprintDomain(blueprintCR.Status.EffectiveBlueprint) + if err != nil { + return domain.EffectiveBlueprint{}, err + } + } + return effectiveBlueprint, nil } // Update persists changes in the blueprint to the corresponding blueprint CR. @@ -117,14 +164,14 @@ func (repo *blueprintSpecRepo) Update(ctx context.Context, spec *domain.Blueprin ResourceVersion: persistenceContext.resourceVersion, CreationTimestamp: metav1.Time{}, }, - Status: v2.BlueprintStatus{ - Phase: v2.StatusPhase(spec.Status), - EffectiveBlueprint: effectiveBlueprint, + Status: &v2.BlueprintStatus{ + EffectiveBlueprint: &effectiveBlueprint, StateDiff: serializerv2.ConvertToStateDiffDTO(spec.StateDiff), + Conditions: spec.Conditions, }, } - logger.Info("update blueprint CR status") + logger.V(2).Info("update blueprint CR status") CRAfterUpdate, err := repo.blueprintClient.UpdateStatus(ctx, updatedBlueprint, metav1.UpdateOptions{}) if err != nil { diff --git a/pkg/adapter/kubernetes/blueprintcr/v2/blueprintSpecCRRepository_test.go b/pkg/adapter/kubernetes/blueprintcr/v2/blueprintSpecCRRepository_test.go index 3d41a6b4..bd66ea69 100644 --- a/pkg/adapter/kubernetes/blueprintcr/v2/blueprintSpecCRRepository_test.go +++ b/pkg/adapter/kubernetes/blueprintcr/v2/blueprintSpecCRRepository_test.go @@ -16,11 +16,21 @@ import ( bpv2 "github.com/cloudogu/k8s-blueprint-lib/v2/api/v2" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domainservice" ) -var ctx = context.Background() +var ( + ctx = context.Background() + testCondition = metav1.Condition{ + Type: domain.ConditionCompleted, + Status: metav1.ConditionUnknown, + ObservedGeneration: 1, + LastTransitionTime: metav1.Time{}, + Reason: "Completed", + Message: "test", + } + trueVar = true +) func Test_blueprintSpecRepo_GetById(t *testing.T) { blueprintId := "MyBlueprint" @@ -34,14 +44,57 @@ func Test_blueprintSpecRepo_GetById(t *testing.T) { cr := &bpv2.Blueprint{ TypeMeta: metav1.TypeMeta{}, ObjectMeta: metav1.ObjectMeta{ResourceVersion: "abc"}, - Spec: bpv2.BlueprintSpec{ + Spec: &bpv2.BlueprintSpec{ + DisplayName: "MyBlueprint", Blueprint: bpv2.BlueprintManifest{}, - BlueprintMask: bpv2.BlueprintMask{}, - AllowDoguNamespaceSwitch: true, + BlueprintMask: &bpv2.BlueprintMask{}, + AllowDoguNamespaceSwitch: &trueVar, + IgnoreDoguHealth: &trueVar, + Stopped: &trueVar, + }, + Status: &bpv2.BlueprintStatus{ + Conditions: []metav1.Condition{testCondition}, + }, + } + restClientMock.EXPECT().Get(ctx, blueprintId, metav1.GetOptions{}).Return(cr, nil) + + // when + spec, err := repo.GetById(ctx, blueprintId) + + // then + require.NoError(t, err) + persistenceContext := make(map[string]interface{}) + persistenceContext[blueprintSpecRepoContextKey] = blueprintSpecRepoContext{"abc"} + assert.Equal(t, &domain.BlueprintSpec{ + Id: blueprintId, + DisplayName: "MyBlueprint", + Config: domain.BlueprintConfiguration{ IgnoreDoguHealth: true, - DryRun: true, + AllowDoguNamespaceSwitch: true, + Stopped: true, + }, + StateDiff: domain.StateDiff{}, + PersistenceContext: persistenceContext, + Conditions: []domain.Condition{testCondition}, + }, spec) + }) + + t.Run("all ok without status", func(t *testing.T) { + // given + restClientMock := newMockBlueprintInterface(t) + eventRecorderMock := newMockEventRecorder(t) + repo := NewBlueprintSpecRepository(restClientMock, eventRecorderMock) + + cr := &bpv2.Blueprint{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ResourceVersion: "abc"}, + Spec: &bpv2.BlueprintSpec{ + Blueprint: bpv2.BlueprintManifest{}, + BlueprintMask: &bpv2.BlueprintMask{}, + AllowDoguNamespaceSwitch: &trueVar, + IgnoreDoguHealth: &trueVar, + Stopped: &trueVar, }, - Status: bpv2.BlueprintStatus{}, } restClientMock.EXPECT().Get(ctx, blueprintId, metav1.GetOptions{}).Return(cr, nil) @@ -57,10 +110,11 @@ func Test_blueprintSpecRepo_GetById(t *testing.T) { Config: domain.BlueprintConfiguration{ IgnoreDoguHealth: true, AllowDoguNamespaceSwitch: true, - DryRun: true, + Stopped: true, }, StateDiff: domain.StateDiff{}, PersistenceContext: persistenceContext, + Conditions: nil, }, spec) }) @@ -72,19 +126,19 @@ func Test_blueprintSpecRepo_GetById(t *testing.T) { cr := &bpv2.Blueprint{ TypeMeta: metav1.TypeMeta{}, ObjectMeta: metav1.ObjectMeta{ResourceVersion: "abc"}, - Spec: bpv2.BlueprintSpec{ + Spec: &bpv2.BlueprintSpec{ Blueprint: bpv2.BlueprintManifest{ Dogus: []bpv2.Dogu{ {Name: "invalid"}, }, }, - BlueprintMask: bpv2.BlueprintMask{ + BlueprintMask: &bpv2.BlueprintMask{ Dogus: []bpv2.MaskDogu{ {Name: "invalid"}, }, }, }, - Status: bpv2.BlueprintStatus{}, + Status: &bpv2.BlueprintStatus{}, } eventRecorderMock.EXPECT().Event(cr, "Warning", "BlueprintSpecInvalid", "cannot deserialize blueprint: cannot convert blueprint dogus: dogu name needs to be in the form 'namespace/dogu' but is 'invalid'") eventRecorderMock.EXPECT().Event(cr, "Warning", "BlueprintSpecInvalid", "cannot deserialize blueprint mask: cannot convert blueprint dogus: dogu name needs to be in the form 'namespace/dogu' but is 'invalid'") @@ -151,14 +205,13 @@ func Test_blueprintSpecRepo_Update(t *testing.T) { restClientMock := newMockBlueprintInterface(t) eventRecorderMock := newMockEventRecorder(t) repo := NewBlueprintSpecRepository(restClientMock, eventRecorderMock) - expectedStatus := bpv2.BlueprintStatus{ - Phase: bpv2.StatusPhaseValidated, - EffectiveBlueprint: bpv2.BlueprintManifest{ - Dogus: []bpv2.Dogu{}, - Components: []bpv2.Component{}, - Config: bpv2.Config{}, + expectedStatus := &bpv2.BlueprintStatus{ + EffectiveBlueprint: &bpv2.BlueprintManifest{ + Dogus: []bpv2.Dogu{}, + Config: nil, }, - StateDiff: bpv2.StateDiff{DoguDiffs: map[string]bpv2.DoguDiff{}, ComponentDiffs: map[string]bpv2.ComponentDiff{}}, + StateDiff: &bpv2.StateDiff{DoguDiffs: map[string]bpv2.DoguDiff{}}, + Conditions: []metav1.Condition{testCondition}, } restClientMock.EXPECT(). UpdateStatus(ctx, mock.Anything, metav1.UpdateOptions{}). @@ -172,9 +225,9 @@ func Test_blueprintSpecRepo_Update(t *testing.T) { persistenceContext[blueprintSpecRepoContextKey] = blueprintSpecRepoContext{"abc"} err := repo.Update(ctx, &domain.BlueprintSpec{ Id: blueprintId, - Status: domain.StatusPhaseValidated, Events: nil, PersistenceContext: persistenceContext, + Conditions: []domain.Condition{testCondition}, }) // then @@ -190,7 +243,6 @@ func Test_blueprintSpecRepo_Update(t *testing.T) { // when err := repo.Update(ctx, &domain.BlueprintSpec{ Id: blueprintId, - Status: domain.StatusPhaseValidated, Events: nil, }) @@ -210,7 +262,6 @@ func Test_blueprintSpecRepo_Update(t *testing.T) { persistenceContext[blueprintSpecRepoContextKey] = 1 err := repo.Update(ctx, &domain.BlueprintSpec{ Id: blueprintId, - Status: domain.StatusPhaseValidated, Events: nil, PersistenceContext: persistenceContext, }) @@ -225,14 +276,13 @@ func Test_blueprintSpecRepo_Update(t *testing.T) { restClientMock := newMockBlueprintInterface(t) eventRecorderMock := newMockEventRecorder(t) repo := NewBlueprintSpecRepository(restClientMock, eventRecorderMock) - expectedStatus := bpv2.BlueprintStatus{ - Phase: bpv2.StatusPhaseValidated, - EffectiveBlueprint: bpv2.BlueprintManifest{ - Dogus: []bpv2.Dogu{}, - Components: []bpv2.Component{}, - Config: bpv2.Config{}, + expectedStatus := &bpv2.BlueprintStatus{ + EffectiveBlueprint: &bpv2.BlueprintManifest{ + Dogus: []bpv2.Dogu{}, + Config: nil, }, - StateDiff: bpv2.StateDiff{DoguDiffs: map[string]bpv2.DoguDiff{}, ComponentDiffs: map[string]bpv2.ComponentDiff{}}, + StateDiff: &bpv2.StateDiff{DoguDiffs: map[string]bpv2.DoguDiff{}}, + Conditions: []metav1.Condition{}, } expectedError := k8sErrors.NewConflict( schema.GroupResource{Group: "blueprints", Resource: blueprintId}, @@ -251,9 +301,9 @@ func Test_blueprintSpecRepo_Update(t *testing.T) { persistenceContext[blueprintSpecRepoContextKey] = blueprintSpecRepoContext{"abc"} err := repo.Update(ctx, &domain.BlueprintSpec{ Id: blueprintId, - Status: domain.StatusPhaseValidated, Events: nil, PersistenceContext: persistenceContext, + Conditions: []domain.Condition{}, }) // then @@ -268,14 +318,13 @@ func Test_blueprintSpecRepo_Update(t *testing.T) { restClientMock := newMockBlueprintInterface(t) eventRecorderMock := newMockEventRecorder(t) repo := NewBlueprintSpecRepository(restClientMock, eventRecorderMock) - expectedStatus := bpv2.BlueprintStatus{ - Phase: bpv2.StatusPhaseValidated, - EffectiveBlueprint: bpv2.BlueprintManifest{ - Dogus: []bpv2.Dogu{}, - Components: []bpv2.Component{}, - Config: bpv2.Config{}, + expectedStatus := &bpv2.BlueprintStatus{ + EffectiveBlueprint: &bpv2.BlueprintManifest{ + Dogus: []bpv2.Dogu{}, + Config: nil, }, - StateDiff: bpv2.StateDiff{DoguDiffs: map[string]bpv2.DoguDiff{}, ComponentDiffs: map[string]bpv2.ComponentDiff{}}, + StateDiff: &bpv2.StateDiff{DoguDiffs: map[string]bpv2.DoguDiff{}}, + Conditions: []metav1.Condition{}, } expectedError := fmt.Errorf("test-error") restClientMock.EXPECT(). @@ -290,9 +339,9 @@ func Test_blueprintSpecRepo_Update(t *testing.T) { persistenceContext[blueprintSpecRepoContextKey] = blueprintSpecRepoContext{"abc"} err := repo.Update(ctx, &domain.BlueprintSpec{ Id: blueprintId, - Status: domain.StatusPhaseValidated, Events: nil, PersistenceContext: persistenceContext, + Conditions: []domain.Condition{}, }) // then @@ -320,28 +369,21 @@ func Test_blueprintSpecRepo_Update_publishEvents(t *testing.T) { var events []domain.Event events = append(events, - domain.BlueprintSpecStaticallyValidatedEvent{}, - domain.BlueprintSpecValidatedEvent{}, - domain.EffectiveBlueprintCalculatedEvent{}, - domain.StateDiffDoguDeterminedEvent{}, - domain.StateDiffComponentDeterminedEvent{}, - domain.EcosystemHealthyUpfrontEvent{}, - domain.EcosystemUnhealthyUpfrontEvent{HealthResult: ecosystem.HealthResult{}}, + domain.StateDiffDeterminedEvent{}, domain.BlueprintSpecInvalidEvent{ValidationError: errors.New("test-error")}, ) - eventRecorderMock.EXPECT().Event(mock.Anything, corev1.EventTypeNormal, "BlueprintSpecStaticallyValidated", "") - eventRecorderMock.EXPECT().Event(mock.Anything, corev1.EventTypeNormal, "BlueprintSpecValidated", "") - eventRecorderMock.EXPECT().Event(mock.Anything, corev1.EventTypeNormal, "EffectiveBlueprintCalculated", "") - eventRecorderMock.EXPECT().Event(mock.Anything, corev1.EventTypeNormal, "StateDiffDoguDetermined", "dogu state diff determined: 0 actions ()") - eventRecorderMock.EXPECT().Event(mock.Anything, corev1.EventTypeNormal, "StateDiffComponentDetermined", "component state diff determined: 0 actions ()") - eventRecorderMock.EXPECT().Event(mock.Anything, corev1.EventTypeNormal, "EcosystemHealthyUpfront", "dogu health ignored: false; component health ignored: false") - eventRecorderMock.EXPECT().Event(mock.Anything, corev1.EventTypeNormal, "EcosystemUnhealthyUpfront", "ecosystem health:\n 0 dogu(s) are unhealthy: \n 0 component(s) are unhealthy: ") + eventRecorderMock.EXPECT().Event(mock.Anything, corev1.EventTypeNormal, "StateDiffDetermined", "state diff determined:\n 0 config changes ()\n 0 dogu actions ()") eventRecorderMock.EXPECT().Event(mock.Anything, corev1.EventTypeNormal, "BlueprintSpecInvalid", "test-error") // when persistenceContext := make(map[string]interface{}) persistenceContext[blueprintSpecRepoContextKey] = blueprintSpecRepoContext{"abc"} - spec := &domain.BlueprintSpec{Id: blueprintId, Events: events, PersistenceContext: persistenceContext} + spec := &domain.BlueprintSpec{ + Id: blueprintId, + Events: events, + PersistenceContext: persistenceContext, + Conditions: []domain.Condition{}, + } err := repo.Update(ctx, spec) // then @@ -351,3 +393,101 @@ func Test_blueprintSpecRepo_Update_publishEvents(t *testing.T) { assert.Empty(t, spec.Events, "events in aggregate should be deleted after publishing them") }) } + +func Test_blueprintSpecRepo_CheckSingleton(t *testing.T) { + t.Run("No error when no blueprints found", func(t *testing.T) { + restClientMock := newMockBlueprintInterface(t) + eventRecorderMock := newMockEventRecorder(t) + repo := NewBlueprintSpecRepository(restClientMock, eventRecorderMock) + + restClientMock.EXPECT().List(ctx, metav1.ListOptions{Limit: 2}).Return(nil, nil) + + // when + err := repo.CheckSingleton(ctx) + + // then + require.NoError(t, err) + }) + + t.Run("No error on empty blueprintList", func(t *testing.T) { + restClientMock := newMockBlueprintInterface(t) + eventRecorderMock := newMockEventRecorder(t) + repo := NewBlueprintSpecRepository(restClientMock, eventRecorderMock) + restClientMock.EXPECT().List(ctx, metav1.ListOptions{Limit: 2}).Return(&bpv2.BlueprintList{}, nil) + + // when + err := repo.CheckSingleton(ctx) + + // then + require.NoError(t, err) + }) + + t.Run("No error on single blueprint resource", func(t *testing.T) { + restClientMock := newMockBlueprintInterface(t) + eventRecorderMock := newMockEventRecorder(t) + repo := NewBlueprintSpecRepository(restClientMock, eventRecorderMock) + list := &bpv2.BlueprintList{ + Items: []bpv2.Blueprint{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "blueprint-1", + }, + }, + }, + } + restClientMock.EXPECT().List(ctx, metav1.ListOptions{Limit: 2}).Return(list, nil) + + // when + err := repo.CheckSingleton(ctx) + + // then + require.NoError(t, err) + }) + + t.Run("MultipleBlueprintsError on two blueprint resources", func(t *testing.T) { + restClientMock := newMockBlueprintInterface(t) + eventRecorderMock := newMockEventRecorder(t) + repo := NewBlueprintSpecRepository(restClientMock, eventRecorderMock) + list := &bpv2.BlueprintList{ + Items: []bpv2.Blueprint{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "blueprint-1", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "blueprint-2", + }, + }, + }, + } + restClientMock.EXPECT().List(ctx, metav1.ListOptions{Limit: 2}).Return(list, nil) + + // when + err := repo.CheckSingleton(ctx) + + // then + require.Error(t, err) + var targetErr *domain.MultipleBlueprintsError + assert.ErrorAs(t, err, &targetErr) + assert.ErrorContains(t, err, "more than one blueprint CR found") + }) + + t.Run("InternalError on List error", func(t *testing.T) { + restClientMock := newMockBlueprintInterface(t) + eventRecorderMock := newMockEventRecorder(t) + repo := NewBlueprintSpecRepository(restClientMock, eventRecorderMock) + + restClientMock.EXPECT().List(ctx, metav1.ListOptions{Limit: 2}).Return(nil, assert.AnError) + + // when + err := repo.CheckSingleton(ctx) + + // then + require.Error(t, err) + var targetErr *domainservice.InternalError + assert.ErrorAs(t, err, &targetErr) + assert.ErrorContains(t, err, "error while listing blueprint resources") + }) +} diff --git a/pkg/adapter/kubernetes/blueprintcr/v2/serializer/blueprint.go b/pkg/adapter/kubernetes/blueprintcr/v2/serializer/blueprint.go index c138eee9..d84bec93 100644 --- a/pkg/adapter/kubernetes/blueprintcr/v2/serializer/blueprint.go +++ b/pkg/adapter/kubernetes/blueprintcr/v2/serializer/blueprint.go @@ -1,54 +1,53 @@ package serializer import ( - "errors" "fmt" + crd "github.com/cloudogu/k8s-blueprint-lib/v2/api/v2" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" ) func ConvertToBlueprintDTO(blueprint domain.EffectiveBlueprint) crd.BlueprintManifest { return crd.BlueprintManifest{ - Dogus: ConvertToDoguDTOs(blueprint.Dogus), - Components: ConvertToComponentDTOs(blueprint.Components), - Config: ConvertToConfigDTO(blueprint.Config), + Dogus: ConvertToDoguDTOs(blueprint.Dogus), + Config: ConvertToConfigDTO(blueprint.Config), } } func ConvertToBlueprintDomain(blueprint crd.BlueprintManifest) (domain.Blueprint, error) { - convertedDogus, doguErr := ConvertDogus(blueprint.Dogus) - convertedComponents, compErr := ConvertComponents(blueprint.Components) - - err := errors.Join(doguErr, compErr) + convertedDogus, err := ConvertDogus(blueprint.Dogus) if err != nil { return domain.Blueprint{}, &domain.InvalidBlueprintError{ WrappedError: err, Message: "cannot deserialize blueprint", } } + configDomain := ConvertToConfigDomain(blueprint.Config) return domain.Blueprint{ - Dogus: convertedDogus, - Components: convertedComponents, - Config: ConvertToConfigDomain(blueprint.Config), + Dogus: convertedDogus, + Config: configDomain, }, nil } -func ConvertToEffectiveBlueprintDomain(blueprint crd.BlueprintManifest) (domain.EffectiveBlueprint, error) { - convertedDogus, doguErr := ConvertDogus(blueprint.Dogus) - convertedComponents, compErr := ConvertComponents(blueprint.Components) - - err := errors.Join(doguErr, compErr) +func ConvertToEffectiveBlueprintDomain(blueprint *crd.BlueprintManifest) (domain.EffectiveBlueprint, error) { + if blueprint == nil { + return domain.EffectiveBlueprint{}, nil + } + convertedDogus, err := ConvertDogus(blueprint.Dogus) if err != nil { return domain.EffectiveBlueprint{}, fmt.Errorf("cannot deserialize effective blueprint: %w", err) } return domain.EffectiveBlueprint{ - Dogus: convertedDogus, - Components: convertedComponents, - Config: ConvertToConfigDomain(blueprint.Config), + Dogus: convertedDogus, + Config: ConvertToConfigDomain(blueprint.Config), }, nil } -func ConvertToBlueprintMaskDomain(mask crd.BlueprintMask) (domain.BlueprintMask, error) { +func ConvertToBlueprintMaskDomain(mask *crd.BlueprintMask) (domain.BlueprintMask, error) { + if mask == nil { + return domain.BlueprintMask{}, nil + } + convertedDogus, err := ConvertMaskDogus(mask.Dogus) if err != nil { diff --git a/pkg/adapter/kubernetes/blueprintcr/v2/serializer/blueprint_test.go b/pkg/adapter/kubernetes/blueprintcr/v2/serializer/blueprint_test.go index eec7dab0..06358ce0 100644 --- a/pkg/adapter/kubernetes/blueprintcr/v2/serializer/blueprint_test.go +++ b/pkg/adapter/kubernetes/blueprintcr/v2/serializer/blueprint_test.go @@ -2,10 +2,11 @@ package serializer import ( "fmt" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" "testing" - "github.com/Masterminds/semver/v3" + "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" + "github.com/cloudogu/k8s-registry-lib/config" + "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -14,32 +15,24 @@ import ( crd "github.com/cloudogu/k8s-blueprint-lib/v2/api/v2" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/common" ) var ( - version3211, _ = core.ParseVersion("3.2.1-1") - version3212, _ = core.ParseVersion("3.2.1-2") - version1_2_3_3, _ = core.ParseVersion("1.2.3-3") + version3211, _ = core.ParseVersion("3.2.1-1") + version3212, _ = core.ParseVersion("3.2.1-2") + version1233, _ = core.ParseVersion("1.2.3-3") ) var ( - compVersion1233 = semver.MustParse("1.2.3-3") - compVersion3211 = semver.MustParse("3.2.1-1") - compVersion3212 = semver.MustParse("3.2.1-2") + trueVar = true + falseVar = false ) -var emptyPlatformConfig = crd.PlatformConfig{ - ResourceConfig: crd.ResourceConfig{ - MinVolumeSize: "0", - }, -} - func TestConvertToBlueprintDTO(t *testing.T) { t.Run("convert dogus", func(t *testing.T) { dogus := []domain.Dogu{ - {Name: cescommons.QualifiedName{Namespace: "official", SimpleName: "dogu1"}, Version: version3211, TargetState: domain.TargetStateAbsent}, - {Name: cescommons.QualifiedName{Namespace: "premium", SimpleName: "dogu3"}, Version: version3212, TargetState: domain.TargetStatePresent}, + {Name: cescommons.QualifiedName{Namespace: "official", SimpleName: "dogu1"}, Version: &version3211, Absent: true}, + {Name: cescommons.QualifiedName{Namespace: "premium", SimpleName: "dogu3"}, Version: &version3212, Absent: false}, } blueprint := domain.EffectiveBlueprint{ Dogus: dogus, @@ -50,222 +43,212 @@ func TestConvertToBlueprintDTO(t *testing.T) { //then convertedDogus := []crd.Dogu{ - {Name: "official/dogu1", PlatformConfig: emptyPlatformConfig, Version: version3211.Raw, Absent: true}, - {Name: "premium/dogu3", PlatformConfig: emptyPlatformConfig, Version: version3212.Raw, Absent: false}, - } - - assert.Equal(t, crd.BlueprintManifest{ - Dogus: convertedDogus, - Components: []crd.Component{}, - }, blueprintV2) - }) - - t.Run("convert components", func(t *testing.T) { - components := []domain.Component{ - {Name: common.QualifiedComponentName{SimpleName: "component1", Namespace: "k8s"}, Version: nil, TargetState: domain.TargetStateAbsent}, - {Name: common.QualifiedComponentName{SimpleName: "component3", Namespace: "k8s-testing"}, Version: compVersion3212, TargetState: domain.TargetStatePresent}, - } - blueprint := domain.EffectiveBlueprint{ - Components: components, + {Name: "official/dogu1", Version: &version3211.Raw, Absent: &trueVar}, + {Name: "premium/dogu3", Version: &version3212.Raw, Absent: &falseVar}, } - - //when - blueprintV2 := ConvertToBlueprintDTO(blueprint) - - //then - convertedComponents := []crd.Component{ - {Name: "k8s/component1", Absent: true}, - {Name: "k8s-testing/component3", Version: compVersion3212.String(), Absent: false}, + expectedManifest := crd.BlueprintManifest{ + Dogus: convertedDogus, } - - assert.Equal(t, crd.BlueprintManifest{ - Dogus: []crd.Dogu{}, - Components: convertedComponents, - }, blueprintV2) + assert.Empty(t, cmp.Diff(expectedManifest, blueprintV2)) }) t.Run("convert config", func(t *testing.T) { + value42 := "42" blueprint := domain.EffectiveBlueprint{ Config: domain.Config{ - Dogus: map[cescommons.SimpleName]domain.CombinedDoguConfig{ + Dogus: map[cescommons.SimpleName]domain.DoguConfigEntries{ "my-dogu": { - Config: domain.DoguConfig{ - Present: map[common.DoguConfigKey]common.DoguConfigValue{ - { - DoguName: "my-dogu", - Key: "config", - }: "42", - }, + { + Key: "config", + Value: (*config.Value)(&value42), }, - SensitiveConfig: domain.SensitiveDoguConfig{ - Present: map[common.SensitiveDoguConfigKey]domain.SensitiveValueRef{ - { - DoguName: "my-dogu", - Key: "sensitive-config", - }: { - SecretName: "mySecret", - SecretKey: "myKey", - }, + { + Key: "sensitive-config", + Sensitive: true, + SecretRef: &domain.SensitiveValueRef{ + SecretName: "mySecret", + SecretKey: "myKey", }, }, }, }, - Global: domain.GlobalConfig{Absent: []common.GlobalConfigKey{"test/key"}}, + Global: domain.GlobalConfigEntries{ + { + Key: "test/key", + Absent: true, + }, + }, }, } blueprintV2 := ConvertToBlueprintDTO(blueprint) - assert.Equal(t, crd.BlueprintManifest{ - Dogus: []crd.Dogu{}, - Components: []crd.Component{}, - Config: crd.Config{ - Dogus: map[string]crd.CombinedDoguConfig{ + expectedManifest := crd.BlueprintManifest{ + Dogus: []crd.Dogu{}, + Config: &crd.Config{ + Dogus: map[string][]crd.ConfigEntry{ "my-dogu": { - Config: &crd.DoguConfig{ - Present: map[string]string{ - "config": "42", - }, + crd.ConfigEntry{ + Key: "config", + Value: &value42, }, - SensitiveConfig: &crd.SensitiveDoguConfig{ - Present: []crd.SensitiveConfigEntry{ - { - Key: "sensitive-config", - SecretName: "mySecret", - SecretKey: "myKey", - }, + crd.ConfigEntry{ + Key: "sensitive-config", + Sensitive: &trueVar, + SecretRef: &crd.SecretReference{ + Name: "mySecret", + Key: "myKey", }, }, }, }, - Global: crd.GlobalConfig{ - Absent: []string{"test/key"}, + Global: []crd.ConfigEntry{ + { + Key: "test/key", + Absent: &trueVar, + }, }, }, - }, blueprintV2) + } + assert.Empty(t, cmp.Diff(expectedManifest, blueprintV2)) }) } func TestConvertToEffectiveBlueprintDomain(t *testing.T) { - //given - convertedDogus := []crd.Dogu{ - {Name: "official/dogu1", Version: version3211.Raw, Absent: true}, - {Name: "official/dogu2", Absent: true}, - {Name: "premium/dogu3", Version: version3212.Raw, Absent: false}, - {Name: "premium/dogu4", Version: version1_2_3_3.Raw, Absent: false}, - { - Name: "premium/dogu5", - Version: version1_2_3_3.Raw, - Absent: false, - PlatformConfig: crd.PlatformConfig{ - ResourceConfig: crd.ResourceConfig{}, - ReverseProxyConfig: crd.ReverseProxyConfig{}, - AdditionalMountsConfig: []crd.AdditionalMount{ - { - SourceType: crd.DataSourceConfigMap, - Name: "config", - Volume: "volume", - Subfolder: "subfolder", + t.Run("all ok", func(t *testing.T) { + //given + subfolder := "subfolder" + convertedDogus := []crd.Dogu{ + {Name: "official/dogu1", Version: &version3211.Raw, Absent: &trueVar}, + {Name: "official/dogu2", Absent: &trueVar}, + {Name: "premium/dogu3", Version: &version3212.Raw, Absent: &falseVar}, + {Name: "premium/dogu4", Version: &version1233.Raw, Absent: &falseVar}, + { + Name: "premium/dogu5", + Version: &version1233.Raw, + Absent: &falseVar, + PlatformConfig: &crd.PlatformConfig{ + ResourceConfig: nil, + ReverseProxyConfig: nil, + AdditionalMountsConfig: []crd.AdditionalMount{ + { + SourceType: crd.DataSourceConfigMap, + Name: "config", + Volume: "volume", + Subfolder: &subfolder, + }, }, }, }, - }, - } - - convertedComponents := []crd.Component{ - {Name: "k8s/component1", Version: version3211.Raw, Absent: true}, - {Name: "k8s/component2", Absent: true}, - {Name: "k8s-testing/component3", Version: version3212.Raw, Absent: false}, - {Name: "k8s-testing/component4", Version: version1_2_3_3.Raw, Absent: false}, - } + } - dto := crd.BlueprintManifest{ - Dogus: convertedDogus, - Components: convertedComponents, - Config: crd.Config{ - Dogus: map[string]crd.CombinedDoguConfig{ - "my-dogu": { - Config: &crd.DoguConfig{ - Present: map[string]string{ - "config": "42", + value42 := "42" + dto := crd.BlueprintManifest{ + Dogus: convertedDogus, + Config: &crd.Config{ + Dogus: map[string][]crd.ConfigEntry{ + "my-dogu": { + crd.ConfigEntry{ + Key: "config", + Value: &value42, }, - }, - SensitiveConfig: &crd.SensitiveDoguConfig{ - Present: []crd.SensitiveConfigEntry{ - { - Key: "sensitive-config", - SecretName: "mySecret", - SecretKey: "myKey", + crd.ConfigEntry{ + Key: "sensitive-config", + Sensitive: &trueVar, + SecretRef: &crd.SecretReference{ + Name: "mySecret", + Key: "myKey", }, }, }, }, + Global: []crd.ConfigEntry{ + { + Key: "test/key", + Absent: &trueVar, + }, + }, }, - Global: crd.GlobalConfig{Absent: []string{"test/key"}}, - }, - } - //when - blueprint, err := ConvertToEffectiveBlueprintDomain(dto) + } + //when + blueprint, err := ConvertToEffectiveBlueprintDomain(&dto) - //then - dogus := []domain.Dogu{ - {Name: cescommons.QualifiedName{Namespace: "official", SimpleName: "dogu1"}, Version: version3211, TargetState: domain.TargetStateAbsent}, - {Name: cescommons.QualifiedName{Namespace: "official", SimpleName: "dogu2"}, TargetState: domain.TargetStateAbsent}, - {Name: cescommons.QualifiedName{Namespace: "premium", SimpleName: "dogu3"}, Version: version3212, TargetState: domain.TargetStatePresent}, - {Name: cescommons.QualifiedName{Namespace: "premium", SimpleName: "dogu4"}, Version: version1_2_3_3}, - { - Name: cescommons.QualifiedName{Namespace: "premium", SimpleName: "dogu5"}, - Version: version1_2_3_3, - AdditionalMounts: []ecosystem.AdditionalMount{ - { - SourceType: ecosystem.DataSourceConfigMap, - Name: "config", - Volume: "volume", - Subfolder: "subfolder", + //then + dogus := []domain.Dogu{ + {Name: cescommons.QualifiedName{Namespace: "official", SimpleName: "dogu1"}, Version: &version3211, Absent: true}, + {Name: cescommons.QualifiedName{Namespace: "official", SimpleName: "dogu2"}, Absent: true}, + {Name: cescommons.QualifiedName{Namespace: "premium", SimpleName: "dogu3"}, Version: &version3212, Absent: false}, + {Name: cescommons.QualifiedName{Namespace: "premium", SimpleName: "dogu4"}, Version: &version1233}, + { + Name: cescommons.QualifiedName{Namespace: "premium", SimpleName: "dogu5"}, + Version: &version1233, + AdditionalMounts: []ecosystem.AdditionalMount{ + { + SourceType: ecosystem.DataSourceConfigMap, + Name: "config", + Volume: "volume", + Subfolder: subfolder, + }, }, }, - }, - } + } - components := []domain.Component{ - {Name: common.QualifiedComponentName{Namespace: "k8s", SimpleName: "component1"}, Version: compVersion3211, TargetState: domain.TargetStateAbsent}, - {Name: common.QualifiedComponentName{Namespace: "k8s", SimpleName: "component2"}, TargetState: domain.TargetStateAbsent}, - {Name: common.QualifiedComponentName{Namespace: "k8s-testing", SimpleName: "component3"}, Version: compVersion3212, TargetState: domain.TargetStatePresent}, - {Name: common.QualifiedComponentName{Namespace: "k8s-testing", SimpleName: "component4"}, Version: compVersion1233}, - } - expected := domain.EffectiveBlueprint{ - Dogus: dogus, - Components: components, - Config: domain.Config{ - Dogus: map[cescommons.SimpleName]domain.CombinedDoguConfig{ - "my-dogu": { - DoguName: "my-dogu", - Config: domain.DoguConfig{ - Present: map[common.DoguConfigKey]common.DoguConfigValue{ - { - DoguName: "my-dogu", - Key: "config", - }: "42", + expected := domain.EffectiveBlueprint{ + Dogus: dogus, + Config: domain.Config{ + Dogus: map[cescommons.SimpleName]domain.DoguConfigEntries{ + "my-dogu": { + { + Key: "config", + Value: (*config.Value)(&value42), }, - }, - SensitiveConfig: domain.SensitiveDoguConfig{ - Present: map[common.SensitiveDoguConfigKey]domain.SensitiveValueRef{ - { - DoguName: "my-dogu", - Key: "sensitive-config", - }: { + { + Key: "sensitive-config", + Sensitive: true, + SecretRef: &domain.SensitiveValueRef{ SecretName: "mySecret", SecretKey: "myKey", }, }, }, }, + Global: domain.GlobalConfigEntries{ + { + Key: "test/key", + Absent: true, + }, + }, }, - Global: domain.GlobalConfig{Absent: []common.GlobalConfigKey{"test/key"}}, - }, - } + } + + require.NoError(t, err) + assert.Empty(t, cmp.Diff(expected, blueprint)) + }) + + t.Run("when manifest is nil return empty effective blueprint", func(t *testing.T) { + //when + blueprint, err := ConvertToEffectiveBlueprintDomain(nil) + + //then + require.NoError(t, err) + assert.Equal(t, domain.EffectiveBlueprint{}, blueprint) + }) + + t.Run("when convert dogu error return empty effective blueprint and error", func(t *testing.T) { + //given + dto := crd.BlueprintManifest{ + Dogus: []crd.Dogu{ + {Name: "dogu1", Version: &version3211.Raw}, // name contains no "/" + }, + } + + //when + blueprint, err := ConvertToEffectiveBlueprintDomain(&dto) - require.NoError(t, err) - assert.Equal(t, expected, blueprint) + //then + require.Error(t, err) + assert.ErrorContains(t, err, "cannot deserialize effective blueprint") + assert.Equal(t, domain.EffectiveBlueprint{}, blueprint) + }) } func TestConvertToBlueprintDomain(t *testing.T) { @@ -278,11 +261,12 @@ func TestConvertToBlueprintDomain(t *testing.T) { }) t.Run("error converting", func(t *testing.T) { + versionUnparsable := "unparsable" given := crd.BlueprintManifest{ Dogus: []crd.Dogu{ { Name: "official/redmine", - Version: "unparsable", + Version: &versionUnparsable, }, }, } @@ -297,7 +281,7 @@ func TestConvertToBlueprintDomain(t *testing.T) { Dogus: []crd.Dogu{ { Name: "official/redmine", - Version: version1_2_3_3.Raw, + Version: &version1233.Raw, }, }, } @@ -307,45 +291,17 @@ func TestConvertToBlueprintDomain(t *testing.T) { assert.Equal(t, domain.Blueprint{ Dogus: []domain.Dogu{ { - Name: cescommons.QualifiedName{Namespace: "official", SimpleName: "redmine"}, - Version: version1_2_3_3, - TargetState: domain.TargetStatePresent, + Name: cescommons.QualifiedName{Namespace: "official", SimpleName: "redmine"}, + Version: &version1233, }, }, }, converted) }) - - t.Run("convert component", func(t *testing.T) { - given := crd.BlueprintManifest{ - Components: []crd.Component{ - { - Name: "k8s/loki", - Version: compVersion1233.Original(), - }, - }, - } - converted, err := ConvertToBlueprintDomain(given) - - require.NoError(t, err) - assert.Equal(t, domain.Blueprint{ - Components: []domain.Component{ - { - Name: common.QualifiedComponentName{ - Namespace: "k8s", - SimpleName: "loki", - }, - Version: compVersion1233, - TargetState: domain.TargetStatePresent, - }, - }, - }, converted) - }) - } func TestConvertToBlueprintMaskDomain(t *testing.T) { type args struct { - mask crd.BlueprintMask + mask *crd.BlueprintMask } tests := []struct { name string @@ -353,19 +309,25 @@ func TestConvertToBlueprintMaskDomain(t *testing.T) { want domain.BlueprintMask wantErr assert.ErrorAssertionFunc }{ + { + name: "nil", + args: args{mask: nil}, + want: domain.BlueprintMask{}, + wantErr: assert.NoError, + }, { name: "empty", - args: args{mask: crd.BlueprintMask{}}, + args: args{mask: &crd.BlueprintMask{}}, want: domain.BlueprintMask{}, wantErr: assert.NoError, }, { name: "will convert a MaskDogu", - args: args{mask: crd.BlueprintMask{ + args: args{mask: &crd.BlueprintMask{ Dogus: []crd.MaskDogu{ { Name: "official/ldap", - Version: version1_2_3_3.Raw, + Version: &version1233.Raw, }, }, }}, @@ -376,8 +338,7 @@ func TestConvertToBlueprintMaskDomain(t *testing.T) { Namespace: "official", SimpleName: "ldap", }, - Version: version1_2_3_3, - TargetState: domain.TargetStatePresent, + Version: version1233, }, }, }, @@ -385,11 +346,11 @@ func TestConvertToBlueprintMaskDomain(t *testing.T) { }, { name: "error if invalid mask", - args: args{mask: crd.BlueprintMask{ + args: args{mask: &crd.BlueprintMask{ Dogus: []crd.MaskDogu{ { Name: "invalid name", - Version: version1_2_3_3.Raw, + Version: &version1233.Raw, }, }, }}, diff --git a/pkg/adapter/kubernetes/blueprintcr/v2/serializer/component.go b/pkg/adapter/kubernetes/blueprintcr/v2/serializer/component.go deleted file mode 100644 index 32e8d6cf..00000000 --- a/pkg/adapter/kubernetes/blueprintcr/v2/serializer/component.go +++ /dev/null @@ -1,75 +0,0 @@ -package serializer - -import ( - "errors" - "fmt" - v2 "github.com/cloudogu/k8s-blueprint-lib/v2/api/v2" - - "github.com/Masterminds/semver/v3" - - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/common" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/util" -) - -// ConvertComponents takes a slice of TargetComponent and returns a new slice with their DTO equivalent. -func ConvertComponents(components []v2.Component) ([]domain.Component, error) { - var convertedComponents []domain.Component - var errorList []error - - for _, component := range components { - var version *semver.Version - if component.Version != "" { - var err error - version, err = semver.NewVersion(component.Version) - if err != nil { - errorList = append(errorList, fmt.Errorf("could not parse version of target component %q: %w", component.Name, err)) - continue - } - } - - name, err := common.QualifiedComponentNameFromString(component.Name) - if err != nil { - errorList = append(errorList, err) - continue - } - - convertedComponents = append(convertedComponents, domain.Component{ - Name: name, - Version: version, - TargetState: ToDomainTargetState(component.Absent), - DeployConfig: ecosystem.DeployConfig(component.DeployConfig), - }) - } - - err := errors.Join(errorList...) - if err != nil { - return convertedComponents, fmt.Errorf("cannot convert blueprint components: %w", err) - } - - return convertedComponents, err -} - -// ConvertToComponentDTOs takes a slice of Component DTOs and returns a new slice with their domain equivalent. -func ConvertToComponentDTOs(components []domain.Component) []v2.Component { - converted := util.Map(components, func(component domain.Component) v2.Component { - isAbsent := ToSerializerAbsentState(component.TargetState) - - // convert the distribution namespace back into the name field so the EffectiveBlueprint has the same syntax - // as the original blueprint json from the Blueprint resource. - joinedComponentName := component.Name.String() - version := "" - if !isAbsent && component.Version != nil { - version = component.Version.String() - } - - return v2.Component{ - Name: joinedComponentName, - Version: version, - Absent: isAbsent, - DeployConfig: map[string]interface{}(component.DeployConfig), - } - }) - return converted -} diff --git a/pkg/adapter/kubernetes/blueprintcr/v2/serializer/componentDiff.go b/pkg/adapter/kubernetes/blueprintcr/v2/serializer/componentDiff.go deleted file mode 100644 index 02dd8484..00000000 --- a/pkg/adapter/kubernetes/blueprintcr/v2/serializer/componentDiff.go +++ /dev/null @@ -1,106 +0,0 @@ -package serializer - -import ( - "errors" - "fmt" - "github.com/Masterminds/semver/v3" - crd "github.com/cloudogu/k8s-blueprint-lib/v2/api/v2" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/common" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" -) - -func convertToComponentDiffDTO(domainModel domain.ComponentDiff) crd.ComponentDiff { - actualVersion := "" - expectedVersion := "" - - if domainModel.Actual.Version != nil { - actualVersion = domainModel.Actual.Version.String() - } - if domainModel.Expected.Version != nil { - expectedVersion = domainModel.Expected.Version.String() - } - - neededActions := domainModel.NeededActions - componentActions := make([]crd.ComponentAction, 0, len(neededActions)) - for _, action := range neededActions { - componentActions = append(componentActions, crd.ComponentAction(action)) - } - - return crd.ComponentDiff{ - Actual: crd.ComponentDiffState{ - Namespace: string(domainModel.Actual.Namespace), - Version: actualVersion, - InstallationState: domainModel.Actual.InstallationState.String(), - DeployConfig: crd.DeployConfig(domainModel.Actual.DeployConfig), - }, - Expected: crd.ComponentDiffState{ - Namespace: string(domainModel.Expected.Namespace), - Version: expectedVersion, - InstallationState: domainModel.Expected.InstallationState.String(), - DeployConfig: crd.DeployConfig(domainModel.Expected.DeployConfig), - }, - NeededActions: componentActions, - } -} - -func convertToComponentDiffDomain(componentName string, dto crd.ComponentDiff) (domain.ComponentDiff, error) { - var actualVersion *semver.Version - var actualVersionErr error - if dto.Actual.Version != "" { - actualVersion, actualVersionErr = semver.NewVersion(dto.Actual.Version) - if actualVersionErr != nil { - actualVersionErr = fmt.Errorf("failed to parse actual version %q: %w", dto.Actual.Version, actualVersionErr) - } - } - - var expectedVersion *semver.Version - var expectedVersionErr error - if dto.Expected.Version != "" { - expectedVersion, expectedVersionErr = semver.NewVersion(dto.Expected.Version) - if expectedVersionErr != nil { - expectedVersionErr = fmt.Errorf("failed to parse expected version %q: %w", dto.Expected.Version, expectedVersionErr) - } - } - - actualState, actualStateErr := ToOldDomainTargetState(dto.Actual.InstallationState) - if actualStateErr != nil { - actualStateErr = fmt.Errorf("failed to parse actual installation state %q: %w", dto.Actual.InstallationState, actualStateErr) - } - - expectedState, expectedStateErr := ToOldDomainTargetState(dto.Expected.InstallationState) - if expectedStateErr != nil { - expectedStateErr = fmt.Errorf("failed to parse expected installation state %q: %w", dto.Expected.InstallationState, expectedStateErr) - } - - actualDistributionNamespace := dto.Actual.Namespace - expectedDistributionNamespace := dto.Expected.Namespace - - neededActions := dto.NeededActions - componentActions := make([]domain.Action, 0, len(neededActions)) - for _, action := range neededActions { - componentActions = append(componentActions, domain.Action(action)) - } - - err := errors.Join(actualVersionErr, expectedVersionErr, actualStateErr, expectedStateErr) - if err != nil { - return domain.ComponentDiff{}, fmt.Errorf("failed to convert component diff dto %q to domain model: %w", componentName, err) - } - - return domain.ComponentDiff{ - Name: common.SimpleComponentName(componentName), - Actual: domain.ComponentDiffState{ - Namespace: common.ComponentNamespace(actualDistributionNamespace), - Version: actualVersion, - InstallationState: actualState, - DeployConfig: ecosystem.DeployConfig(dto.Actual.DeployConfig), - }, - Expected: domain.ComponentDiffState{ - Namespace: common.ComponentNamespace(expectedDistributionNamespace), - Version: expectedVersion, - InstallationState: expectedState, - DeployConfig: ecosystem.DeployConfig(dto.Expected.DeployConfig), - }, - NeededActions: componentActions, - }, nil -} diff --git a/pkg/adapter/kubernetes/blueprintcr/v2/serializer/componentDiff_test.go b/pkg/adapter/kubernetes/blueprintcr/v2/serializer/componentDiff_test.go deleted file mode 100644 index 9434b1e8..00000000 --- a/pkg/adapter/kubernetes/blueprintcr/v2/serializer/componentDiff_test.go +++ /dev/null @@ -1,140 +0,0 @@ -package serializer - -import ( - "github.com/Masterminds/semver/v3" - crd "github.com/cloudogu/k8s-blueprint-lib/v2/api/v2" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "testing" -) - -const testNamespace = "k8s" - -var testDeployConfig = map[string]interface{}{"key": "value", "key1": map[string]interface{}{"key": "value2"}} - -func Test_convertToComponentDiffDTO(t *testing.T) { - t.Run("should copy model diff to DTO diff - absent", func(t *testing.T) { - // given - domainDiff := domain.ComponentDiff{ - Name: testComponentName, - Actual: domain.ComponentDiffState{Version: nil, InstallationState: domain.TargetStateAbsent}, - Expected: domain.ComponentDiffState{Version: testVersionHigh, InstallationState: domain.TargetStatePresent, Namespace: testNamespace, DeployConfig: testDeployConfig}, - NeededActions: []domain.Action{domain.ActionInstall}} - - // when - actual := convertToComponentDiffDTO(domainDiff) - - // then - expected := crd.ComponentDiff{ - Actual: crd.ComponentDiffState{Version: "", InstallationState: "absent"}, - Expected: crd.ComponentDiffState{Version: testVersionHighRaw, InstallationState: "present", Namespace: testNamespace, DeployConfig: testDeployConfig}, - NeededActions: []crd.ComponentAction{domain.ActionInstall}, - } - assert.Equal(t, expected, actual) - }) - t.Run("should copy model diff to DTO diff - present", func(t *testing.T) { - // given - domainDiff := domain.ComponentDiff{ - Name: testComponentName, - Actual: domain.ComponentDiffState{Version: testVersionHigh, InstallationState: domain.TargetStatePresent}, - Expected: domain.ComponentDiffState{Version: nil, InstallationState: domain.TargetStateAbsent}, - NeededActions: []domain.Action{domain.ActionUninstall}} - - // when - actual := convertToComponentDiffDTO(domainDiff) - - // then - expected := crd.ComponentDiff{ - Actual: crd.ComponentDiffState{Version: testVersionHighRaw, InstallationState: "present"}, - Expected: crd.ComponentDiffState{Version: "", InstallationState: "absent"}, - NeededActions: []crd.ComponentAction{domain.ActionUninstall}, - } - assert.Equal(t, expected, actual) - }) -} - -func Test_convertToComponentDiffDomain(t *testing.T) { - t.Run("should copy model diff to DTO diff - absent", func(t *testing.T) { - // given - diff := crd.ComponentDiff{ - Actual: crd.ComponentDiffState{Namespace: "", Version: "", InstallationState: "absent"}, - Expected: crd.ComponentDiffState{Namespace: "k8s", Version: testVersionHighRaw, InstallationState: "present", DeployConfig: testDeployConfig}, - NeededActions: []crd.ComponentAction{domain.ActionInstall}, - } - - // when - actual, err := convertToComponentDiffDomain(testComponentName, diff) - - // then - require.NoError(t, err) - expected := domain.ComponentDiff{ - Name: testComponentName, - Actual: domain.ComponentDiffState{Namespace: "", Version: nil, InstallationState: domain.TargetStateAbsent}, - Expected: domain.ComponentDiffState{Namespace: "k8s", Version: testVersionHigh, InstallationState: domain.TargetStatePresent, DeployConfig: testDeployConfig}, - NeededActions: []domain.Action{domain.ActionInstall}, - } - assert.Equal(t, expected, actual) - }) - t.Run("should copy model diff to DTO diff - present", func(t *testing.T) { - // given - diff := crd.ComponentDiff{ - Actual: crd.ComponentDiffState{Namespace: "k8s", Version: testVersionHighRaw, InstallationState: "present"}, - Expected: crd.ComponentDiffState{Namespace: "", Version: "", InstallationState: "absent"}, - NeededActions: []crd.ComponentAction{domain.ActionUninstall}, - } - - // when - actual, err := convertToComponentDiffDomain(testComponentName, diff) - - // then - require.NoError(t, err) - expected := domain.ComponentDiff{ - Name: testComponentName, - Actual: domain.ComponentDiffState{Namespace: "k8s", Version: testVersionHigh, InstallationState: domain.TargetStatePresent}, - Expected: domain.ComponentDiffState{Namespace: "", Version: nil, InstallationState: domain.TargetStateAbsent}, - NeededActions: []domain.Action{domain.ActionUninstall}, - } - assert.Equal(t, expected, actual) - }) - t.Run("should fail in all ways", func(t *testing.T) { - // given - diff := crd.ComponentDiff{ - Actual: crd.ComponentDiffState{Namespace: "", Version: "a-b-c", InstallationState: "☹"}, - Expected: crd.ComponentDiffState{Namespace: "", Version: "a-b-c", InstallationState: "☹"}, - NeededActions: []crd.ComponentAction{domain.ActionUninstall}, - } - - // when - _, err := convertToComponentDiffDomain(testComponentName, diff) - - // then - require.Error(t, err) - assert.ErrorContains(t, err, "failed to parse actual version") - assert.ErrorContains(t, err, "failed to parse expected version") - assert.ErrorContains(t, err, "failed to parse actual installation state") - assert.ErrorContains(t, err, "failed to parse expected installation state") - }) - t.Run("should accept dev version", func(t *testing.T) { - compVersion080dev := semver.MustParse("0.8.0-dev") - // given - diff := crd.ComponentDiff{ - Actual: crd.ComponentDiffState{Namespace: "k8s", Version: compVersion080dev.String(), InstallationState: "present"}, - Expected: crd.ComponentDiffState{Namespace: "", Version: "", InstallationState: "absent"}, - NeededActions: []crd.ComponentAction{domain.ActionUninstall}, - } - - // when - actual, err := convertToComponentDiffDomain(testComponentName, diff) - - // then - require.NoError(t, err) - expected := domain.ComponentDiff{ - Name: testComponentName, - Actual: domain.ComponentDiffState{Namespace: "k8s", Version: compVersion080dev, InstallationState: domain.TargetStatePresent}, - Expected: domain.ComponentDiffState{Namespace: "", Version: nil, InstallationState: domain.TargetStateAbsent}, - NeededActions: []domain.Action{domain.ActionUninstall}, - } - assert.Equal(t, expected, actual) - }) -} diff --git a/pkg/adapter/kubernetes/blueprintcr/v2/serializer/component_test.go b/pkg/adapter/kubernetes/blueprintcr/v2/serializer/component_test.go deleted file mode 100644 index a9074e33..00000000 --- a/pkg/adapter/kubernetes/blueprintcr/v2/serializer/component_test.go +++ /dev/null @@ -1,113 +0,0 @@ -package serializer - -import ( - "fmt" - bpv2 "github.com/cloudogu/k8s-blueprint-lib/v2/api/v2" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/common" -) - -var ( - k8sK8sDoguOperator = common.QualifiedComponentName{Namespace: "k8s", SimpleName: "k8s-dogu-operator"} -) - -func TestConvertComponents(t *testing.T) { - type args struct { - components []bpv2.Component - } - tests := []struct { - name string - args args - want []domain.Component - wantErr assert.ErrorAssertionFunc - }{ - { - name: "nil", - args: args{components: nil}, - want: nil, - wantErr: assert.NoError, - }, - { - name: "empty list", - args: args{components: []bpv2.Component{}}, - want: nil, - wantErr: assert.NoError, - }, - { - name: "normal component", - args: args{components: []bpv2.Component{{Name: "k8s/k8s-dogu-operator", Version: version3211.Raw, Absent: false, DeployConfig: map[string]interface{}{"deployNamespace": "longhorn-system", "configOverwrite": map[string]string{"key": "value"}}}}}, - want: []domain.Component{{Name: k8sK8sDoguOperator, Version: compVersion3211, TargetState: domain.TargetStatePresent, DeployConfig: map[string]interface{}{"deployNamespace": "longhorn-system", "configOverwrite": map[string]string{"key": "value"}}}}, - wantErr: assert.NoError, - }, - { - name: "absent component", - args: args{components: []bpv2.Component{{Name: "k8s/k8s-dogu-operator", Absent: true}}}, - want: []domain.Component{{Name: k8sK8sDoguOperator, TargetState: domain.TargetStateAbsent}}, - wantErr: assert.NoError, - }, - { - name: "unparsable version", - args: args{components: []bpv2.Component{{Name: "k8s/k8s-dogu-operator", Version: "1."}}}, - want: nil, - wantErr: assert.Error, - }, - { - name: "invalid component name", - args: args{components: []bpv2.Component{{Name: "k8s/k8s-dogu-operator/oh/no", Version: "1.0.0"}}}, - want: nil, - wantErr: assert.Error, - }, - { - name: "does not contain distribution namespace", - args: args{components: []bpv2.Component{{Name: "k8s-dogu-operator", Version: version3211.Raw}}}, - want: nil, - wantErr: assert.Error, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := ConvertComponents(tt.args.components) - if !tt.wantErr(t, err, fmt.Sprintf("ConvertComponents(%v)", tt.args.components)) { - return - } - assert.Equalf(t, tt.want, got, "ConvertComponents(%v)", tt.args.components) - }) - } -} - -func TestConvertToComponentDTOs(t *testing.T) { - type args struct { - components []domain.Component - } - tests := []struct { - name string - args args - want []bpv2.Component - }{ - { - name: "nil", - args: args{}, - want: []bpv2.Component{}, - }, - { - name: "empty list", - args: args{components: []domain.Component{}}, - want: []bpv2.Component{}, - }, - { - name: "ok", - args: args{components: []domain.Component{{Name: k8sK8sDoguOperator, Version: compVersion3211, TargetState: domain.TargetStatePresent}}}, - want: []bpv2.Component{{Name: "k8s/k8s-dogu-operator", Version: version3211.Raw, Absent: false}}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := ConvertToComponentDTOs(tt.args.components) - assert.Equalf(t, tt.want, got, "ConvertToComponentDTOs(%v)", tt.args.components) - }) - } -} diff --git a/pkg/adapter/kubernetes/blueprintcr/v2/serializer/config.go b/pkg/adapter/kubernetes/blueprintcr/v2/serializer/config.go index c100b260..68f58f6e 100644 --- a/pkg/adapter/kubernetes/blueprintcr/v2/serializer/config.go +++ b/pkg/adapter/kubernetes/blueprintcr/v2/serializer/config.go @@ -3,37 +3,44 @@ package serializer import ( cescommons "github.com/cloudogu/ces-commons-lib/dogu" v2 "github.com/cloudogu/k8s-blueprint-lib/v2/api/v2" - "github.com/cloudogu/k8s-registry-lib/config" + libconfig "github.com/cloudogu/k8s-registry-lib/config" + "k8s.io/utils/ptr" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/common" ) -func ConvertToConfigDTO(config domain.Config) v2.Config { - var dogus map[string]v2.CombinedDoguConfig +func ConvertToConfigDTO(config domain.Config) *v2.Config { + if config.IsEmpty() { + return nil + } + + var dogus map[string][]v2.ConfigEntry // we check for empty values to make good use of default values // this makes testing easier if len(config.Dogus) != 0 { - dogus = make(map[string]v2.CombinedDoguConfig, len(config.Dogus)) + dogus = make(map[string][]v2.ConfigEntry, len(config.Dogus)) for doguName, doguConfig := range config.Dogus { - dogus[string(doguName)] = convertToCombinedDoguConfigDTO(doguConfig) + dogus[string(doguName)] = convertToDoguConfigDTO(doguConfig) } } - return v2.Config{ + return &v2.Config{ Dogus: dogus, Global: convertToGlobalConfigDTO(config.Global), } } -func ConvertToConfigDomain(config v2.Config) domain.Config { - var dogus map[cescommons.SimpleName]domain.CombinedDoguConfig +func ConvertToConfigDomain(config *v2.Config) domain.Config { + if config == nil { + return domain.Config{} + } + var dogus map[cescommons.SimpleName]domain.DoguConfigEntries // we check for empty values to make good use of default values // this makes testing easier if len(config.Dogus) != 0 { - dogus = make(map[cescommons.SimpleName]domain.CombinedDoguConfig, len(config.Dogus)) + dogus = make(map[cescommons.SimpleName]domain.DoguConfigEntries, len(config.Dogus)) for doguName, doguConfig := range config.Dogus { - dogus[cescommons.SimpleName(doguName)] = convertToCombinedDoguConfigDomain(doguName, doguConfig) + dogus[cescommons.SimpleName(doguName)] = convertToDoguConfigEntriesDomain(doguConfig) } } @@ -43,141 +50,88 @@ func ConvertToConfigDomain(config v2.Config) domain.Config { } } -func convertToCombinedDoguConfigDTO(config domain.CombinedDoguConfig) v2.CombinedDoguConfig { - return v2.CombinedDoguConfig{ - Config: convertToDoguConfigDTO(config.Config), - SensitiveConfig: convertToSensitiveDoguConfigDTO(config.SensitiveConfig), - } +func convertToDoguConfigDTO(config domain.DoguConfigEntries) []v2.ConfigEntry { + return convertToConfigEntriesDTO(domain.ConfigEntries(config)) } -func convertToCombinedDoguConfigDomain(doguName string, config v2.CombinedDoguConfig) domain.CombinedDoguConfig { - return domain.CombinedDoguConfig{ - DoguName: cescommons.SimpleName(doguName), - Config: convertToDoguConfigDomain(doguName, config.Config), - SensitiveConfig: convertToSensitiveDoguConfigDomain(doguName, config.SensitiveConfig), - } +func convertToDoguConfigEntriesDomain(config []v2.ConfigEntry) domain.DoguConfigEntries { + return domain.DoguConfigEntries(convertToConfigEntriesDomain(config)) +} + +func convertToGlobalConfigDTO(config domain.GlobalConfigEntries) []v2.ConfigEntry { + return convertToConfigEntriesDTO(domain.ConfigEntries(config)) } -func convertToDoguConfigDTO(config domain.DoguConfig) *v2.DoguConfig { - // empty struct -> nil - if len(config.Present) == 0 && len(config.Absent) == 0 { +func convertToConfigEntriesDTO(config domain.ConfigEntries) []v2.ConfigEntry { + if len(config) == 0 { return nil } - var present map[string]string - // we check for empty values to make good use of default values - // this makes testing easier - if len(config.Present) != 0 { - present = make(map[string]string, len(config.Present)) - for key, value := range config.Present { - present[string(key.Key)] = string(value) - } - } + result := make([]v2.ConfigEntry, len(config)) - var absent []string - // we check for empty values to make good use of default values - // this makes testing easier - if len(config.Absent) != 0 { - absent = make([]string, len(config.Absent)) - for i, key := range config.Absent { - absent[i] = string(key.Key) + for i, domainEntry := range config { + var absent *bool + if domainEntry.Absent { + absent = &domainEntry.Absent } - } - return &v2.DoguConfig{ - Present: present, - Absent: absent, - } -} - -func convertToDoguConfigDomain(doguName string, config *v2.DoguConfig) domain.DoguConfig { - if config == nil { - return domain.DoguConfig{} - } + var sensitive *bool + var value *string + if domainEntry.Sensitive { + sensitive = &domainEntry.Sensitive + } else { + // only set value if not sensitive + value = (*string)(domainEntry.Value) + } - var present map[common.DoguConfigKey]common.DoguConfigValue - // we check for empty values to make good use of default values - // this makes testing easier - if len(config.Present) != 0 { - present = make(map[common.DoguConfigKey]common.DoguConfigValue, len(config.Present)) - for key, value := range config.Present { - present[convertToDoguConfigKeyDomain(doguName, key)] = common.DoguConfigValue(value) + var secretRef *v2.SecretReference + if domainEntry.SecretRef != nil { + secretRef = &v2.SecretReference{ + Name: domainEntry.SecretRef.SecretName, + Key: domainEntry.SecretRef.SecretKey, + } } - } - var absent []common.DoguConfigKey - // we check for empty values to make good use of default values - // this makes testing easier - if len(config.Absent) != 0 { - absent = make([]common.DoguConfigKey, len(config.Absent)) - for i, key := range config.Absent { - absent[i] = convertToDoguConfigKeyDomain(doguName, key) + result[i] = v2.ConfigEntry{ + Key: domainEntry.Key.String(), + Absent: absent, + Value: value, + Sensitive: sensitive, + SecretRef: secretRef, } } - return domain.DoguConfig{ - Present: present, - Absent: absent, - } + return result } -func convertToDoguConfigKeyDomain(doguName, key string) common.DoguConfigKey { - return common.DoguConfigKey{ - DoguName: cescommons.SimpleName(doguName), - Key: config.Key(key), - } +func convertToGlobalConfigDomain(config []v2.ConfigEntry) domain.GlobalConfigEntries { + return domain.GlobalConfigEntries(convertToConfigEntriesDomain(config)) } -func convertToGlobalConfigDTO(config domain.GlobalConfig) v2.GlobalConfig { - var present map[string]string - // we check for empty values to make good use of default values - // this makes testing easier - if len(config.Present) != 0 { - present = make(map[string]string, len(config.Present)) - for key, value := range config.Present { - present[string(key)] = string(value) - } - } - - var absent []string - // we check for empty values to make good use of default values - // this makes testing easier - if len(config.Absent) != 0 { - absent = make([]string, len(config.Absent)) - for i, key := range config.Absent { - absent[i] = string(key) - } +func convertToConfigEntriesDomain(config []v2.ConfigEntry) domain.ConfigEntries { + if len(config) == 0 { + return nil } - return v2.GlobalConfig{ - Present: present, - Absent: absent, - } -} + result := make([]domain.ConfigEntry, len(config)) -func convertToGlobalConfigDomain(config v2.GlobalConfig) domain.GlobalConfig { - var present map[common.GlobalConfigKey]common.GlobalConfigValue - // we check for empty values to make good use of default values - // this makes testing easier - if len(config.Present) != 0 { - present = make(map[common.GlobalConfigKey]common.GlobalConfigValue, len(config.Present)) - for key, value := range config.Present { - present[common.GlobalConfigKey(key)] = common.GlobalConfigValue(value) + for i, v2Entry := range config { + var secretRef *domain.SensitiveValueRef + if v2Entry.SecretRef != nil { + secretRef = &domain.SensitiveValueRef{ + SecretName: v2Entry.SecretRef.Name, + SecretKey: v2Entry.SecretRef.Key, + } } - } - var absent []common.GlobalConfigKey - // we check for empty values to make good use of default values - // this makes testing easier - if len(config.Absent) != 0 { - absent = make([]common.GlobalConfigKey, len(config.Absent)) - for i, key := range config.Absent { - absent[i] = common.GlobalConfigKey(key) + result[i] = domain.ConfigEntry{ + Key: libconfig.Key(v2Entry.Key), + Absent: ptr.Deref(v2Entry.Absent, false), + Value: (*libconfig.Value)(v2Entry.Value), + Sensitive: ptr.Deref(v2Entry.Sensitive, false), + SecretRef: secretRef, } } - return domain.GlobalConfig{ - Present: present, - Absent: absent, - } + return result } diff --git a/pkg/adapter/kubernetes/blueprintcr/v2/serializer/config_test.go b/pkg/adapter/kubernetes/blueprintcr/v2/serializer/config_test.go index 423c108b..e0a576fb 100644 --- a/pkg/adapter/kubernetes/blueprintcr/v2/serializer/config_test.go +++ b/pkg/adapter/kubernetes/blueprintcr/v2/serializer/config_test.go @@ -1,57 +1,70 @@ package serializer import ( + "testing" + v2 "github.com/cloudogu/k8s-blueprint-lib/v2/api/v2" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/common" + "github.com/cloudogu/k8s-registry-lib/config" "github.com/stretchr/testify/assert" - "testing" +) + +var ( + val1 = "val1" ) func Test_convertToDoguConfigDTO(t *testing.T) { tests := []struct { name string - config domain.DoguConfig - want *v2.DoguConfig + config domain.DoguConfigEntries + want []v2.ConfigEntry }{ { name: "nil config", - config: domain.DoguConfig{}, + config: nil, want: nil, }, { - name: "empty config", - config: domain.DoguConfig{ - Present: map[common.DoguConfigKey]common.DoguConfigValue{}, - Absent: []common.DoguConfigKey{}, - }, - want: nil, + name: "empty config", + config: domain.DoguConfigEntries{}, + want: nil, }, { name: "convert present config", - config: domain.DoguConfig{ - Present: map[common.DoguConfigKey]common.DoguConfigValue{ - testDoguKey1: "val1", + config: domain.DoguConfigEntries{ + { + Key: testDoguKey1.Key, + Value: (*config.Value)(&val1), }, }, - want: &v2.DoguConfig{ - Present: map[string]string{ - testDoguKey1.Key.String(): "val1", - }, + want: []v2.ConfigEntry{ + {Key: testDoguKey1.Key.String(), Value: &val1}, }, }, { name: "convert absent config", - config: domain.DoguConfig{ - Absent: []common.DoguConfigKey{ - testDoguKey1, + config: domain.DoguConfigEntries{ + { + Key: testDoguKey1.Key, + Absent: true, }, }, - want: &v2.DoguConfig{ - Absent: []string{ - testDoguKey1.Key.String(), + want: []v2.ConfigEntry{ + {Key: testDoguKey1.Key.String(), Absent: &trueVar}, + }, + }, + { + name: "censor sensitive config values", + config: domain.DoguConfigEntries{ + { + Key: testDoguKey1.Key, + Sensitive: true, + Value: (*config.Value)(&val1), }, }, + want: []v2.ConfigEntry{ + {Key: testDoguKey1.Key.String(), Sensitive: &trueVar}, + }, }, } for _, tt := range tests { @@ -61,67 +74,56 @@ func Test_convertToDoguConfigDTO(t *testing.T) { } } -func Test_convertToDoguConfigDomain(t *testing.T) { - type args struct { - doguName string - config *v2.DoguConfig - } +func Test_convertToDoguConfigEntriesDomain(t *testing.T) { tests := []struct { - name string - args args - want domain.DoguConfig + name string + config []v2.ConfigEntry + want domain.DoguConfigEntries }{ { - name: "nil", - args: args{ - doguName: string(testDoguKey1.DoguName), - }, - want: domain.DoguConfig{}, + name: "nil", + config: nil, + want: nil, }, { - name: "nil config", - args: args{ - doguName: string(testDoguKey1.DoguName), - config: &v2.DoguConfig{}, - }, - want: domain.DoguConfig{}, + name: "empty config", + config: []v2.ConfigEntry{}, + want: nil, }, { name: "convert present config", - args: args{ - doguName: string(testDoguKey1.DoguName), - config: &v2.DoguConfig{ - Present: map[string]string{ - testDoguKey1.Key.String(): "val1", - }, + config: []v2.ConfigEntry{ + { + Key: testDoguKey1.Key.String(), + Value: &val1, }, }, - want: domain.DoguConfig{ - Present: map[common.DoguConfigKey]common.DoguConfigValue{ - testDoguKey1: "val1", + want: domain.DoguConfigEntries{ + { + Key: testDoguKey1.Key, + Value: (*config.Value)(&val1), }, }, }, { name: "convert absent config", - args: args{ - doguName: string(testDoguKey1.DoguName), - config: &v2.DoguConfig{ - Absent: []string{ - testDoguKey1.Key.String(), - }, + config: []v2.ConfigEntry{ + { + Key: testDoguKey1.Key.String(), + Absent: &trueVar, }, }, - want: domain.DoguConfig{ - Absent: []common.DoguConfigKey{ - testDoguKey1, + want: domain.DoguConfigEntries{ + { + Key: testDoguKey1.Key, + Absent: true, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.Equalf(t, tt.want, convertToDoguConfigDomain(tt.args.doguName, tt.args.config), "convertToDoguConfigDomain(%v, %v)", tt.args.doguName, tt.args.config) + assert.Equalf(t, tt.want, convertToDoguConfigEntriesDomain(tt.config), "convertToDoguConfigDomain%v)", tt.config) }) } } @@ -129,37 +131,46 @@ func Test_convertToDoguConfigDomain(t *testing.T) { func Test_convertToGlobalConfigDTO(t *testing.T) { tests := []struct { name string - config domain.GlobalConfig - want v2.GlobalConfig + config domain.GlobalConfigEntries + want []v2.ConfigEntry }{ + { + name: "nil", + config: nil, + want: nil, + }, { name: "empty", - config: domain.GlobalConfig{}, - want: v2.GlobalConfig{}, + config: domain.GlobalConfigEntries{}, + want: nil, }, { name: "convert present", - config: domain.GlobalConfig{ - Present: map[common.GlobalConfigKey]common.GlobalConfigValue{ - "test": "val1", + config: domain.GlobalConfigEntries{ + { + Key: "test", + Value: (*config.Value)(&val1), }, }, - want: v2.GlobalConfig{ - Present: map[string]string{ - "test": "val1", + want: []v2.ConfigEntry{ + { + Key: "test", + Value: &val1, }, }, }, { name: "convert absent", - config: domain.GlobalConfig{ - Absent: []common.GlobalConfigKey{ - "test", + config: domain.GlobalConfigEntries{ + { + Key: "test", + Absent: true, }, }, - want: v2.GlobalConfig{ - Absent: []string{ - "test", + want: []v2.ConfigEntry{ + { + Key: "test", + Absent: &trueVar, }, }, }, @@ -174,37 +185,41 @@ func Test_convertToGlobalConfigDTO(t *testing.T) { func Test_convertToGlobalConfigDomain(t *testing.T) { tests := []struct { name string - config v2.GlobalConfig - want domain.GlobalConfig + config []v2.ConfigEntry + want domain.GlobalConfigEntries }{ { - name: "empty", - config: v2.GlobalConfig{}, - want: domain.GlobalConfig{}, + name: "nil", + config: nil, + want: nil, }, { name: "convert present", - config: v2.GlobalConfig{ - Present: map[string]string{ - "test": "val1", + config: []v2.ConfigEntry{ + { + Key: "test", + Value: &val1, }, }, - want: domain.GlobalConfig{ - Present: map[common.GlobalConfigKey]common.GlobalConfigValue{ - "test": "val1", + want: domain.GlobalConfigEntries{ + { + Key: "test", + Value: (*config.Value)(&val1), }, }, }, { - name: "convert present", - config: v2.GlobalConfig{ - Absent: []string{ - "test", + name: "convert absent", + config: []v2.ConfigEntry{ + { + Key: "test", + Absent: &trueVar, }, }, - want: domain.GlobalConfig{ - Absent: []common.GlobalConfigKey{ - "test", + want: domain.GlobalConfigEntries{ + { + Key: "test", + Absent: true, }, }, }, diff --git a/pkg/adapter/kubernetes/blueprintcr/v2/serializer/dogu.go b/pkg/adapter/kubernetes/blueprintcr/v2/serializer/dogu.go index ec1cf625..bb560faf 100644 --- a/pkg/adapter/kubernetes/blueprintcr/v2/serializer/dogu.go +++ b/pkg/adapter/kubernetes/blueprintcr/v2/serializer/dogu.go @@ -3,9 +3,12 @@ package serializer import ( "errors" "fmt" + cescommons "github.com/cloudogu/ces-commons-lib/dogu" "github.com/cloudogu/cesapp-lib/core" bpv2 "github.com/cloudogu/k8s-blueprint-lib/v2/api/v2" + "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/utils/ptr" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" @@ -17,47 +20,33 @@ func ConvertDogus(dogus []bpv2.Dogu) ([]domain.Dogu, error) { var errorList []error for _, dogu := range dogus { + result := domain.Dogu{} name, err := cescommons.QualifiedNameFromString(dogu.Name) if err != nil { errorList = append(errorList, err) continue } + result.Name = name - var version core.Version - if dogu.Version != "" { - version, err = core.ParseVersion(dogu.Version) + var version *core.Version + if dogu.Version != nil && *dogu.Version != "" { + coreVersion, err := core.ParseVersion(*dogu.Version) + version = &coreVersion if err != nil { errorList = append(errorList, fmt.Errorf("could not parse version of target dogu %q: %w", dogu.Name, err)) continue } } + result.Version = version + result.Absent = ptr.Deref(dogu.Absent, false) - minVolumeSizeStr := dogu.PlatformConfig.ResourceConfig.MinVolumeSize - minVolumeSize, minVolumeSizeErr := ecosystem.GetNonNilQuantityRef(minVolumeSizeStr) - if minVolumeSizeErr != nil { - errorList = append(errorList, fmt.Errorf("could not parse minimum volume size %q for dogu %q", minVolumeSizeStr, dogu.Name)) - continue - } - - maxBodySizeStr := dogu.PlatformConfig.ReverseProxyConfig.MaxBodySize - maxBodySize, maxBodySizeErr := ecosystem.GetQuantityReference(maxBodySizeStr) - if maxBodySizeErr != nil { - errorList = append(errorList, fmt.Errorf("could not parse maximum proxy body size %q for dogu %q", maxBodySizeStr, dogu.Name)) + err = convertPlatformConfigFromDTOToDomain(&dogu, &result) + if err != nil { + errorList = append(errorList, err) continue } - convertedDogus = append(convertedDogus, domain.Dogu{ - Name: name, - Version: version, - TargetState: ToDomainTargetState(dogu.Absent), - MinVolumeSize: *minVolumeSize, - ReverseProxyConfig: ecosystem.ReverseProxyConfig{ - MaxBodySize: maxBodySize, - RewriteTarget: ecosystem.RewriteTarget(dogu.PlatformConfig.ReverseProxyConfig.RewriteTarget), - AdditionalConfig: ecosystem.AdditionalConfig(dogu.PlatformConfig.ReverseProxyConfig.AdditionalConfig), - }, - AdditionalMounts: convertAdditionalMountsFromDTOToDomain(dogu.PlatformConfig.AdditionalMountsConfig), - }) + convertedDogus = append(convertedDogus, result) } err := errors.Join(errorList...) @@ -68,6 +57,47 @@ func ConvertDogus(dogus []bpv2.Dogu) ([]domain.Dogu, error) { return convertedDogus, err } +func convertPlatformConfigFromDTOToDomain(dtoDogu *bpv2.Dogu, domainDogu *domain.Dogu) error { + if dtoDogu.PlatformConfig == nil { + return nil + } + + var minVolumeSize, maxBodySize *resource.Quantity + var additionalMounts []ecosystem.AdditionalMount + var err error + if dtoDogu.PlatformConfig.ResourceConfig != nil && dtoDogu.PlatformConfig.ResourceConfig.MinVolumeSize != nil { + minVolumeSizeStr := dtoDogu.PlatformConfig.ResourceConfig.MinVolumeSize + minVolumeSize, err = ecosystem.GetNonNilQuantityRef(*minVolumeSizeStr) + if err != nil { + return fmt.Errorf("could not parse minimum volume size %q for dogu %q", *minVolumeSizeStr, dtoDogu.Name) + } + domainDogu.MinVolumeSize = minVolumeSize + } + + if dtoDogu.PlatformConfig.ReverseProxyConfig != nil { + if dtoDogu.PlatformConfig.ReverseProxyConfig.MaxBodySize != nil { + maxBodySizeStr := dtoDogu.PlatformConfig.ReverseProxyConfig.MaxBodySize + maxBodySize, err = ecosystem.GetQuantityReference(*maxBodySizeStr) + if err != nil { + return fmt.Errorf("could not parse maximum proxy body size %q for dogu %q", *maxBodySizeStr, dtoDogu.Name) + } + } + + domainDogu.ReverseProxyConfig = ecosystem.ReverseProxyConfig{ + MaxBodySize: maxBodySize, + RewriteTarget: ecosystem.RewriteTarget(ptr.Deref(dtoDogu.PlatformConfig.ReverseProxyConfig.RewriteTarget, "")), + AdditionalConfig: ecosystem.AdditionalConfig(ptr.Deref(dtoDogu.PlatformConfig.ReverseProxyConfig.AdditionalConfig, "")), + } + } + + if dtoDogu.PlatformConfig.AdditionalMountsConfig != nil { + additionalMounts = convertAdditionalMountsFromDTOToDomain(dtoDogu.PlatformConfig.AdditionalMountsConfig) + domainDogu.AdditionalMounts = additionalMounts + } + + return nil +} + func ConvertMaskDogus(dogus []bpv2.MaskDogu) ([]domain.MaskDogu, error) { var convertedDogus []domain.MaskDogu var errorList []error @@ -80,8 +110,8 @@ func ConvertMaskDogus(dogus []bpv2.MaskDogu) ([]domain.MaskDogu, error) { } var version core.Version - if dogu.Version != "" { - version, err = core.ParseVersion(dogu.Version) + if dogu.Version != nil && *dogu.Version != "" { + version, err = core.ParseVersion(*dogu.Version) if err != nil { errorList = append(errorList, fmt.Errorf("could not parse version of mask dogu %q: %w", dogu.Name, err)) continue @@ -89,9 +119,9 @@ func ConvertMaskDogus(dogus []bpv2.MaskDogu) ([]domain.MaskDogu, error) { } convertedDogus = append(convertedDogus, domain.MaskDogu{ - Name: name, - Version: version, - TargetState: ToDomainTargetState(dogu.Absent), + Name: name, + Version: version, + Absent: ptr.Deref(dogu.Absent, false), }) } @@ -110,7 +140,7 @@ func convertAdditionalMountsFromDTOToDomain(mounts []bpv2.AdditionalMount) []eco SourceType: ecosystem.DataSourceType(m.SourceType), Name: m.Name, Volume: m.Volume, - Subfolder: m.Subfolder, + Subfolder: ptr.Deref(m.Subfolder, ""), }) } @@ -119,49 +149,67 @@ func convertAdditionalMountsFromDTOToDomain(mounts []bpv2.AdditionalMount) []eco func ConvertToDoguDTOs(dogus []domain.Dogu) []bpv2.Dogu { converted := util.Map(dogus, func(dogu domain.Dogu) bpv2.Dogu { + var version *string + if dogu.Version != nil { + version = &dogu.Version.Raw + } return bpv2.Dogu{ Name: dogu.Name.String(), - Version: dogu.Version.Raw, - Absent: ToSerializerAbsentState(dogu.TargetState), + Version: version, + Absent: &dogu.Absent, PlatformConfig: convertPlatformConfigDTO(dogu), } }) return converted } -func convertPlatformConfigDTO(dogu domain.Dogu) bpv2.PlatformConfig { +func convertPlatformConfigDTO(dogu domain.Dogu) *bpv2.PlatformConfig { + if dogu.ReverseProxyConfig.IsEmpty() && dogu.MinVolumeSize == nil && len(dogu.AdditionalMounts) == 0 { + return nil + } + config := bpv2.PlatformConfig{} config.ResourceConfig = convertResourceConfigDTO(dogu) config.ReverseProxyConfig = convertReverseProxyConfigDTO(dogu) - config.AdditionalMountsConfig = convertAdditionalMountsConfig(dogu) + config.AdditionalMountsConfig = convertAdditionalMountsConfigDTO(dogu) - return config + return &config } -func convertReverseProxyConfigDTO(dogu domain.Dogu) bpv2.ReverseProxyConfig { - config := bpv2.ReverseProxyConfig{} - config.RewriteTarget = string(dogu.ReverseProxyConfig.RewriteTarget) - config.AdditionalConfig = string(dogu.ReverseProxyConfig.AdditionalConfig) - config.MaxBodySize = ecosystem.GetQuantityString(dogu.ReverseProxyConfig.MaxBodySize) - - return config +func convertReverseProxyConfigDTO(dogu domain.Dogu) *bpv2.ReverseProxyConfig { + var rewriteTarget, additionalConfig *string + if dogu.ReverseProxyConfig.RewriteTarget != "" { + rewriteTarget = (*string)(&dogu.ReverseProxyConfig.RewriteTarget) + } + if dogu.ReverseProxyConfig.AdditionalConfig != "" { + additionalConfig = (*string)(&dogu.ReverseProxyConfig.AdditionalConfig) + } + return &bpv2.ReverseProxyConfig{ + RewriteTarget: rewriteTarget, + AdditionalConfig: additionalConfig, + MaxBodySize: ecosystem.GetQuantityString(dogu.ReverseProxyConfig.MaxBodySize), + } } -func convertResourceConfigDTO(dogu domain.Dogu) bpv2.ResourceConfig { +func convertResourceConfigDTO(dogu domain.Dogu) *bpv2.ResourceConfig { config := bpv2.ResourceConfig{} - config.MinVolumeSize = ecosystem.GetQuantityString(&dogu.MinVolumeSize) + config.MinVolumeSize = ecosystem.GetQuantityString(dogu.MinVolumeSize) - return config + return &config } -func convertAdditionalMountsConfig(dogu domain.Dogu) []bpv2.AdditionalMount { +func convertAdditionalMountsConfigDTO(dogu domain.Dogu) []bpv2.AdditionalMount { var config []bpv2.AdditionalMount for _, m := range dogu.AdditionalMounts { + var subfolder *string + if m.Subfolder != "" { + subfolder = &m.Subfolder + } config = append(config, bpv2.AdditionalMount{ SourceType: bpv2.DataSourceType(m.SourceType), Name: m.Name, Volume: m.Volume, - Subfolder: m.Subfolder, + Subfolder: subfolder, }) } return config diff --git a/pkg/adapter/kubernetes/blueprintcr/v2/serializer/doguConfigDiff.go b/pkg/adapter/kubernetes/blueprintcr/v2/serializer/doguConfigDiff.go index f994ffc3..de8d65c4 100644 --- a/pkg/adapter/kubernetes/blueprintcr/v2/serializer/doguConfigDiff.go +++ b/pkg/adapter/kubernetes/blueprintcr/v2/serializer/doguConfigDiff.go @@ -1,58 +1,33 @@ package serializer import ( - cescommons "github.com/cloudogu/ces-commons-lib/dogu" crd "github.com/cloudogu/k8s-blueprint-lib/v2/api/v2" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/common" - "github.com/cloudogu/k8s-registry-lib/config" ) -func convertToDoguConfigDiffsDomain(doguName string, dtoDiffs crd.DoguConfigDiff) domain.DoguConfigDiffs { - var doguConfigDiff domain.DoguConfigDiffs - for _, entryDiff := range dtoDiffs { - doguConfigDiff = append(doguConfigDiff, convertToDoguConfigEntryDiffDomain(doguName, entryDiff)) - } - return doguConfigDiff -} - -func convertToDoguConfigEntryDiffDomain(doguName string, dto crd.DoguConfigEntryDiff) domain.DoguConfigEntryDiff { - return domain.DoguConfigEntryDiff{ - Key: common.DoguConfigKey{ - DoguName: cescommons.SimpleName(doguName), - Key: config.Key(dto.Key), - }, - Actual: domain.DoguConfigValueState{ - Value: dto.Actual.Value, - Exists: dto.Actual.Exists, - }, - Expected: domain.DoguConfigValueState{ - Value: dto.Expected.Value, - Exists: dto.Expected.Exists, - }, - NeededAction: domain.ConfigAction(dto.NeededAction), - } -} - -func convertToDoguConfigEntryDiffsDTO(domainDiffs domain.DoguConfigDiffs) []crd.DoguConfigEntryDiff { - var dtoDiffs []crd.DoguConfigEntryDiff +func convertToDoguConfigEntryDiffsDTO(domainDiffs domain.DoguConfigDiffs, isSensitive bool) crd.DoguConfigDiff { + var dtoDiffs []crd.ConfigEntryDiff for _, domainDiff := range domainDiffs { - dtoDiffs = append(dtoDiffs, convertToDoguConfigEntryDiffDTO(domainDiff)) + dtoDiffs = append(dtoDiffs, convertToDoguConfigEntryDiffDTO(domainDiff, isSensitive)) } return dtoDiffs } -func convertToDoguConfigEntryDiffDTO(domainModel domain.DoguConfigEntryDiff) crd.DoguConfigEntryDiff { - return crd.DoguConfigEntryDiff{ - Key: string(domainModel.Key.Key), - Actual: crd.DoguConfigValueState{ - Value: domainModel.Actual.Value, - Exists: domainModel.Actual.Exists, - }, - Expected: crd.DoguConfigValueState{ - Value: domainModel.Expected.Value, - Exists: domainModel.Expected.Exists, - }, +func convertToDoguConfigEntryDiffDTO(domainModel domain.DoguConfigEntryDiff, isSensitive bool) crd.ConfigEntryDiff { + actual := crd.ConfigValueState{ + Exists: domainModel.Actual.Exists, + } + expected := crd.ConfigValueState{ + Exists: domainModel.Expected.Exists, + } + if !isSensitive { + actual.Value = domainModel.Actual.Value + expected.Value = domainModel.Expected.Value + } + return crd.ConfigEntryDiff{ + Key: string(domainModel.Key.Key), + Actual: actual, + Expected: expected, NeededAction: crd.ConfigAction(domainModel.NeededAction), } } diff --git a/pkg/adapter/kubernetes/blueprintcr/v2/serializer/doguConfigDiff_test.go b/pkg/adapter/kubernetes/blueprintcr/v2/serializer/doguConfigDiff_test.go index ffbccd6e..20c4aee4 100644 --- a/pkg/adapter/kubernetes/blueprintcr/v2/serializer/doguConfigDiff_test.go +++ b/pkg/adapter/kubernetes/blueprintcr/v2/serializer/doguConfigDiff_test.go @@ -1,18 +1,26 @@ package serializer import ( + "testing" + crd "github.com/cloudogu/k8s-blueprint-lib/v2/api/v2" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/common" "github.com/stretchr/testify/assert" - "testing" +) + +var ( + limit512 = "512m" + limit1024 = "1024m" ) func Test_convertToDoguConfigEntryDiffsDTO(t *testing.T) { + tests := []struct { name string domainModel domain.DoguConfigDiffs - want []crd.DoguConfigEntryDiff + want crd.DoguConfigDiff + isSensitive bool }{ { name: "should exit early if slices are empty", @@ -28,11 +36,11 @@ func Test_convertToDoguConfigEntryDiffsDTO(t *testing.T) { Key: "container_config/memory_limit", }, Actual: domain.DoguConfigValueState{ - Value: "512m", + Value: &limit512, Exists: true, }, Expected: domain.DoguConfigValueState{ - Value: "1024m", + Value: &limit1024, Exists: true, }, NeededAction: domain.ConfigActionSet, @@ -46,120 +54,42 @@ func Test_convertToDoguConfigEntryDiffsDTO(t *testing.T) { Exists: false, }, Expected: domain.DoguConfigValueState{ - Value: "512m", + Value: &limit512, Exists: true, }, NeededAction: domain.ConfigActionSet, }, }, - want: []crd.DoguConfigEntryDiff{ - { - Key: "container_config/memory_limit", - Actual: crd.DoguConfigValueState{ - Value: "512m", - Exists: true, - }, - Expected: crd.DoguConfigValueState{ - Value: "1024m", - Exists: true, - }, - NeededAction: "set", - }, - { - Key: "container_config/swap_limit", - Actual: crd.DoguConfigValueState{ - Exists: false, - }, - Expected: crd.DoguConfigValueState{ - Value: "512m", - Exists: true, - }, - NeededAction: "set", - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assert.Equalf(t, tt.want, convertToDoguConfigEntryDiffsDTO(tt.domainModel), "convertToDoguConfigEntryDiffsDTO(%v)", tt.domainModel) - }) - } -} - -func Test_convertToDoguConfigDiffsDomain(t *testing.T) { - tests := []struct { - name string - dto crd.DoguConfigDiff - want domain.DoguConfigDiffs - }{ - { - name: "should exit early if slices are empty", - dto: crd.DoguConfigDiff{}, - want: nil, - }, - { - name: "should convert multiple dogu config diffs", - dto: crd.DoguConfigDiff{ + want: crd.DoguConfigDiff{ { Key: "container_config/memory_limit", - Actual: crd.DoguConfigValueState{ - Value: "512m", + Actual: crd.ConfigValueState{ + Value: &limit512, Exists: true, }, - Expected: crd.DoguConfigValueState{ - Value: "1024m", + Expected: crd.ConfigValueState{ + Value: &limit1024, Exists: true, }, NeededAction: "set", }, { Key: "container_config/swap_limit", - Actual: crd.DoguConfigValueState{ + Actual: crd.ConfigValueState{ Exists: false, }, - Expected: crd.DoguConfigValueState{ - Value: "512m", + Expected: crd.ConfigValueState{ + Value: &limit512, Exists: true, }, NeededAction: "set", }, }, - want: domain.DoguConfigDiffs{ - { - Key: common.DoguConfigKey{ - DoguName: "ldap", - Key: "container_config/memory_limit", - }, - Actual: domain.DoguConfigValueState{ - Value: "512m", - Exists: true, - }, - Expected: domain.DoguConfigValueState{ - Value: "1024m", - Exists: true, - }, - NeededAction: domain.ConfigActionSet, - }, - { - Key: common.DoguConfigKey{ - DoguName: "ldap", - Key: "container_config/swap_limit", - }, - Actual: domain.DoguConfigValueState{ - Exists: false, - }, - Expected: domain.DoguConfigValueState{ - Value: "512m", - Exists: true, - }, - NeededAction: domain.ConfigActionSet, - }, - }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.Equalf(t, tt.want, convertToDoguConfigDiffsDomain("ldap", tt.dto), "convertToDoguConfigDiffsDomain(%v, %v)", "ldap", tt.dto) + assert.Equalf(t, tt.want, convertToDoguConfigEntryDiffsDTO(tt.domainModel, tt.isSensitive), "convertToDoguConfigEntryDiffsDTO(%v)", tt.domainModel) }) } } diff --git a/pkg/adapter/kubernetes/blueprintcr/v2/serializer/doguDiff.go b/pkg/adapter/kubernetes/blueprintcr/v2/serializer/doguDiff.go index f44c214c..8920241d 100644 --- a/pkg/adapter/kubernetes/blueprintcr/v2/serializer/doguDiff.go +++ b/pkg/adapter/kubernetes/blueprintcr/v2/serializer/doguDiff.go @@ -1,10 +1,6 @@ package serializer import ( - "errors" - "fmt" - cescommons "github.com/cloudogu/ces-commons-lib/dogu" - "github.com/cloudogu/cesapp-lib/core" crd "github.com/cloudogu/k8s-blueprint-lib/v2/api/v2" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" @@ -18,152 +14,68 @@ func convertToDoguDiffDTO(domainModel domain.DoguDiff) crd.DoguDiff { } return crd.DoguDiff{ - Actual: crd.DoguDiffState{ - Namespace: string(domainModel.Actual.Namespace), - Version: domainModel.Actual.Version.Raw, - InstallationState: domainModel.Actual.InstallationState.String(), - ResourceConfig: crd.ResourceConfig{ - MinVolumeSize: convertMinimumVolumeSizeToDTO(domainModel.Actual.MinVolumeSize), - }, - ReverseProxyConfig: crd.ReverseProxyConfig{ - MaxBodySize: ecosystem.GetQuantityString(domainModel.Actual.ReverseProxyConfig.MaxBodySize), - RewriteTarget: string(domainModel.Actual.ReverseProxyConfig.RewriteTarget), - AdditionalConfig: string(domainModel.Actual.ReverseProxyConfig.AdditionalConfig), - }, - AdditionalMounts: convertAdditionalMountsToDoguDiffDTO(domainModel.Actual.AdditionalMounts), - }, - Expected: crd.DoguDiffState{ - Namespace: string(domainModel.Expected.Namespace), - Version: domainModel.Expected.Version.Raw, - InstallationState: domainModel.Expected.InstallationState.String(), - ResourceConfig: crd.ResourceConfig{ - MinVolumeSize: convertMinimumVolumeSizeToDTO(domainModel.Expected.MinVolumeSize), - }, - ReverseProxyConfig: crd.ReverseProxyConfig{ - MaxBodySize: ecosystem.GetQuantityString(domainModel.Expected.ReverseProxyConfig.MaxBodySize), - RewriteTarget: string(domainModel.Expected.ReverseProxyConfig.RewriteTarget), - AdditionalConfig: string(domainModel.Expected.ReverseProxyConfig.AdditionalConfig), - }, - AdditionalMounts: convertAdditionalMountsToDoguDiffDTO(domainModel.Expected.AdditionalMounts), - }, + Actual: convertToDoguDiffStateDTO(domainModel.Actual), + Expected: convertToDoguDiffStateDTO(domainModel.Expected), NeededActions: doguActions, } } -func convertMinimumVolumeSizeToDTO(minVolSize ecosystem.VolumeSize) string { - if minVolSize.IsZero() { - return "" - } else { - return minVolSize.String() +func convertToDoguDiffStateDTO(domainModel domain.DoguDiffState) crd.DoguDiffState { + var version *string + if domainModel.Version != nil { + version = &domainModel.Version.Raw } -} -func convertAdditionalMountsToDoguDiffDTO(mounts []ecosystem.AdditionalMount) []crd.AdditionalMount { - if len(mounts) == 0 { - // an empty slice and nil are serialized differently - // we want no entry instead of an empty json list if there are no mounts given - return nil - } - result := make([]crd.AdditionalMount, len(mounts)) - for index, mount := range mounts { - result[index] = crd.AdditionalMount{ - SourceType: crd.DataSourceType(mount.SourceType), - Name: mount.Name, - Volume: mount.Volume, - Subfolder: mount.Subfolder, + var reverseProxyConfig *crd.ReverseProxyConfig + if !domainModel.ReverseProxyConfig.IsEmpty() { + var rewriteTarget, additionalConfig *string + if domainModel.ReverseProxyConfig.RewriteTarget != "" { + rewriteTarget = (*string)(&domainModel.ReverseProxyConfig.RewriteTarget) } - } - return result -} - -func convertToDoguDiffDomain(doguName string, dto crd.DoguDiff) (domain.DoguDiff, error) { - - actualState, actualStateErr := convertDoguDiffStateDomain(dto.Actual) - if actualStateErr != nil { - actualStateErr = fmt.Errorf("failed to convert actual dogu diff state: %w", actualStateErr) - } - expectedState, expectedStateErr := convertDoguDiffStateDomain(dto.Expected) - if actualStateErr != nil { - actualStateErr = fmt.Errorf("failed to convert expected dogu diff state: %w", actualStateErr) - } - - err := errors.Join(actualStateErr, expectedStateErr) - if err != nil { - return domain.DoguDiff{}, fmt.Errorf("failed to convert dogu diff dto %q to domain model: %w", doguName, err) - } - - neededActions := dto.NeededActions - doguActions := make([]domain.Action, 0, len(neededActions)) - for _, action := range neededActions { - doguActions = append(doguActions, domain.Action(action)) - } - - return domain.DoguDiff{ - DoguName: cescommons.SimpleName(doguName), - Expected: expectedState, - Actual: actualState, - NeededActions: doguActions, - }, nil -} - -func convertDoguDiffStateDomain(dto crd.DoguDiffState) (domain.DoguDiffState, error) { - var errorList []error - - var version core.Version - var versionErr error - if dto.Version != "" { - version, versionErr = core.ParseVersion(dto.Version) - if versionErr != nil { - versionErr = fmt.Errorf("failed to parse version %q: %w", dto.Version, versionErr) + if domainModel.ReverseProxyConfig.AdditionalConfig != "" { + additionalConfig = (*string)(&domainModel.ReverseProxyConfig.AdditionalConfig) + } + reverseProxyConfig = &crd.ReverseProxyConfig{ + RewriteTarget: rewriteTarget, + AdditionalConfig: additionalConfig, + MaxBodySize: ecosystem.GetQuantityString(domainModel.ReverseProxyConfig.MaxBodySize), } - } - errorList = append(errorList, versionErr) - - state, stateErr := ToOldDomainTargetState(dto.InstallationState) - if stateErr != nil { - errorList = append(errorList, fmt.Errorf("failed to parse installation state %q: %w", dto.InstallationState, stateErr)) } - minVolumeSize, volumeSizeErr := ecosystem.GetNonNilQuantityRef(dto.ResourceConfig.MinVolumeSize) - if volumeSizeErr != nil { - errorList = append(errorList, fmt.Errorf("failed to parse minimum volume size %q: %w", dto.ResourceConfig.MinVolumeSize, volumeSizeErr)) + var resourceConfig *crd.ResourceConfig + if domainModel.MinVolumeSize != nil { + resourceConfig = &crd.ResourceConfig{ + MinVolumeSize: ecosystem.GetQuantityString(domainModel.MinVolumeSize), + } } - - maxBodySize, bodySizeErr := ecosystem.GetQuantityReference(dto.ReverseProxyConfig.MaxBodySize) - if bodySizeErr != nil { - errorList = append(errorList, fmt.Errorf("failed to parse maximum proxy body size %q: %w", dto.ReverseProxyConfig.MaxBodySize, bodySizeErr)) + return crd.DoguDiffState{ + Namespace: string(domainModel.Namespace), + Version: version, + Absent: domainModel.Absent, + ResourceConfig: resourceConfig, + ReverseProxyConfig: reverseProxyConfig, + AdditionalMounts: convertAdditionalMountsToDoguDiffDTO(domainModel.AdditionalMounts), } - - return domain.DoguDiffState{ - Namespace: cescommons.Namespace(dto.Namespace), - Version: version, - InstallationState: state, - MinVolumeSize: *minVolumeSize, - ReverseProxyConfig: ecosystem.ReverseProxyConfig{ - MaxBodySize: maxBodySize, - RewriteTarget: ecosystem.RewriteTarget(dto.ReverseProxyConfig.RewriteTarget), - AdditionalConfig: ecosystem.AdditionalConfig(dto.ReverseProxyConfig.AdditionalConfig), - }, - AdditionalMounts: convertAdditionalMountsToDoguDiffDomain(dto.AdditionalMounts), - }, errors.Join(errorList...) } -func convertAdditionalMountsToDoguDiffDomain(mounts []crd.AdditionalMount) []ecosystem.AdditionalMount { +func convertAdditionalMountsToDoguDiffDTO(mounts []ecosystem.AdditionalMount) []crd.AdditionalMount { if len(mounts) == 0 { // an empty slice and nil are serialized differently // we want no entry instead of an empty json list if there are no mounts given return nil } - result := make([]ecosystem.AdditionalMount, len(mounts)) - + result := make([]crd.AdditionalMount, len(mounts)) for index, mount := range mounts { - result[index] = ecosystem.AdditionalMount{ - SourceType: ecosystem.DataSourceType(mount.SourceType), + var subfolder *string + if mount.Subfolder != "" { + subfolder = &mount.Subfolder + } + result[index] = crd.AdditionalMount{ + SourceType: crd.DataSourceType(mount.SourceType), Name: mount.Name, Volume: mount.Volume, - Subfolder: mount.Subfolder, + Subfolder: subfolder, } } - return result } diff --git a/pkg/adapter/kubernetes/blueprintcr/v2/serializer/doguDiff_test.go b/pkg/adapter/kubernetes/blueprintcr/v2/serializer/doguDiff_test.go index e9973067..5c851d93 100644 --- a/pkg/adapter/kubernetes/blueprintcr/v2/serializer/doguDiff_test.go +++ b/pkg/adapter/kubernetes/blueprintcr/v2/serializer/doguDiff_test.go @@ -1,32 +1,66 @@ package serializer import ( + "testing" + + crd "github.com/cloudogu/k8s-blueprint-lib/v2/api/v2" + "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" + "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" - "k8s.io/apimachinery/pkg/api/resource" - "testing" ) -func Test_convertMinimumVolumeSizeToDTO(t *testing.T) { - tests := []struct { - name string - minVolSize ecosystem.VolumeSize - want string - }{ - { - name: "empty", - minVolSize: ecosystem.VolumeSize{}, - want: "", - }, - { - name: "empty", - minVolSize: resource.MustParse("1Gi"), - want: "1Gi", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assert.Equalf(t, tt.want, convertMinimumVolumeSizeToDTO(tt.minVolSize), "convertMinimumVolumeSizeToDTO(%v)", tt.minVolSize) - }) - } +func Test_convertToDoguDiffStateDTO(t *testing.T) { + t.Run("should convert empty reverse proxy config to nil", func(t *testing.T) { + // given + domainDiffState := domain.DoguDiffState{ + ReverseProxyConfig: ecosystem.ReverseProxyConfig{}, + } + // when + result := convertToDoguDiffStateDTO(domainDiffState) + // then + assert.Nil(t, result.ReverseProxyConfig) + }) + + t.Run("should convert reverse proxy config", func(t *testing.T) { + // given + domainDiffState := domain.DoguDiffState{ + ReverseProxyConfig: ecosystem.ReverseProxyConfig{ + MaxBodySize: &proxyBodySize, + RewriteTarget: ecosystem.RewriteTarget(rewriteTarget), + AdditionalConfig: ecosystem.AdditionalConfig(additionalConfig), + }, + } + // when + result := convertToDoguDiffStateDTO(domainDiffState) + // then + want := crd.DoguDiffState{ + ReverseProxyConfig: &crd.ReverseProxyConfig{ + MaxBodySize: &proxyBodySizeString, + RewriteTarget: &rewriteTarget, + AdditionalConfig: &additionalConfig, + }, + } + + assert.NotNil(t, result.ReverseProxyConfig) + assert.Empty(t, cmp.Diff(want, result)) + }) + + t.Run("should convert resource config", func(t *testing.T) { + // given + domainDiffState := domain.DoguDiffState{ + MinVolumeSize: &volumeSize, + } + // when + result := convertToDoguDiffStateDTO(domainDiffState) + // then + want := crd.DoguDiffState{ + ResourceConfig: &crd.ResourceConfig{ + MinVolumeSize: &volumeSizeString, + }, + } + + assert.NotNil(t, result.ResourceConfig) + assert.Empty(t, cmp.Diff(want, result)) + }) } diff --git a/pkg/adapter/kubernetes/blueprintcr/v2/serializer/dogu_test.go b/pkg/adapter/kubernetes/blueprintcr/v2/serializer/dogu_test.go index 39ae6c42..907e6c75 100644 --- a/pkg/adapter/kubernetes/blueprintcr/v2/serializer/dogu_test.go +++ b/pkg/adapter/kubernetes/blueprintcr/v2/serializer/dogu_test.go @@ -2,9 +2,10 @@ package serializer import ( "fmt" - bpv2 "github.com/cloudogu/k8s-blueprint-lib/v2/api/v2" "testing" + bpv2 "github.com/cloudogu/k8s-blueprint-lib/v2/api/v2" + "github.com/stretchr/testify/assert" "k8s.io/apimachinery/pkg/api/resource" @@ -14,12 +15,26 @@ import ( "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" ) +var ( + wrongVersion = "1." + rewriteTarget = "/" + additionalConfig = "additional" + volumeSize = resource.MustParse("1Gi") + volumeSizeString = volumeSize.String() + proxyBodySize = resource.MustParse("1G") + proxyBodySizeString = proxyBodySize.String() + subfolder = "subfolder" + subfolder2 = "secsubfolder" +) + func TestConvertDogus(t *testing.T) { type args struct { dogus []bpv2.Dogu } - proxyBodySize := resource.MustParse("1G") - volumeSize := resource.MustParse("1Gi") + + wrongBodySize := "1GE" + wrongVolumeSize := "1GIE" + tests := []struct { name string args args @@ -40,61 +55,61 @@ func TestConvertDogus(t *testing.T) { }, { name: "normal dogu", - args: args{dogus: []bpv2.Dogu{{Name: "official/postgres", Version: version3211.Raw, Absent: false}}}, - want: []domain.Dogu{{Name: cescommons.QualifiedName{Namespace: "official", SimpleName: "postgres"}, Version: version3211, TargetState: domain.TargetStatePresent}}, + args: args{dogus: []bpv2.Dogu{{Name: "official/postgres", Version: &version3211.Raw, Absent: &falseVar}}}, + want: []domain.Dogu{{Name: cescommons.QualifiedName{Namespace: "official", SimpleName: "postgres"}, Version: &version3211, Absent: false}}, wantErr: assert.NoError, }, { name: "absent dogu", - args: args{dogus: []bpv2.Dogu{{Name: "official/postgres", Absent: true}}}, - want: []domain.Dogu{{Name: cescommons.QualifiedName{Namespace: "official", SimpleName: "postgres"}, TargetState: domain.TargetStateAbsent}}, + args: args{dogus: []bpv2.Dogu{{Name: "official/postgres", Absent: &trueVar}}}, + want: []domain.Dogu{{Name: cescommons.QualifiedName{Namespace: "official", SimpleName: "postgres"}, Absent: true}}, wantErr: assert.NoError, }, { name: "dogu with max proxy body size", - args: args{dogus: []bpv2.Dogu{{Name: "official/postgres", Version: version3211.Raw, Absent: false, PlatformConfig: bpv2.PlatformConfig{ReverseProxyConfig: bpv2.ReverseProxyConfig{MaxBodySize: "1G"}}}}}, - want: []domain.Dogu{{Name: cescommons.QualifiedName{Namespace: "official", SimpleName: "postgres"}, Version: version3211, TargetState: domain.TargetStatePresent, ReverseProxyConfig: ecosystem.ReverseProxyConfig{MaxBodySize: &proxyBodySize}}}, + args: args{dogus: []bpv2.Dogu{{Name: "official/postgres", Version: &version3211.Raw, Absent: &falseVar, PlatformConfig: &bpv2.PlatformConfig{ReverseProxyConfig: &bpv2.ReverseProxyConfig{MaxBodySize: &proxyBodySizeString}}}}}, + want: []domain.Dogu{{Name: cescommons.QualifiedName{Namespace: "official", SimpleName: "postgres"}, Version: &version3211, Absent: false, ReverseProxyConfig: ecosystem.ReverseProxyConfig{MaxBodySize: &proxyBodySize}}}, wantErr: assert.NoError, }, { name: "dogu with proxy rewrite target", - args: args{dogus: []bpv2.Dogu{{Name: "official/postgres", Version: version3211.Raw, Absent: false, PlatformConfig: bpv2.PlatformConfig{ReverseProxyConfig: bpv2.ReverseProxyConfig{RewriteTarget: "/"}}}}}, - want: []domain.Dogu{{Name: cescommons.QualifiedName{Namespace: "official", SimpleName: "postgres"}, Version: version3211, TargetState: domain.TargetStatePresent, ReverseProxyConfig: ecosystem.ReverseProxyConfig{RewriteTarget: "/"}}}, + args: args{dogus: []bpv2.Dogu{{Name: "official/postgres", Version: &version3211.Raw, Absent: &falseVar, PlatformConfig: &bpv2.PlatformConfig{ReverseProxyConfig: &bpv2.ReverseProxyConfig{RewriteTarget: &rewriteTarget}}}}}, + want: []domain.Dogu{{Name: cescommons.QualifiedName{Namespace: "official", SimpleName: "postgres"}, Version: &version3211, Absent: false, ReverseProxyConfig: ecosystem.ReverseProxyConfig{RewriteTarget: ecosystem.RewriteTarget(rewriteTarget)}}}, wantErr: assert.NoError, }, { name: "dogu with proxy additional config", - args: args{dogus: []bpv2.Dogu{{Name: "official/postgres", Version: version3211.Raw, Absent: false, PlatformConfig: bpv2.PlatformConfig{ReverseProxyConfig: bpv2.ReverseProxyConfig{AdditionalConfig: "additional"}}}}}, - want: []domain.Dogu{{Name: cescommons.QualifiedName{Namespace: "official", SimpleName: "postgres"}, Version: version3211, TargetState: domain.TargetStatePresent, ReverseProxyConfig: ecosystem.ReverseProxyConfig{AdditionalConfig: "additional"}}}, + args: args{dogus: []bpv2.Dogu{{Name: "official/postgres", Version: &version3211.Raw, Absent: &falseVar, PlatformConfig: &bpv2.PlatformConfig{ReverseProxyConfig: &bpv2.ReverseProxyConfig{AdditionalConfig: &additionalConfig}}}}}, + want: []domain.Dogu{{Name: cescommons.QualifiedName{Namespace: "official", SimpleName: "postgres"}, Version: &version3211, Absent: false, ReverseProxyConfig: ecosystem.ReverseProxyConfig{AdditionalConfig: ecosystem.AdditionalConfig(additionalConfig)}}}, wantErr: assert.NoError, }, { name: "dogu with invalid proxy body size", - args: args{dogus: []bpv2.Dogu{{Name: "official/postgres", Version: version3211.Raw, Absent: false, PlatformConfig: bpv2.PlatformConfig{ReverseProxyConfig: bpv2.ReverseProxyConfig{MaxBodySize: "1GE"}}}}}, + args: args{dogus: []bpv2.Dogu{{Name: "official/postgres", Version: &version3211.Raw, Absent: &falseVar, PlatformConfig: &bpv2.PlatformConfig{ReverseProxyConfig: &bpv2.ReverseProxyConfig{MaxBodySize: &wrongBodySize}}}}}, want: nil, wantErr: assert.Error, }, { name: "dogu with min volume size", - args: args{dogus: []bpv2.Dogu{{Name: "official/postgres", Version: version3211.Raw, PlatformConfig: bpv2.PlatformConfig{ResourceConfig: bpv2.ResourceConfig{MinVolumeSize: "1Gi"}}}}}, - want: []domain.Dogu{{Name: cescommons.QualifiedName{Namespace: "official", SimpleName: "postgres"}, Version: version3211, TargetState: domain.TargetStatePresent, MinVolumeSize: volumeSize}}, + args: args{dogus: []bpv2.Dogu{{Name: "official/postgres", Version: &version3211.Raw, PlatformConfig: &bpv2.PlatformConfig{ResourceConfig: &bpv2.ResourceConfig{MinVolumeSize: &volumeSizeString}}}}}, + want: []domain.Dogu{{Name: cescommons.QualifiedName{Namespace: "official", SimpleName: "postgres"}, Version: &version3211, Absent: false, MinVolumeSize: &volumeSize}}, wantErr: assert.NoError, }, { name: "dogu with invalid volume size", - args: args{dogus: []bpv2.Dogu{{Name: "official/postgres", Version: version3211.Raw, PlatformConfig: bpv2.PlatformConfig{ResourceConfig: bpv2.ResourceConfig{MinVolumeSize: "1GIE"}}}}}, + args: args{dogus: []bpv2.Dogu{{Name: "official/postgres", Version: &version3211.Raw, PlatformConfig: &bpv2.PlatformConfig{ResourceConfig: &bpv2.ResourceConfig{MinVolumeSize: &wrongVolumeSize}}}}}, want: nil, wantErr: assert.Error, }, { name: "no namespace", - args: args{dogus: []bpv2.Dogu{{Name: "postgres", Version: version3211.Raw}}}, + args: args{dogus: []bpv2.Dogu{{Name: "postgres", Version: &version3211.Raw}}}, want: nil, wantErr: assert.Error, }, { name: "unparsable version", - args: args{dogus: []bpv2.Dogu{{Name: "official/postgres", Version: "1."}}}, + args: args{dogus: []bpv2.Dogu{{Name: "official/postgres", Version: &wrongVersion}}}, want: nil, wantErr: assert.Error, }, @@ -102,40 +117,40 @@ func TestConvertDogus(t *testing.T) { name: "should convert additionalMounts", args: args{dogus: []bpv2.Dogu{{ Name: "official/postgres", - Version: version3211.Raw, - PlatformConfig: bpv2.PlatformConfig{ + Version: &version3211.Raw, + PlatformConfig: &bpv2.PlatformConfig{ AdditionalMountsConfig: []bpv2.AdditionalMount{ { SourceType: bpv2.DataSourceConfigMap, Name: "configMap", Volume: "volume", - Subfolder: "subfolder", + Subfolder: &subfolder, }, { SourceType: bpv2.DataSourceSecret, Name: "sec", Volume: "secvolume", - Subfolder: "secsubfolder", + Subfolder: &subfolder2, }, }, }, }}}, want: []domain.Dogu{{ - Name: cescommons.QualifiedName{Namespace: "official", SimpleName: "postgres"}, - Version: version3211, - TargetState: domain.TargetStatePresent, + Name: cescommons.QualifiedName{Namespace: "official", SimpleName: "postgres"}, + Version: &version3211, + Absent: false, AdditionalMounts: []ecosystem.AdditionalMount{ { SourceType: ecosystem.DataSourceConfigMap, Name: "configMap", Volume: "volume", - Subfolder: "subfolder", + Subfolder: subfolder, }, { SourceType: ecosystem.DataSourceSecret, Name: "sec", Volume: "secvolume", - Subfolder: "secsubfolder", + Subfolder: subfolder2, }, }}}, wantErr: assert.NoError, @@ -144,15 +159,15 @@ func TestConvertDogus(t *testing.T) { name: "should return nil slice if dogu contains an nil slice", args: args{dogus: []bpv2.Dogu{{ Name: "official/postgres", - Version: version3211.Raw, - PlatformConfig: bpv2.PlatformConfig{ + Version: &version3211.Raw, + PlatformConfig: &bpv2.PlatformConfig{ AdditionalMountsConfig: nil, }, }}}, want: []domain.Dogu{{ Name: cescommons.QualifiedName{Namespace: "official", SimpleName: "postgres"}, - Version: version3211, - TargetState: domain.TargetStatePresent, + Version: &version3211, + Absent: false, AdditionalMounts: nil, }}, wantErr: assert.NoError, @@ -193,25 +208,25 @@ func TestConvertMaskDogus(t *testing.T) { }, { name: "normal dogu", - args: args{dogus: []bpv2.MaskDogu{{Name: "official/postgres", Version: version3211.Raw, Absent: false}}}, - want: []domain.MaskDogu{{Name: cescommons.QualifiedName{Namespace: "official", SimpleName: "postgres"}, Version: version3211, TargetState: domain.TargetStatePresent}}, + args: args{dogus: []bpv2.MaskDogu{{Name: "official/postgres", Version: &version3211.Raw, Absent: &falseVar}}}, + want: []domain.MaskDogu{{Name: cescommons.QualifiedName{Namespace: "official", SimpleName: "postgres"}, Version: version3211, Absent: false}}, wantErr: assert.NoError, }, { name: "absent dogu", - args: args{dogus: []bpv2.MaskDogu{{Name: "official/postgres", Absent: true}}}, - want: []domain.MaskDogu{{Name: cescommons.QualifiedName{Namespace: "official", SimpleName: "postgres"}, TargetState: domain.TargetStateAbsent}}, + args: args{dogus: []bpv2.MaskDogu{{Name: "official/postgres", Absent: &trueVar}}}, + want: []domain.MaskDogu{{Name: cescommons.QualifiedName{Namespace: "official", SimpleName: "postgres"}, Absent: true}}, wantErr: assert.NoError, }, { name: "no namespace", - args: args{dogus: []bpv2.MaskDogu{{Name: "postgres", Version: version3211.Raw}}}, + args: args{dogus: []bpv2.MaskDogu{{Name: "postgres", Version: &version3211.Raw}}}, want: nil, wantErr: assert.Error, }, { name: "unparsable version", - args: args{dogus: []bpv2.MaskDogu{{Name: "official/postgres", Version: "1."}}}, + args: args{dogus: []bpv2.MaskDogu{{Name: "official/postgres", Version: &wrongVersion}}}, want: nil, wantErr: assert.Error, }, @@ -231,8 +246,6 @@ func TestConvertToDoguDTOs(t *testing.T) { type args struct { dogus []domain.Dogu } - bodySize := resource.MustParse("100M") - volumeSize := resource.MustParse("1G") tests := []struct { name string args args @@ -250,51 +263,51 @@ func TestConvertToDoguDTOs(t *testing.T) { }, { name: "ok", - args: args{dogus: []domain.Dogu{{Name: cescommons.QualifiedName{Namespace: "official", SimpleName: "postgres"}, Version: version3211, TargetState: domain.TargetStatePresent, MinVolumeSize: volumeSize, ReverseProxyConfig: ecosystem.ReverseProxyConfig{MaxBodySize: &bodySize, RewriteTarget: "/", AdditionalConfig: "additional"}}}}, - want: []bpv2.Dogu{{Name: "official/postgres", Version: version3211.Raw, Absent: false, PlatformConfig: bpv2.PlatformConfig{ResourceConfig: bpv2.ResourceConfig{MinVolumeSize: "1G"}, ReverseProxyConfig: bpv2.ReverseProxyConfig{MaxBodySize: "100M", RewriteTarget: "/", AdditionalConfig: "additional"}}}}, + args: args{dogus: []domain.Dogu{{Name: cescommons.QualifiedName{Namespace: "official", SimpleName: "postgres"}, Version: &version3211, Absent: false, MinVolumeSize: &volumeSize, ReverseProxyConfig: ecosystem.ReverseProxyConfig{MaxBodySize: &proxyBodySize, RewriteTarget: ecosystem.RewriteTarget(rewriteTarget), AdditionalConfig: ecosystem.AdditionalConfig(additionalConfig)}}}}, + want: []bpv2.Dogu{{Name: "official/postgres", Version: &version3211.Raw, Absent: &falseVar, PlatformConfig: &bpv2.PlatformConfig{ResourceConfig: &bpv2.ResourceConfig{MinVolumeSize: &volumeSizeString}, ReverseProxyConfig: &bpv2.ReverseProxyConfig{MaxBodySize: &proxyBodySizeString, RewriteTarget: &rewriteTarget, AdditionalConfig: &additionalConfig}}}}, }, { name: "additionalMountsConfig", args: args{dogus: []domain.Dogu{{ Name: cescommons.QualifiedName{Namespace: "official", SimpleName: "postgres"}, - Version: version3211, - TargetState: domain.TargetStatePresent, - MinVolumeSize: volumeSize, - ReverseProxyConfig: ecosystem.ReverseProxyConfig{MaxBodySize: &bodySize, RewriteTarget: "/", AdditionalConfig: "additional"}, + Version: &version3211, + Absent: false, + MinVolumeSize: &volumeSize, + ReverseProxyConfig: ecosystem.ReverseProxyConfig{MaxBodySize: &proxyBodySize, RewriteTarget: ecosystem.RewriteTarget(rewriteTarget), AdditionalConfig: ecosystem.AdditionalConfig(additionalConfig)}, AdditionalMounts: []ecosystem.AdditionalMount{ { SourceType: ecosystem.DataSourceConfigMap, Name: "configMap", Volume: "volume", - Subfolder: "subfolder", + Subfolder: subfolder, }, { SourceType: ecosystem.DataSourceSecret, Name: "sec", Volume: "secvolume", - Subfolder: "secsubfolder", + Subfolder: subfolder2, }, }, }}}, want: []bpv2.Dogu{{ Name: "official/postgres", - Version: version3211.Raw, - Absent: false, - PlatformConfig: bpv2.PlatformConfig{ - ResourceConfig: bpv2.ResourceConfig{MinVolumeSize: "1G"}, - ReverseProxyConfig: bpv2.ReverseProxyConfig{MaxBodySize: "100M", RewriteTarget: "/", AdditionalConfig: "additional"}, + Version: &version3211.Raw, + Absent: &falseVar, + PlatformConfig: &bpv2.PlatformConfig{ + ResourceConfig: &bpv2.ResourceConfig{MinVolumeSize: &volumeSizeString}, + ReverseProxyConfig: &bpv2.ReverseProxyConfig{MaxBodySize: &proxyBodySizeString, RewriteTarget: &rewriteTarget, AdditionalConfig: &additionalConfig}, AdditionalMountsConfig: []bpv2.AdditionalMount{ { SourceType: bpv2.DataSourceConfigMap, Name: "configMap", Volume: "volume", - Subfolder: "subfolder", + Subfolder: &subfolder, }, { SourceType: bpv2.DataSourceSecret, Name: "sec", Volume: "secvolume", - Subfolder: "secsubfolder", + Subfolder: &subfolder2, }, }, }}}, @@ -303,19 +316,19 @@ func TestConvertToDoguDTOs(t *testing.T) { name: "should return nil slice if dogu contains an nil slice", args: args{dogus: []domain.Dogu{{ Name: cescommons.QualifiedName{Namespace: "official", SimpleName: "postgres"}, - Version: version3211, - TargetState: domain.TargetStatePresent, - MinVolumeSize: volumeSize, - ReverseProxyConfig: ecosystem.ReverseProxyConfig{MaxBodySize: &bodySize, RewriteTarget: "/", AdditionalConfig: "additional"}, + Version: &version3211, + Absent: false, + MinVolumeSize: &volumeSize, + ReverseProxyConfig: ecosystem.ReverseProxyConfig{MaxBodySize: &proxyBodySize, RewriteTarget: ecosystem.RewriteTarget(rewriteTarget), AdditionalConfig: ecosystem.AdditionalConfig(additionalConfig)}, AdditionalMounts: nil, }}}, want: []bpv2.Dogu{{ Name: "official/postgres", - Version: version3211.Raw, - Absent: false, - PlatformConfig: bpv2.PlatformConfig{ - ResourceConfig: bpv2.ResourceConfig{MinVolumeSize: "1G"}, - ReverseProxyConfig: bpv2.ReverseProxyConfig{MaxBodySize: "100M", RewriteTarget: "/", AdditionalConfig: "additional"}, + Version: &version3211.Raw, + Absent: &falseVar, + PlatformConfig: &bpv2.PlatformConfig{ + ResourceConfig: &bpv2.ResourceConfig{MinVolumeSize: &volumeSizeString}, + ReverseProxyConfig: &bpv2.ReverseProxyConfig{MaxBodySize: &proxyBodySizeString, RewriteTarget: &rewriteTarget, AdditionalConfig: &additionalConfig}, AdditionalMountsConfig: nil, }}}, }, diff --git a/pkg/adapter/kubernetes/blueprintcr/v2/serializer/globalConfigDiff.go b/pkg/adapter/kubernetes/blueprintcr/v2/serializer/globalConfigDiff.go index 2838bd0b..74391050 100644 --- a/pkg/adapter/kubernetes/blueprintcr/v2/serializer/globalConfigDiff.go +++ b/pkg/adapter/kubernetes/blueprintcr/v2/serializer/globalConfigDiff.go @@ -3,36 +3,8 @@ package serializer import ( crd "github.com/cloudogu/k8s-blueprint-lib/v2/api/v2" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/common" ) -func convertToGlobalConfigDiffDomain(dto crd.GlobalConfigDiff) domain.GlobalConfigDiffs { - if len(dto) == 0 { - return nil - } - - globalConfigDiff := make(domain.GlobalConfigDiffs, len(dto)) - for i, entryDiff := range dto { - globalConfigDiff[i] = convertToGlobalConfigEntryDiffDomain(entryDiff) - } - return globalConfigDiff -} - -func convertToGlobalConfigEntryDiffDomain(dto crd.GlobalConfigEntryDiff) domain.GlobalConfigEntryDiff { - return domain.GlobalConfigEntryDiff{ - Key: common.GlobalConfigKey(dto.Key), - Actual: domain.GlobalConfigValueState{ - Value: dto.Actual.Value, - Exists: dto.Actual.Exists, - }, - Expected: domain.GlobalConfigValueState{ - Value: dto.Expected.Value, - Exists: dto.Expected.Exists, - }, - NeededAction: domain.ConfigAction(dto.NeededAction), - } -} - func convertToGlobalConfigDiffDTO(domainModel domain.GlobalConfigDiffs) crd.GlobalConfigDiff { if len(domainModel) == 0 { return nil @@ -45,14 +17,14 @@ func convertToGlobalConfigDiffDTO(domainModel domain.GlobalConfigDiffs) crd.Glob return globalConfigDiff } -func convertToGlobalConfigEntryDiffDTO(domainModel domain.GlobalConfigEntryDiff) crd.GlobalConfigEntryDiff { - return crd.GlobalConfigEntryDiff{ +func convertToGlobalConfigEntryDiffDTO(domainModel domain.GlobalConfigEntryDiff) crd.ConfigEntryDiff { + return crd.ConfigEntryDiff{ Key: string(domainModel.Key), - Actual: crd.GlobalConfigValueState{ + Actual: crd.ConfigValueState{ Value: domainModel.Actual.Value, Exists: domainModel.Actual.Exists, }, - Expected: crd.GlobalConfigValueState{ + Expected: crd.ConfigValueState{ Value: domainModel.Expected.Value, Exists: domainModel.Expected.Exists, }, diff --git a/pkg/adapter/kubernetes/blueprintcr/v2/serializer/sensitiveConfig.go b/pkg/adapter/kubernetes/blueprintcr/v2/serializer/sensitiveConfig.go deleted file mode 100644 index 5677d02c..00000000 --- a/pkg/adapter/kubernetes/blueprintcr/v2/serializer/sensitiveConfig.go +++ /dev/null @@ -1,92 +0,0 @@ -package serializer - -import ( - cescommons "github.com/cloudogu/ces-commons-lib/dogu" - v2 "github.com/cloudogu/k8s-blueprint-lib/v2/api/v2" - "github.com/cloudogu/k8s-registry-lib/config" - - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/common" -) - -func convertToSensitiveDoguConfigDTO(config domain.SensitiveDoguConfig) *v2.SensitiveDoguConfig { - // empty struct -> nil - if len(config.Absent) == 0 && len(config.Present) == 0 { - return nil - } - - var present []v2.SensitiveConfigEntry - // we check for empty values to make good use of default values - // this makes testing easier - if len(config.Present) != 0 { - present = make([]v2.SensitiveConfigEntry, len(config.Present)) - index := 0 - for key, value := range config.Present { - present[index] = v2.SensitiveConfigEntry{ - Key: string(key.Key), - SecretName: value.SecretName, - SecretKey: value.SecretKey, - } - index += 1 - } - } - - var absent []string - // we check for empty values to make good use of default values - // this makes testing easier - if len(config.Absent) != 0 { - absent = make([]string, len(config.Absent)) - for i, key := range config.Absent { - absent[i] = string(key.Key) - } - } - - return &v2.SensitiveDoguConfig{ - Present: present, - Absent: absent, - } -} - -func convertToSensitiveDoguConfigDomain(doguName string, doguConfig *v2.SensitiveDoguConfig) domain.SensitiveDoguConfig { - if doguConfig == nil { - return domain.SensitiveDoguConfig{} - } - - var present map[common.SensitiveDoguConfigKey]domain.SensitiveValueRef - // we check for empty values to make good use of default values - // this makes testing easier - if len(doguConfig.Present) != 0 { - present = make(map[common.SensitiveDoguConfigKey]domain.SensitiveValueRef, len(doguConfig.Present)) - for _, value := range doguConfig.Present { - present[convertToSensitiveDoguConfigKeyDomain(doguName, value.Key)] = domain.SensitiveValueRef{ - SecretName: value.SecretName, - SecretKey: value.SecretKey, - } - } - } - - var absent []common.SensitiveDoguConfigKey - // we check for empty values to make good use of default values - // this makes testing easier - if len(doguConfig.Absent) != 0 { - absent = make([]common.SensitiveDoguConfigKey, len(doguConfig.Absent)) - for i, key := range doguConfig.Absent { - absent[i] = common.SensitiveDoguConfigKey{ - DoguName: cescommons.SimpleName(doguName), - Key: config.Key(key), - } - } - } - - return domain.SensitiveDoguConfig{ - Present: present, - Absent: absent, - } -} - -func convertToSensitiveDoguConfigKeyDomain(doguName, key string) common.SensitiveDoguConfigKey { - return common.SensitiveDoguConfigKey{ - DoguName: cescommons.SimpleName(doguName), - Key: config.Key(key), - } -} diff --git a/pkg/adapter/kubernetes/blueprintcr/v2/serializer/sensitiveConfig_test.go b/pkg/adapter/kubernetes/blueprintcr/v2/serializer/sensitiveConfig_test.go deleted file mode 100644 index 1d870a6b..00000000 --- a/pkg/adapter/kubernetes/blueprintcr/v2/serializer/sensitiveConfig_test.go +++ /dev/null @@ -1,143 +0,0 @@ -package serializer - -import ( - v2 "github.com/cloudogu/k8s-blueprint-lib/v2/api/v2" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/common" - "github.com/stretchr/testify/assert" - "testing" -) - -func Test_convertToSensitiveDoguConfigDTO(t *testing.T) { - - tests := []struct { - name string - config domain.SensitiveDoguConfig - want *v2.SensitiveDoguConfig - }{ - { - name: "empty struct to nil", - config: domain.SensitiveDoguConfig{}, - want: nil, - }, - { - name: "empty config to nil", - config: domain.SensitiveDoguConfig{ - Present: nil, - Absent: nil, - }, - want: nil, - }, - { - name: "convert present config", - config: domain.SensitiveDoguConfig{ - Present: map[common.DoguConfigKey]domain.SensitiveValueRef{ - testDoguKey1: { - SecretName: "mySecret", - SecretKey: "myKey", - }, - }, - }, - want: &v2.SensitiveDoguConfig{ - Present: []v2.SensitiveConfigEntry{ - { - SecretName: "mySecret", - SecretKey: "myKey", - Key: string(testDoguKey1.Key), - }, - }, - }, - }, - { - name: "convert absent config", - config: domain.SensitiveDoguConfig{ - Absent: []common.SensitiveDoguConfigKey{ - testDoguKey1, - }, - }, - want: &v2.SensitiveDoguConfig{ - Absent: []string{ - string(testDoguKey1.Key), - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assert.Equalf(t, tt.want, convertToSensitiveDoguConfigDTO(tt.config), "convertToSensitiveDoguConfigDTO(%v)", tt.config) - }) - } -} - -func Test_convertToSensitiveDoguConfigDomain(t *testing.T) { - type args struct { - doguName string - doguConfig *v2.SensitiveDoguConfig - } - tests := []struct { - name string - args args - want domain.SensitiveDoguConfig - }{ - { - name: "nil -> empty struct", - args: args{ - doguName: string(testDoguKey1.DoguName), - doguConfig: nil, - }, - want: domain.SensitiveDoguConfig{}, - }, - { - name: "empty", - args: args{ - doguName: string(testDoguKey1.DoguName), - doguConfig: &v2.SensitiveDoguConfig{}, - }, - want: domain.SensitiveDoguConfig{}, - }, - { - name: "convert present config", - args: args{ - doguName: string(testDoguKey1.DoguName), - doguConfig: &v2.SensitiveDoguConfig{ - Present: []v2.SensitiveConfigEntry{ - { - SecretName: "mySecret", - SecretKey: "myKey", - Key: string(testDoguKey1.Key), - }, - }, - }, - }, - want: domain.SensitiveDoguConfig{ - Present: map[common.DoguConfigKey]domain.SensitiveValueRef{ - testDoguKey1: { - SecretName: "mySecret", - SecretKey: "myKey", - }, - }, - }, - }, - { - name: "convert present config", - args: args{ - doguName: string(testDoguKey1.DoguName), - doguConfig: &v2.SensitiveDoguConfig{ - Absent: []string{ - string(testDoguKey1.Key), - }, - }, - }, - want: domain.SensitiveDoguConfig{ - Absent: []common.SensitiveDoguConfigKey{ - testDoguKey1, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assert.Equalf(t, tt.want, convertToSensitiveDoguConfigDomain(tt.args.doguName, tt.args.doguConfig), "convertToSensitiveDoguConfigDomain(%v, %v)", tt.args.doguName, tt.args.doguConfig) - }) - } -} diff --git a/pkg/adapter/kubernetes/blueprintcr/v2/serializer/stateDiff.go b/pkg/adapter/kubernetes/blueprintcr/v2/serializer/stateDiff.go index ce1f14d2..bc90bf24 100644 --- a/pkg/adapter/kubernetes/blueprintcr/v2/serializer/stateDiff.go +++ b/pkg/adapter/kubernetes/blueprintcr/v2/serializer/stateDiff.go @@ -1,25 +1,19 @@ package serializer import ( - "errors" - "fmt" + "slices" + cescommons "github.com/cloudogu/ces-commons-lib/dogu" crd "github.com/cloudogu/k8s-blueprint-lib/v2/api/v2" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" - "slices" ) -func ConvertToStateDiffDTO(domainModel domain.StateDiff) crd.StateDiff { +func ConvertToStateDiffDTO(domainModel domain.StateDiff) *crd.StateDiff { doguDiffs := make(map[string]crd.DoguDiff, len(domainModel.DoguDiffs)) for _, doguDiff := range domainModel.DoguDiffs { doguDiffs[string(doguDiff.DoguName)] = convertToDoguDiffDTO(doguDiff) } - componentDiffs := make(map[string]crd.ComponentDiff, len(domainModel.ComponentDiffs)) - for _, componentDiff := range domainModel.ComponentDiffs { - componentDiffs[string(componentDiff.Name)] = convertToComponentDiffDTO(componentDiff) - } - var dogus []cescommons.SimpleName var combinedConfigDiffs map[string]crd.CombinedDoguConfigDiff var doguConfigDiffByDogu map[cescommons.SimpleName]crd.DoguConfigDiff @@ -29,12 +23,12 @@ func ConvertToStateDiffDTO(domainModel domain.StateDiff) crd.StateDiff { combinedConfigDiffs = make(map[string]crd.CombinedDoguConfigDiff) doguConfigDiffByDogu = make(map[cescommons.SimpleName]crd.DoguConfigDiff) for doguName, doguConfigDiff := range domainModel.DoguConfigDiffs { - doguConfigDiffByDogu[doguName] = convertToDoguConfigEntryDiffsDTO(doguConfigDiff) + doguConfigDiffByDogu[doguName] = convertToDoguConfigEntryDiffsDTO(doguConfigDiff, false) dogus = append(dogus, doguName) } sensitiveDoguConfigDiff = make(map[cescommons.SimpleName]crd.SensitiveDoguConfigDiff) for doguName, doguConfigDiff := range domainModel.SensitiveDoguConfigDiffs { - sensitiveDoguConfigDiff[doguName] = convertToDoguConfigEntryDiffsDTO(doguConfigDiff) + sensitiveDoguConfigDiff[doguName] = convertToDoguConfigEntryDiffsDTO(doguConfigDiff, true) dogus = append(dogus, doguName) } @@ -48,52 +42,9 @@ func ConvertToStateDiffDTO(domainModel domain.StateDiff) crd.StateDiff { } } - return crd.StateDiff{ + return &crd.StateDiff{ DoguDiffs: doguDiffs, - ComponentDiffs: componentDiffs, DoguConfigDiffs: combinedConfigDiffs, GlobalConfigDiff: convertToGlobalConfigDiffDTO(domainModel.GlobalConfigDiffs), } } - -func ConvertToStateDiffDomain(dto crd.StateDiff) (domain.StateDiff, error) { - var errs []error - - var doguDiffs []domain.DoguDiff - for doguName, doguDiff := range dto.DoguDiffs { - doguDiffDomainModel, err := convertToDoguDiffDomain(doguName, doguDiff) - errs = append(errs, err) - doguDiffs = append(doguDiffs, doguDiffDomainModel) - } - - var componentDiffs []domain.ComponentDiff - for componentName, componentDiff := range dto.ComponentDiffs { - componentDiffDomainModel, err := convertToComponentDiffDomain(componentName, componentDiff) - errs = append(errs, err) - componentDiffs = append(componentDiffs, componentDiffDomainModel) - } - - err := errors.Join(errs...) - if err != nil { - return domain.StateDiff{}, fmt.Errorf("failed to convert state diff DTO to domain model: %w", err) - } - - var doguConfigDiffs map[cescommons.SimpleName]domain.DoguConfigDiffs - var sensitiveDoguConfigDiffs map[cescommons.SimpleName]domain.SensitiveDoguConfigDiffs - if len(dto.DoguConfigDiffs) != 0 { - doguConfigDiffs = map[cescommons.SimpleName]domain.DoguConfigDiffs{} - sensitiveDoguConfigDiffs = map[cescommons.SimpleName]domain.SensitiveDoguConfigDiffs{} - for doguName, combinedConfigDiff := range dto.DoguConfigDiffs { - doguConfigDiffs[cescommons.SimpleName(doguName)] = convertToDoguConfigDiffsDomain(doguName, combinedConfigDiff.DoguConfigDiff) - sensitiveDoguConfigDiffs[cescommons.SimpleName(doguName)] = convertToDoguConfigDiffsDomain(doguName, combinedConfigDiff.SensitiveDoguConfigDiff) - } - } - - return domain.StateDiff{ - DoguDiffs: doguDiffs, - ComponentDiffs: componentDiffs, - DoguConfigDiffs: doguConfigDiffs, - SensitiveDoguConfigDiffs: sensitiveDoguConfigDiffs, - GlobalConfigDiffs: convertToGlobalConfigDiffDomain(dto.GlobalConfigDiff), - }, nil -} diff --git a/pkg/adapter/kubernetes/blueprintcr/v2/serializer/stateDiff_test.go b/pkg/adapter/kubernetes/blueprintcr/v2/serializer/stateDiff_test.go index 52fa9520..222c296a 100644 --- a/pkg/adapter/kubernetes/blueprintcr/v2/serializer/stateDiff_test.go +++ b/pkg/adapter/kubernetes/blueprintcr/v2/serializer/stateDiff_test.go @@ -1,70 +1,71 @@ package serializer import ( - "cmp" - "github.com/Masterminds/semver/v3" + "reflect" + "testing" + cescommons "github.com/cloudogu/ces-commons-lib/dogu" "github.com/cloudogu/cesapp-lib/core" crd "github.com/cloudogu/k8s-blueprint-lib/v2/api/v2" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/common" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" + googlecmp "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" - "reflect" - "slices" - "testing" ) -const testComponentName = "my-component" - var ( - testVersionLowRaw = "1.2.3" - testVersionLow = semver.MustParse(testVersionLowRaw) - testVersionHighRaw = "2.3.4" - testVersionHigh = semver.MustParse(testVersionHighRaw) - testDogu = cescommons.SimpleName("testDogu") - testDogu2 = cescommons.SimpleName("testDogu2") - testDoguKey1 = common.DoguConfigKey{DoguName: testDogu, Key: "key1"} - testDoguKey2 = common.DoguConfigKey{DoguName: testDogu2, Key: "key2"} + testCoreVersionLow = mustParseVersion("1.1.1-1") + testCoreVersionLowStr = testCoreVersionLow.String() + testCoreVersionHigh = mustParseVersion("1.2.3-1") + testCoreVersionHighStr = testCoreVersionHigh.String() + testDogu = cescommons.SimpleName("testDogu") + testDogu2 = cescommons.SimpleName("testDogu2") + testDoguKey1 = common.DoguConfigKey{DoguName: testDogu, Key: "key1"} + testDoguKey2 = common.DoguConfigKey{DoguName: testDogu2, Key: "key2"} + testFqdn1 = "ces1.example.com" + testFqdn2 = "ces2.example.com" + testSubfolderStr = "subfolder" + testSubfolderStr2 = "different_subfolder" ) func TestConvertToDTO(t *testing.T) { tests := []struct { name string domainModel domain.StateDiff - want crd.StateDiff + want *crd.StateDiff }{ { name: "should convert single dogu diff", domainModel: domain.StateDiff{DoguDiffs: []domain.DoguDiff{{ DoguName: "ldap", Actual: domain.DoguDiffState{ - Namespace: "official", - Version: mustParseVersion("1.1.1-1"), - InstallationState: domain.TargetStatePresent, + Namespace: "official", + Version: &testCoreVersionLow, + Absent: false, }, Expected: domain.DoguDiffState{ - Namespace: "official", - Version: mustParseVersion("1.2.3-1"), - InstallationState: domain.TargetStatePresent, + Namespace: "official", + Version: &testCoreVersionHigh, + Absent: false, }, NeededActions: []domain.Action{domain.ActionUpgrade}, }}}, - want: crd.StateDiff{DoguDiffs: map[string]crd.DoguDiff{ + want: &crd.StateDiff{DoguDiffs: map[string]crd.DoguDiff{ "ldap": { Actual: crd.DoguDiffState{ - Namespace: "official", - Version: "1.1.1-1", - InstallationState: "present", + Namespace: "official", + Version: &testCoreVersionLowStr, + Absent: false, }, Expected: crd.DoguDiffState{ - Namespace: "official", - Version: "1.2.3-1", - InstallationState: "present", + Namespace: "official", + Version: &testCoreVersionHighStr, + Absent: false, }, NeededActions: []crd.DoguAction{"upgrade"}, }, - }, ComponentDiffs: map[string]crd.ComponentDiff{}}, + }}, }, { name: "should convert multiple dogu diffs", @@ -72,89 +73,56 @@ func TestConvertToDTO(t *testing.T) { { DoguName: "ldap", Actual: domain.DoguDiffState{ - Namespace: "official", - InstallationState: domain.TargetStateAbsent, + Namespace: "official", + Absent: true, }, Expected: domain.DoguDiffState{ - Namespace: "official", - Version: mustParseVersion("1.2.3-1"), - InstallationState: domain.TargetStatePresent, + Namespace: "official", + Version: &testCoreVersionHigh, + Absent: false, }, NeededActions: []domain.Action{domain.ActionInstall}, }, { DoguName: "nginx-ingress", Actual: domain.DoguDiffState{ - Namespace: "k8s", - Version: mustParseVersion("8.2.3-2"), - InstallationState: domain.TargetStatePresent, + Namespace: "k8s", + Version: &testCoreVersionLow, + Absent: false, }, Expected: domain.DoguDiffState{ - Namespace: "k8s", - InstallationState: domain.TargetStateAbsent, + Namespace: "k8s", + Absent: true, }, NeededActions: []domain.Action{domain.ActionUninstall}, }, }}, - want: crd.StateDiff{DoguDiffs: map[string]crd.DoguDiff{ + want: &crd.StateDiff{DoguDiffs: map[string]crd.DoguDiff{ "ldap": { Actual: crd.DoguDiffState{ - Namespace: "official", - InstallationState: "absent", + Namespace: "official", + Absent: true, }, Expected: crd.DoguDiffState{ - Namespace: "official", - Version: "1.2.3-1", - InstallationState: "present", + Namespace: "official", + Version: &testCoreVersionHighStr, + Absent: false, }, NeededActions: []crd.DoguAction{"install"}, }, "nginx-ingress": { Actual: crd.DoguDiffState{ - Namespace: "k8s", - Version: "8.2.3-2", - InstallationState: "present", + Namespace: "k8s", + Version: &testCoreVersionLowStr, + Absent: false, }, Expected: crd.DoguDiffState{ - Namespace: "k8s", - InstallationState: "absent", + Namespace: "k8s", + Absent: true, }, NeededActions: []crd.DoguAction{"uninstall"}, }, - }, ComponentDiffs: map[string]crd.ComponentDiff{}}, - }, - { - name: "should convert multiple component diffs", - domainModel: domain.StateDiff{ - DoguDiffs: domain.DoguDiffs{}, - ComponentDiffs: []domain.ComponentDiff{ - { - Name: testComponentName, - Actual: domain.ComponentDiffState{Version: testVersionLow, InstallationState: domain.TargetStatePresent}, - Expected: domain.ComponentDiffState{Version: testVersionHigh, InstallationState: domain.TargetStatePresent}, - NeededActions: []domain.Action{domain.ActionUpgrade, domain.ActionSwitchComponentNamespace}, - }, - { - Name: "my-component-2", - Actual: domain.ComponentDiffState{Version: testVersionHigh, InstallationState: domain.TargetStatePresent}, - Expected: domain.ComponentDiffState{InstallationState: domain.TargetStateAbsent}, - NeededActions: []domain.Action{domain.ActionUninstall}, - }, - }}, - want: crd.StateDiff{ - DoguDiffs: map[string]crd.DoguDiff{}, - ComponentDiffs: map[string]crd.ComponentDiff{ - testComponentName: { - Actual: crd.ComponentDiffState{Version: testVersionLowRaw, InstallationState: "present"}, - Expected: crd.ComponentDiffState{Version: testVersionHighRaw, InstallationState: "present"}, - NeededActions: []crd.ComponentAction{"upgrade", "component namespace switch"}, - }, - "my-component-2": { - Actual: crd.ComponentDiffState{Version: testVersionHighRaw, InstallationState: "present"}, - Expected: crd.ComponentDiffState{InstallationState: "absent"}, - NeededActions: []crd.ComponentAction{"uninstall"}, - }, - }}, + }}, }, { name: "should convert multiple dogu config diffs", @@ -168,9 +136,8 @@ func TestConvertToDTO(t *testing.T) { "postfix": {}, }, }, - want: crd.StateDiff{ - DoguDiffs: map[string]crd.DoguDiff{}, - ComponentDiffs: map[string]crd.ComponentDiff{}, + want: &crd.StateDiff{ + DoguDiffs: map[string]crd.DoguDiff{}, DoguConfigDiffs: map[string]crd.CombinedDoguConfigDiff{ "ldap": {}, "postfix": {}, @@ -183,27 +150,26 @@ func TestConvertToDTO(t *testing.T) { GlobalConfigDiffs: []domain.GlobalConfigEntryDiff{{ Key: "fqdn", Actual: domain.GlobalConfigValueState{ - Value: "ces1.example.com", + Value: &testFqdn1, Exists: true, }, Expected: domain.GlobalConfigValueState{ - Value: "ces2.example.com", + Value: &testFqdn2, Exists: true, }, NeededAction: domain.ConfigActionSet, }}, }, - want: crd.StateDiff{ - DoguDiffs: map[string]crd.DoguDiff{}, - ComponentDiffs: map[string]crd.ComponentDiff{}, - GlobalConfigDiff: []crd.GlobalConfigEntryDiff{{ + want: &crd.StateDiff{ + DoguDiffs: map[string]crd.DoguDiff{}, + GlobalConfigDiff: []crd.ConfigEntryDiff{{ Key: "fqdn", - Actual: crd.GlobalConfigValueState{ - Value: "ces1.example.com", + Actual: crd.ConfigValueState{ + Value: &testFqdn1, Exists: true, }, - Expected: crd.GlobalConfigValueState{ - Value: "ces2.example.com", + Expected: crd.ConfigValueState{ + Value: &testFqdn2, Exists: true, }, NeededAction: "set", @@ -215,60 +181,59 @@ func TestConvertToDTO(t *testing.T) { domainModel: domain.StateDiff{DoguDiffs: []domain.DoguDiff{{ DoguName: "ldap", Actual: domain.DoguDiffState{ - Namespace: "official", - Version: mustParseVersion("1.1.1-1"), - InstallationState: domain.TargetStatePresent, + Namespace: "official", + Version: &testCoreVersionLow, + Absent: false, AdditionalMounts: []ecosystem.AdditionalMount{ { SourceType: ecosystem.DataSourceConfigMap, Name: "configmap", Volume: "volume", - Subfolder: "different_subfolder", + Subfolder: testSubfolderStr, }, }, }, Expected: domain.DoguDiffState{ - Namespace: "official", - Version: mustParseVersion("1.2.3-1"), - InstallationState: domain.TargetStatePresent, + Namespace: "official", + Version: &testCoreVersionHigh, + Absent: false, AdditionalMounts: []ecosystem.AdditionalMount{ { SourceType: ecosystem.DataSourceConfigMap, Name: "secret", Volume: "volume2", - Subfolder: "different_subfolder2", + Subfolder: testSubfolderStr2, }, }, }, NeededActions: []domain.Action{domain.ActionUpgrade}, }}}, - want: crd.StateDiff{ - ComponentDiffs: map[string]crd.ComponentDiff{}, + want: &crd.StateDiff{ DoguDiffs: map[string]crd.DoguDiff{ "ldap": { Actual: crd.DoguDiffState{ - Namespace: "official", - Version: "1.1.1-1", - InstallationState: "present", + Namespace: "official", + Version: &testCoreVersionLowStr, + Absent: false, AdditionalMounts: []crd.AdditionalMount{ { SourceType: crd.DataSourceConfigMap, Name: "configmap", Volume: "volume", - Subfolder: "different_subfolder", + Subfolder: &testSubfolderStr, }, }, }, Expected: crd.DoguDiffState{ - Namespace: "official", - Version: "1.2.3-1", - InstallationState: "present", + Namespace: "official", + Version: &testCoreVersionHighStr, + Absent: false, AdditionalMounts: []crd.AdditionalMount{ { SourceType: crd.DataSourceConfigMap, Name: "secret", Volume: "volume2", - Subfolder: "different_subfolder2", + Subfolder: &testSubfolderStr2, }, }, }, @@ -282,7 +247,7 @@ func TestConvertToDTO(t *testing.T) { t.Run(tt.name, func(t *testing.T) { if got := ConvertToStateDiffDTO(tt.domainModel); !reflect.DeepEqual(got, tt.want) { t.Errorf("ConvertToStateDiffDTO() = %v, want %v", got, tt.want) - assert.Equal(t, tt.want, got) + assert.Empty(t, googlecmp.Diff(tt.want, got)) } }) } @@ -297,518 +262,104 @@ func mustParseVersion(raw string) core.Version { return version } -func TestConvertToDomainModel(t *testing.T) { +func TestConvertToStateDiffDTO(t *testing.T) { + value1 := "1" + value123 := "123" tests := []struct { - name string - dto crd.StateDiff - want domain.StateDiff - wantErr assert.ErrorAssertionFunc + name string + model domain.StateDiff + want *crd.StateDiff }{ { - name: "fail to parse actual version of single dogu diff", - dto: crd.StateDiff{ - DoguDiffs: map[string]crd.DoguDiff{ - "ldap": { - Actual: crd.DoguDiffState{ - Namespace: "official", - Version: "a.b.c-d", - InstallationState: "present", - }, - Expected: crd.DoguDiffState{ - Namespace: "official", - Version: "1.2.3-4", - InstallationState: "present", - }, - NeededActions: []crd.DoguAction{"upgrade"}, - }, - }, - }, - want: domain.StateDiff{}, - wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { - return assert.ErrorContains(t, err, "failed to parse version \"a.b.c-d\"") && - assert.ErrorContains(t, err, "failed to convert dogu diff dto \"ldap\" to domain model") - }, - }, - { - name: "fail to parse expected version of single dogu diff", - dto: crd.StateDiff{ - DoguDiffs: map[string]crd.DoguDiff{ - "ldap": { - Actual: crd.DoguDiffState{ - Namespace: "official", - Version: "1.2.3-4", - InstallationState: "present", - }, - Expected: crd.DoguDiffState{ - Namespace: "official", - Version: "a.b.c-d", - InstallationState: "present", - }, - NeededActions: []crd.DoguAction{"downgrade"}, - }, - }, - }, - want: domain.StateDiff{}, - wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { - return assert.ErrorContains(t, err, "failed to parse version \"a.b.c-d\"") && - assert.ErrorContains(t, err, "failed to convert dogu diff dto \"ldap\" to domain model") - }, - }, - { - name: "fail to parse actual installation state of single dogu diff", - dto: crd.StateDiff{ - DoguDiffs: map[string]crd.DoguDiff{ - "ldap": { - Actual: crd.DoguDiffState{ - Namespace: "official", - Version: "1.2.3-4", - InstallationState: "invalid", - }, - Expected: crd.DoguDiffState{ - Namespace: "official", - Version: "2.3.4-5", - InstallationState: "present", - }, - NeededActions: []crd.DoguAction{"install"}, - }, - }, - }, - want: domain.StateDiff{}, - wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { - return assert.ErrorContains(t, err, "failed to parse installation state \"invalid\"") && - assert.ErrorContains(t, err, "failed to convert dogu diff dto \"ldap\" to domain model") - }, - }, - { - name: "fail to parse expected installation state of single dogu diff", - dto: crd.StateDiff{ - DoguDiffs: map[string]crd.DoguDiff{ - "ldap": { - Actual: crd.DoguDiffState{ - Namespace: "official", - Version: "1.2.3-4", - InstallationState: "present", - }, - Expected: crd.DoguDiffState{ - Namespace: "official", - Version: "2.3.4-5", - InstallationState: "invalid", - }, - NeededActions: []crd.DoguAction{"upgrade"}, - }, - }, - }, - want: domain.StateDiff{}, - wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { - return assert.ErrorContains(t, err, "failed to parse installation state \"invalid\"") && - assert.ErrorContains(t, err, "failed to convert dogu diff dto \"ldap\" to domain model") - }, - }, - { - name: "fail with multiple errors in single dogu diff", - dto: crd.StateDiff{ - DoguDiffs: map[string]crd.DoguDiff{ - "ldap": { - Actual: crd.DoguDiffState{ - Namespace: "official", - Version: "a.b.c-d", - InstallationState: "invalid", - }, - Expected: crd.DoguDiffState{ - Namespace: "official", - Version: "a.b.c-d", - InstallationState: "invalid", - }, - NeededActions: []crd.DoguAction{"none"}, - }, - }, - }, - want: domain.StateDiff{}, - wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { - return assert.ErrorContains(t, err, "failed to parse version \"a.b.c-d\"") && - assert.ErrorContains(t, err, "failed to parse installation state \"invalid\"") && - assert.ErrorContains(t, err, "failed to convert dogu diff dto \"ldap\" to domain model") - }, - }, - { - name: "fail for one of multiple dogu diffs", - dto: crd.StateDiff{ - DoguDiffs: map[string]crd.DoguDiff{ - "postfix": { - Actual: crd.DoguDiffState{ - Namespace: "official", - Version: "1.2.3-4", - InstallationState: "present", - }, - Expected: crd.DoguDiffState{ - Namespace: "official", - Version: "2.3.4-5", - InstallationState: "present", - }, - NeededActions: []crd.DoguAction{"upgrade"}, - }, - "ldap": { - Actual: crd.DoguDiffState{ - Namespace: "official", - Version: "1.2.3-4", - InstallationState: "present", - }, - Expected: crd.DoguDiffState{ - Namespace: "official", - Version: "2.3.4-5", - InstallationState: "invalid", - }, - NeededActions: []crd.DoguAction{"upgrade"}, - }, - }, - }, - want: domain.StateDiff{}, - wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { - return assert.ErrorContains(t, err, "failed to parse installation state \"invalid\"") && - assert.ErrorContains(t, err, "failed to convert dogu diff dto \"ldap\" to domain model") - }, - }, - { - name: "fail for multiple dogu diffs", - dto: crd.StateDiff{ - DoguDiffs: map[string]crd.DoguDiff{ - "postfix": { - Actual: crd.DoguDiffState{ - Namespace: "official", - Version: "a.b.c-d", - InstallationState: "present", - }, - Expected: crd.DoguDiffState{ - Namespace: "official", - Version: "2.3.4-5", - InstallationState: "present", - }, - NeededActions: []crd.DoguAction{"none"}, - }, - "ldap": { - Actual: crd.DoguDiffState{ - Namespace: "official", - Version: "1.2.3-4", - InstallationState: "present", - }, - Expected: crd.DoguDiffState{ - Namespace: "official", - Version: "2.3.4-5", - InstallationState: "invalid", - }, - NeededActions: []crd.DoguAction{"upgrade"}, - }, - }, - }, - want: domain.StateDiff{}, - wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { - return assert.ErrorContains(t, err, "failed to parse version \"a.b.c-d\"") && - assert.ErrorContains(t, err, "failed to parse installation state \"invalid\"") && - assert.ErrorContains(t, err, "failed to convert dogu diff dto \"ldap\" to domain model") && - assert.ErrorContains(t, err, "failed to convert dogu diff dto \"postfix\" to domain model") - }, - }, - { - name: "succeed for multiple dogu diffs", - dto: crd.StateDiff{ - DoguDiffs: map[string]crd.DoguDiff{ - "postfix": { - Actual: crd.DoguDiffState{ - Namespace: "official", - Version: "1.2.3-4", - InstallationState: "present", - }, - Expected: crd.DoguDiffState{ - Namespace: "official", - Version: "2.3.4-5", - InstallationState: "present", - }, - NeededActions: []crd.DoguAction{"upgrade"}, - }, - "ldap": { - Actual: crd.DoguDiffState{ - Namespace: "official", - Version: "1.2.3-4", - InstallationState: "present", - }, - Expected: crd.DoguDiffState{ - Namespace: "official", - InstallationState: "absent", - }, - NeededActions: []crd.DoguAction{"uninstall"}, - }, - }, - }, - want: domain.StateDiff{DoguDiffs: []domain.DoguDiff{ - { - DoguName: "ldap", - Actual: domain.DoguDiffState{ - Namespace: "official", - Version: mustParseVersion("1.2.3-4"), - InstallationState: domain.TargetStatePresent, - }, - Expected: domain.DoguDiffState{ - Namespace: "official", - InstallationState: domain.TargetStateAbsent, - }, NeededActions: []domain.Action{domain.ActionUninstall}, - }, - { - DoguName: "postfix", - Actual: domain.DoguDiffState{ - Namespace: "official", - Version: mustParseVersion("1.2.3-4"), - InstallationState: domain.TargetStatePresent, - }, - Expected: domain.DoguDiffState{ - Namespace: "official", - Version: mustParseVersion("2.3.4-5"), - InstallationState: domain.TargetStatePresent, - }, - NeededActions: []domain.Action{domain.ActionUpgrade}, - }, - }}, - wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { - return assert.NoError(t, err) - }, - }, - { - name: "succeed for multiple dogu config diffs", - dto: crd.StateDiff{ - DoguConfigDiffs: map[string]crd.CombinedDoguConfigDiff{ - "ldap": { - DoguConfigDiff: crd.DoguConfigDiff{}, - SensitiveDoguConfigDiff: crd.SensitiveDoguConfigDiff{}, - }, - "postfix": { - DoguConfigDiff: crd.DoguConfigDiff{}, - SensitiveDoguConfigDiff: crd.SensitiveDoguConfigDiff{}, - }, - }, - }, - want: domain.StateDiff{ + name: "normal dogu config", + model: domain.StateDiff{ + DoguDiffs: nil, DoguConfigDiffs: map[cescommons.SimpleName]domain.DoguConfigDiffs{ - "ldap": nil, - "postfix": nil, - }, - SensitiveDoguConfigDiffs: map[cescommons.SimpleName]domain.SensitiveDoguConfigDiffs{ - "ldap": nil, - "postfix": nil, - }, - }, - wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { - return assert.NoError(t, err) - }, - }, - { - name: "succeed for global config diffs", - dto: crd.StateDiff{ - GlobalConfigDiff: crd.GlobalConfigDiff{{ - Key: "fqdn", - Actual: crd.GlobalConfigValueState{ - Value: "ces1.example.com", - Exists: true, - }, - Expected: crd.GlobalConfigValueState{ - Value: "ces2.example.com", - Exists: true, - }, - NeededAction: "set", - }}, - }, - want: domain.StateDiff{ - GlobalConfigDiffs: []domain.GlobalConfigEntryDiff{{ - Key: "fqdn", - Actual: domain.GlobalConfigValueState{ - Value: "ces1.example.com", - Exists: true, - }, - Expected: domain.GlobalConfigValueState{ - Value: "ces2.example.com", - Exists: true, - }, - NeededAction: domain.ConfigActionSet, - }}, - }, - wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { - return assert.NoError(t, err) - }, - }, - { - name: "succeed for multiple component diffs", - dto: crd.StateDiff{ - ComponentDiffs: map[string]crd.ComponentDiff{ - testComponentName: { - Actual: crd.ComponentDiffState{Version: testVersionLowRaw, InstallationState: "present"}, - Expected: crd.ComponentDiffState{Version: testVersionHighRaw, InstallationState: "present"}, - NeededActions: []crd.ComponentAction{"upgrade", "component namespace switch"}, - }, - "my-component-2": { - Actual: crd.ComponentDiffState{Version: testVersionHighRaw, InstallationState: "present"}, - Expected: crd.ComponentDiffState{InstallationState: "absent"}, - NeededActions: []crd.ComponentAction{"uninstall"}, - }, - }, - }, - want: domain.StateDiff{ComponentDiffs: []domain.ComponentDiff{ - { - Name: testComponentName, - Actual: domain.ComponentDiffState{Version: testVersionLow, InstallationState: domain.TargetStatePresent}, - Expected: domain.ComponentDiffState{Version: testVersionHigh, InstallationState: domain.TargetStatePresent}, - NeededActions: []domain.Action{domain.ActionUpgrade, domain.ActionSwitchComponentNamespace}, - }, - { - Name: "my-component-2", - Actual: domain.ComponentDiffState{Version: testVersionHigh, InstallationState: domain.TargetStatePresent}, - Expected: domain.ComponentDiffState{InstallationState: domain.TargetStateAbsent}, - NeededActions: []domain.Action{domain.ActionUninstall}, - }, - }}, - wantErr: assert.NoError, - }, - { - name: "fail for multiple component diffs", - dto: crd.StateDiff{ - ComponentDiffs: map[string]crd.ComponentDiff{ - testComponentName: { - Actual: crd.ComponentDiffState{ - Version: "a.b.c-d", - InstallationState: "present", - }, - Expected: crd.ComponentDiffState{ - Version: "2.3.4-5", - InstallationState: "present", - }, - NeededActions: []crd.ComponentAction{"none"}, - }, - "my-component-2": { - Actual: crd.ComponentDiffState{ - Version: "1.2.3-4", - InstallationState: "present", - }, - Expected: crd.ComponentDiffState{ - Version: "2.3.4-5", - InstallationState: "invalid", + testDogu: { + { + Key: testDoguKey1, + Actual: domain.DoguConfigValueState{ + Value: &value1, + Exists: true, + }, + Expected: domain.DoguConfigValueState{ + Value: &value123, + Exists: true, + }, + NeededAction: domain.ConfigActionSet, }, - NeededActions: []crd.ComponentAction{"upgrade"}, }, - }, - }, - want: domain.StateDiff{}, - wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { - return assert.ErrorContains(t, err, "failed to parse actual version \"a.b.c-d\"") && - assert.ErrorContains(t, err, "failed to parse expected installation state \"invalid\"") && - assert.ErrorContains(t, err, "failed to convert component diff dto \"my-component\" to domain model") && - assert.ErrorContains(t, err, "failed to convert component diff dto \"my-component-2\" to domain model") - }, - }, - - { - name: "should convert additional mounts", - dto: crd.StateDiff{ - DoguDiffs: map[string]crd.DoguDiff{ - "ldap": { - Actual: crd.DoguDiffState{ - Namespace: "official", - InstallationState: "present", - AdditionalMounts: []crd.AdditionalMount{ - { - SourceType: crd.DataSourceConfigMap, - Name: "config", - Volume: "volume", - Subfolder: "subfolder", - }, + testDogu2: { + { + Key: testDoguKey2, + Actual: domain.DoguConfigValueState{ + Value: nil, + Exists: false, }, - }, - Expected: crd.DoguDiffState{ - Namespace: "official", - InstallationState: "present", - AdditionalMounts: []crd.AdditionalMount{ - { - SourceType: crd.DataSourceConfigMap, - Name: "config-different", - Volume: "volume", - Subfolder: "subfolder", - }, + Expected: domain.DoguConfigValueState{ + Value: &value123, + Exists: true, }, + NeededAction: domain.ConfigActionSet, }, - NeededActions: []crd.DoguAction{"update additional mounts"}, }, }, + GlobalConfigDiffs: nil, }, - want: domain.StateDiff{DoguDiffs: []domain.DoguDiff{ - { - DoguName: "ldap", - Actual: domain.DoguDiffState{ - Namespace: "official", - InstallationState: domain.TargetStatePresent, - AdditionalMounts: []ecosystem.AdditionalMount{ + want: &crd.StateDiff{ + DoguDiffs: map[string]crd.DoguDiff{}, + DoguConfigDiffs: map[string]crd.CombinedDoguConfigDiff{ + testDogu.String(): { + DoguConfigDiff: crd.DoguConfigDiff{ { - SourceType: ecosystem.DataSourceConfigMap, - Name: "config", - Volume: "volume", - Subfolder: "subfolder", + Key: testDoguKey1.Key.String(), + Actual: crd.ConfigValueState{ + Value: &value1, + Exists: true, + }, + Expected: crd.ConfigValueState{ + Value: &value123, + Exists: true, + }, + NeededAction: crd.ConfigAction("set"), }, }, }, - Expected: domain.DoguDiffState{ - Namespace: "official", - InstallationState: domain.TargetStatePresent, - AdditionalMounts: []ecosystem.AdditionalMount{ + testDogu2.String(): { + DoguConfigDiff: crd.DoguConfigDiff{ { - SourceType: ecosystem.DataSourceConfigMap, - Name: "config-different", - Volume: "volume", - Subfolder: "subfolder", + Key: testDoguKey2.Key.String(), + Actual: crd.ConfigValueState{ + Value: nil, + Exists: false, + }, + Expected: crd.ConfigValueState{ + Value: &value123, + Exists: true, + }, + NeededAction: crd.ConfigAction("set"), }, }, }, - NeededActions: []domain.Action{domain.ActionUpdateAdditionalMounts}, }, - }}, - wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { - return assert.NoError(t, err) + GlobalConfigDiff: nil, }, }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := ConvertToStateDiffDomain(tt.dto) - tt.wantErr(t, err) - // sort to avoid flaky tests - slices.SortFunc(got.DoguDiffs, func(a, b domain.DoguDiff) int { - return cmp.Compare(a.DoguName, b.DoguName) - }) - slices.SortFunc(got.ComponentDiffs, func(a, b domain.ComponentDiff) int { - return cmp.Compare(a.Name, b.Name) - }) - assert.Equalf(t, tt.want, got, "ConvertToStateDiffDomain(%v)", tt.dto) - }) - } -} - -func TestConvertToStateDiffDTO(t *testing.T) { - - tests := []struct { - name string - model domain.StateDiff - want crd.StateDiff - }{ { - name: "ok", + name: "censor sensitive config", model: domain.StateDiff{ DoguDiffs: nil, - ComponentDiffs: nil, DoguConfigDiffs: map[cescommons.SimpleName]domain.DoguConfigDiffs{}, SensitiveDoguConfigDiffs: map[cescommons.SimpleName]domain.SensitiveDoguConfigDiffs{ testDogu: { { Key: testDoguKey1, Actual: domain.DoguConfigValueState{ - Value: "1", + Value: &value1, Exists: true, }, Expected: domain.DoguConfigValueState{ - Value: "123", + Value: &value123, Exists: true, }, NeededAction: domain.ConfigActionSet, @@ -818,11 +369,11 @@ func TestConvertToStateDiffDTO(t *testing.T) { { Key: testDoguKey2, Actual: domain.DoguConfigValueState{ - Value: "", + Value: nil, Exists: false, }, Expected: domain.DoguConfigValueState{ - Value: "123", + Value: &value123, Exists: true, }, NeededAction: domain.ConfigActionSet, @@ -831,21 +382,19 @@ func TestConvertToStateDiffDTO(t *testing.T) { }, GlobalConfigDiffs: nil, }, - want: crd.StateDiff{ - DoguDiffs: map[string]crd.DoguDiff{}, - ComponentDiffs: map[string]crd.ComponentDiff{}, + want: &crd.StateDiff{ + DoguDiffs: map[string]crd.DoguDiff{}, DoguConfigDiffs: map[string]crd.CombinedDoguConfigDiff{ testDogu.String(): { - DoguConfigDiff: crd.DoguConfigDiff(nil), SensitiveDoguConfigDiff: crd.SensitiveDoguConfigDiff{ - crd.DoguConfigEntryDiff{ + { Key: testDoguKey1.Key.String(), - Actual: crd.DoguConfigValueState{ - Value: "1", + Actual: crd.ConfigValueState{ + Value: nil, Exists: true, }, - Expected: crd.DoguConfigValueState{ - Value: "123", + Expected: crd.ConfigValueState{ + Value: nil, Exists: true, }, NeededAction: crd.ConfigAction("set"), @@ -854,14 +403,14 @@ func TestConvertToStateDiffDTO(t *testing.T) { }, testDogu2.String(): { SensitiveDoguConfigDiff: crd.SensitiveDoguConfigDiff{ - crd.DoguConfigEntryDiff{ + { Key: testDoguKey2.Key.String(), - Actual: crd.DoguConfigValueState{ - Value: "", + Actual: crd.ConfigValueState{ + Value: nil, Exists: false, }, - Expected: crd.DoguConfigValueState{ - Value: "123", + Expected: crd.ConfigValueState{ + Value: nil, Exists: true, }, NeededAction: crd.ConfigAction("set"), diff --git a/pkg/adapter/kubernetes/blueprintcr/v2/serializer/targetState.go b/pkg/adapter/kubernetes/blueprintcr/v2/serializer/targetState.go deleted file mode 100644 index f068a42e..00000000 --- a/pkg/adapter/kubernetes/blueprintcr/v2/serializer/targetState.go +++ /dev/null @@ -1,42 +0,0 @@ -package serializer - -import ( - "fmt" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" -) - -// ToDomainTargetState maps a string to a domain.TargetState or returns an error if this is not possible. -func ToDomainTargetState(absent bool) domain.TargetState { - if absent { - return domain.TargetStateAbsent - } else { - return domain.TargetStatePresent - } -} - -// ToSerializerAbsentState maps a domain.TargetState to the absent state of dogus in the CR. -// If the state is not present, it will be interpreted as absent -func ToSerializerAbsentState(domainState domain.TargetState) bool { - return domainState != domain.TargetStatePresent -} - -//FIXME: remove old TargetState types, we need to change the domain.StateDiff for that -// we do this in #54968 if these changes on the blueprint CRD got merged, so we do not have to revert everything - -// ToID provides common mappings from strings to domain.TargetState, e.g. for dogus. -var ToID = map[string]domain.TargetState{ - "": domain.TargetStatePresent, - "present": domain.TargetStatePresent, - "absent": domain.TargetStateAbsent, -} - -// ToOldDomainTargetState maps a string to a domain.TargetState or returns an error if this is not possible. -func ToOldDomainTargetState(stateString string) (domain.TargetState, error) { - // Note that if the string is not found then it will be set to the zero value, which is 'Created'. - id := ToID[stateString] - var err error - if id == domain.TargetStatePresent && stateString != "present" && stateString != "" { - err = fmt.Errorf("unknown target state %q", stateString) - } - return id, err -} diff --git a/pkg/adapter/kubernetes/blueprintcr/v2/serializer/targetState_test.go b/pkg/adapter/kubernetes/blueprintcr/v2/serializer/targetState_test.go deleted file mode 100644 index 447ac053..00000000 --- a/pkg/adapter/kubernetes/blueprintcr/v2/serializer/targetState_test.go +++ /dev/null @@ -1,18 +0,0 @@ -package serializer - -import ( - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" - "github.com/stretchr/testify/assert" - "testing" -) - -func Test_toDomainTargetState(t *testing.T) { - assert.Equal(t, domain.TargetState(domain.TargetStatePresent), ToDomainTargetState(false)) - assert.Equal(t, domain.TargetState(domain.TargetStateAbsent), ToDomainTargetState(true)) -} - -func Test_ToSerializerAbsentState(t *testing.T) { - assert.False(t, ToSerializerAbsentState(domain.TargetStatePresent)) - assert.True(t, ToSerializerAbsentState(domain.TargetState(-1))) - assert.True(t, ToSerializerAbsentState(domain.TargetStateAbsent)) -} diff --git a/pkg/adapter/kubernetes/componentcr/componentInstallationRepo.go b/pkg/adapter/kubernetes/componentcr/componentInstallationRepo.go deleted file mode 100644 index ff3ffbdf..00000000 --- a/pkg/adapter/kubernetes/componentcr/componentInstallationRepo.go +++ /dev/null @@ -1,109 +0,0 @@ -package componentcr - -import ( - "context" - "fmt" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/common" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/log" - - k8sErrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domainservice" - compCli "github.com/cloudogu/k8s-component-operator/pkg/api/ecosystem" -) - -const ( - ComponentNameLabelKey = "k8s.cloudogu.com/component.name" - ComponentVersionLabelKey = "k8s.cloudogu.com/component.version" - componentInstallationRepoContextKey = "componentInstallationRepoContext" -) - -type componentInstallationRepoContext struct { - resourceVersion string -} - -type componentInstallationRepo struct { - componentClient compCli.ComponentInterface -} - -// NewComponentInstallationRepo creates a new component repo adapter. -func NewComponentInstallationRepo(componentClient compCli.ComponentInterface) domainservice.ComponentInstallationRepository { - return &componentInstallationRepo{componentClient: componentClient} -} - -// GetByName fetches a named component resource and returns it as ecosystem.ComponentInstallation. -func (repo *componentInstallationRepo) GetByName(ctx context.Context, componentName common.SimpleComponentName) (*ecosystem.ComponentInstallation, error) { - cr, err := repo.componentClient.Get(ctx, string(componentName), metav1.GetOptions{}) - if err != nil { - if k8sErrors.IsNotFound(err) { - return nil, &domainservice.NotFoundError{ - WrappedError: err, - Message: fmt.Sprintf("cannot read component CR %q as it does not exist", componentName), - } - } - return nil, domainservice.NewInternalError(err, "error while reading component CR %q", componentName) - } - - return parseComponentCR(cr) -} - -// GetAll fetches all installed component resources and returns them as list of ecosystem.ComponentInstallation. -func (repo *componentInstallationRepo) GetAll(ctx context.Context) (map[common.SimpleComponentName]*ecosystem.ComponentInstallation, error) { - list, err := repo.componentClient.List(ctx, metav1.ListOptions{}) - if err != nil { - return nil, err - } - - componentInstallations := make(map[common.SimpleComponentName]*ecosystem.ComponentInstallation, len(list.Items)) - for _, componentCr := range list.Items { - cr, err := parseComponentCR(&componentCr) - if err != nil { - return nil, domainservice.NewInternalError(err, "failed to parse component CR %#v", componentCr) - } - componentInstallations[common.SimpleComponentName(componentCr.Name)] = cr - } - - return componentInstallations, nil -} - -func (repo *componentInstallationRepo) Update(ctx context.Context, component *ecosystem.ComponentInstallation) error { - logger := log.FromContext(ctx).WithName("doguInstallationRepo.Update") - patch, err := toComponentCRPatchBytes(component) - if err != nil { - return domainservice.NewInternalError(err, "failed to get patch bytes from component %q", component.Name.SimpleName) - } - - logger.Info("patch component CR", "doguName", component.Name.SimpleName, "doguPatch", string(patch)) - - _, err = repo.componentClient.Patch(ctx, string(component.Name.SimpleName), types.MergePatchType, patch, metav1.PatchOptions{}) - if err != nil { - return domainservice.NewInternalError(err, "failed to patch component %q", component.Name.SimpleName) - } - - return nil -} - -func (repo *componentInstallationRepo) Delete(ctx context.Context, componentName common.SimpleComponentName) error { - err := repo.componentClient.Delete(ctx, string(componentName), metav1.DeleteOptions{}) - if err != nil { - return domainservice.NewInternalError(err, "failed to delete component CR %q", componentName) - } - - return nil -} - -func (repo *componentInstallationRepo) Create(ctx context.Context, component *ecosystem.ComponentInstallation) error { - cr, err := toComponentCR(component) - if err != nil { - return domainservice.NewInternalError(err, "failed to convert component installation %q", component.Name) - } - _, err = repo.componentClient.Create(ctx, cr, metav1.CreateOptions{}) - if err != nil { - return domainservice.NewInternalError(err, "failed to create component CR %q", component.Name.SimpleName) - } - - return nil -} diff --git a/pkg/adapter/kubernetes/componentcr/componentInstallationRepo_test.go b/pkg/adapter/kubernetes/componentcr/componentInstallationRepo_test.go deleted file mode 100644 index 7bfe53df..00000000 --- a/pkg/adapter/kubernetes/componentcr/componentInstallationRepo_test.go +++ /dev/null @@ -1,422 +0,0 @@ -package componentcr - -import ( - "context" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/common" - "k8s.io/apimachinery/pkg/types" - "testing" - - "github.com/Masterminds/semver/v3" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" - - compV1 "github.com/cloudogu/k8s-component-operator/pkg/api/v1" - - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domainservice" -) - -var testCtx = context.Background() - -const ( - testComponentNameRaw = "my-component" - testNamespace = "k8s" -) - -var testComponentName = common.QualifiedComponentName{ - Namespace: testNamespace, - SimpleName: testComponentNameRaw, -} - -func Test_componentInstallationRepo_GetAll(t *testing.T) { - t.Run("should return error when k8s client fails generically", func(t *testing.T) { - // given - mockRepo := newMockComponentRepo(t) - mockRepo.EXPECT().List(testCtx, mock.Anything).Return(nil, assert.AnError) - sut := componentInstallationRepo{componentClient: mockRepo} - - // when - _, err := sut.GetAll(testCtx) - - // then - require.Error(t, err) - assert.ErrorIs(t, err, assert.AnError) - }) - t.Run("should return InternalError when resource parsing fails", func(t *testing.T) { - // given - mockRepo := newMockComponentRepo(t) - sut := componentInstallationRepo{componentClient: mockRepo} - listWithErroneousElement := &compV1.ComponentList{Items: []compV1.Component{{ - Spec: compV1.ComponentSpec{ - Name: string(testComponentName.SimpleName), - Namespace: testNamespace, - Version: "a-b.c:d@1.2@parse-fail-here", - }, - }}} - mockRepo.EXPECT().List(testCtx, mock.Anything).Return(listWithErroneousElement, nil) - - // when - _, err := sut.GetAll(testCtx) - - // then - require.Error(t, err) - assert.IsType(t, err, &domainservice.InternalError{}) - assert.ErrorContains(t, err, "failed to parse component CR") - - }) - t.Run("should return all existing blueprint resources", func(t *testing.T) { - // given - mockRepo := newMockComponentRepo(t) - sut := componentInstallationRepo{componentClient: mockRepo} - list := &compV1.ComponentList{Items: []compV1.Component{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: string(testComponentName.SimpleName), - ResourceVersion: "42", - }, - Spec: compV1.ComponentSpec{ - Name: string(testComponentName.SimpleName), - Namespace: string(testComponentName.Namespace), - Version: "1.2.3-4", - }, - Status: compV1.ComponentStatus{ - Status: compV1.ComponentStatusInstalled, - Health: compV1.PendingHealthStatus, - }, - }, - }} - mockRepo.EXPECT().List(testCtx, mock.Anything).Return(list, nil) - - // when - actual, err := sut.GetAll(testCtx) - - // then - require.NoError(t, err) - - expected := map[common.SimpleComponentName]*ecosystem.ComponentInstallation{} - version, _ := semver.NewVersion("1.2.3-4") - expected[testComponentName.SimpleName] = &ecosystem.ComponentInstallation{ - Name: testComponentName, - ExpectedVersion: version, - Status: "installed", - Health: "", - PersistenceContext: nil, - } - assert.Equal(t, expected[testComponentName.SimpleName].Name, actual[testComponentName.SimpleName].Name) - assert.Equal(t, expected[testComponentName.SimpleName].Status, actual[testComponentName.SimpleName].Status) - assert.Equal(t, expected[testComponentName.SimpleName].ExpectedVersion, actual[testComponentName.SimpleName].ExpectedVersion) - assert.Equal(t, expected[testComponentName.SimpleName].Health, actual[testComponentName.SimpleName].Health) - // map pointers are hard to compare, test each field individually - assert.Equal(t, - map[string]any{componentInstallationRepoContextKey: componentInstallationRepoContext{resourceVersion: "42"}}, - actual[testComponentName.SimpleName].PersistenceContext) - }) -} - -func Test_componentInstallationRepo_GetByName(t *testing.T) { - t.Run("should return error when k8s client fails generically", func(t *testing.T) { - // given - mockRepo := newMockComponentRepo(t) - mockRepo.EXPECT().Get(testCtx, string(testComponentName.SimpleName), metav1.GetOptions{}).Return(nil, assert.AnError) - sut := componentInstallationRepo{componentClient: mockRepo} - - // when - _, err := sut.GetByName(testCtx, testComponentName.SimpleName) - - // then - require.Error(t, err) - assert.ErrorIs(t, err, assert.AnError) - }) - t.Run("should return InternalError when resource parsing fails", func(t *testing.T) { - // given - mockRepo := newMockComponentRepo(t) - sut := componentInstallationRepo{componentClient: mockRepo} - erroneousComponent := &compV1.Component{ - Spec: compV1.ComponentSpec{ - Name: string(testComponentName.SimpleName), - Namespace: testNamespace, - Version: "a-b.c:d@1.2@parse-fail-here", - }, - } - mockRepo.EXPECT().Get(testCtx, string(testComponentName.SimpleName), metav1.GetOptions{}).Return(erroneousComponent, nil) - - // when - _, err := sut.GetByName(testCtx, testComponentName.SimpleName) - - // then - require.Error(t, err) - assert.IsType(t, err, &domainservice.InternalError{}) - assert.ErrorContains(t, err, "cannot load component CR as it cannot be parsed correctly") - }) - t.Run("should return InternalError when resource is nil", func(t *testing.T) { - // given - mockRepo := newMockComponentRepo(t) - sut := componentInstallationRepo{componentClient: mockRepo} - mockRepo.EXPECT().Get(testCtx, string(testComponentName.SimpleName), metav1.GetOptions{}).Return(nil, nil) - - // when - _, err := sut.GetByName(testCtx, testComponentName.SimpleName) - - // then - require.Error(t, err) - assert.IsType(t, err, &domainservice.InternalError{}) - assert.ErrorContains(t, err, "cannot parse component CR as it is nil") - }) - t.Run("should return NotFoundError when resource does not exist", func(t *testing.T) { - // given - mockRepo := newMockComponentRepo(t) - sut := componentInstallationRepo{componentClient: mockRepo} - errNotFound := errors.NewNotFound( - schema.GroupResource{Group: compV1.GroupVersion.Group, Resource: "component"}, - string(testComponentName.SimpleName)) - mockRepo.EXPECT().Get(testCtx, string(testComponentName.SimpleName), metav1.GetOptions{}).Return(nil, errNotFound) - - // when - _, err := sut.GetByName(testCtx, testComponentName.SimpleName) - - // then - require.Error(t, err) - assert.IsType(t, err, &domainservice.NotFoundError{}) - assert.ErrorContains(t, err, `cannot read component CR "my-component" as it does not exist`) - }) - t.Run("should return all existing blueprint resources", func(t *testing.T) { - // given - mockRepo := newMockComponentRepo(t) - sut := componentInstallationRepo{componentClient: mockRepo} - result := &compV1.Component{ - ObjectMeta: metav1.ObjectMeta{ - Name: string(testComponentName.SimpleName), - ResourceVersion: "42", - }, - Spec: compV1.ComponentSpec{ - Name: string(testComponentName.SimpleName), - Namespace: testNamespace, - Version: "1.2.3-4", - }, - Status: compV1.ComponentStatus{ - Status: compV1.ComponentStatusInstalled, - Health: compV1.PendingHealthStatus, - }, - } - mockRepo.EXPECT().Get(testCtx, string(testComponentName.SimpleName), metav1.GetOptions{}).Return(result, nil) - - // when - actual, err := sut.GetByName(testCtx, testComponentName.SimpleName) - - // then - require.NoError(t, err) - - version, _ := semver.NewVersion("1.2.3-4") - expected := ecosystem.ComponentInstallation{ - Name: testComponentName, - ExpectedVersion: version, - Status: "installed", - Health: "", - PersistenceContext: nil, - } - assert.Equal(t, expected.Name, actual.Name) - assert.Equal(t, expected.Name, testComponentName) - assert.Equal(t, expected.Status, actual.Status) - assert.Equal(t, expected.ExpectedVersion, actual.ExpectedVersion) - assert.Equal(t, expected.Health, actual.Health) - // map pointers are hard to compare, test each field individually - assert.Equal(t, - map[string]any{componentInstallationRepoContextKey: componentInstallationRepoContext{resourceVersion: "42"}}, actual.PersistenceContext) - }) -} - -func TestNewComponentInstallationRepo(t *testing.T) { - t.Run("should return proper repo interface implementation", func(t *testing.T) { - // given - mockRepo := newMockComponentRepo(t) - - // when - actual := NewComponentInstallationRepo(mockRepo) - - // then - assert.Implements(t, (*domainservice.ComponentInstallationRepository)(nil), actual) - }) -} - -func Test_componentInstallationRepo_Update(t *testing.T) { - t.Run("should patch cr on update", func(t *testing.T) { - // given - componentClientMock := newMockComponentRepo(t) - sut := componentInstallationRepo{ - componentClient: componentClientMock, - } - componentInstallation := &ecosystem.ComponentInstallation{ - Name: testComponentName, - ExpectedVersion: testVersion1, - DeployConfig: map[string]interface{}{ - "deployNamespace": "longhorn-system", - "overwriteConfig": map[string]interface{}{"key": "value"}, - }, - } - patch := []byte("{\"spec\":{\"namespace\":\"k8s\",\"name\":\"my-component\",\"version\":\"1.0.0-1\",\"deployNamespace\":\"longhorn-system\",\"valuesYamlOverwrite\":\"key: value\\n\"}}") - componentClientMock.EXPECT().Patch(testCtx, string(testComponentName.SimpleName), types.MergePatchType, patch, metav1.PatchOptions{}).Return(nil, nil) - - // when - err := sut.Update(testCtx, componentInstallation) - - // then - require.NoError(t, err) - }) - - t.Run("should return error on patch error", func(t *testing.T) { - // given - componentClientMock := newMockComponentRepo(t) - sut := componentInstallationRepo{ - componentClient: componentClientMock, - } - componentInstallation := &ecosystem.ComponentInstallation{ - Name: testComponentName, - ExpectedVersion: testVersion1, - } - patch := []byte("{\"spec\":{\"namespace\":\"k8s\",\"name\":\"my-component\",\"version\":\"1.0.0-1\",\"deployNamespace\":null,\"valuesYamlOverwrite\":null}}") - componentClientMock.EXPECT().Patch(testCtx, string(testComponentName.SimpleName), types.MergePatchType, patch, metav1.PatchOptions{}).Return(nil, assert.AnError) - - // when - err := sut.Update(testCtx, componentInstallation) - - // then - require.Error(t, err) - assert.ErrorContains(t, err, "failed to patch component \"my-component\"") - assert.IsType(t, err, &domainservice.InternalError{}) - }) -} - -func Test_componentInstallationRepo_Delete(t *testing.T) { - t.Run("should delete cr on delete", func(t *testing.T) { - // given - componentClientMock := newMockComponentRepo(t) - sut := componentInstallationRepo{ - componentClient: componentClientMock, - } - - componentClientMock.EXPECT().Delete(testCtx, string(testComponentName.SimpleName), metav1.DeleteOptions{}).Return(nil) - - // when - err := sut.Delete(testCtx, testComponentName.SimpleName) - - // then - require.NoError(t, err) - }) - - t.Run("should return error on delete error", func(t *testing.T) { - // given - componentClientMock := newMockComponentRepo(t) - sut := componentInstallationRepo{ - componentClient: componentClientMock, - } - - componentClientMock.EXPECT().Delete(testCtx, string(testComponentName.SimpleName), metav1.DeleteOptions{}).Return(assert.AnError) - - // when - err := sut.Delete(testCtx, testComponentName.SimpleName) - - // then - require.Error(t, err) - assert.ErrorContains(t, err, "failed to delete component CR \"my-component\"") - assert.IsType(t, err, &domainservice.InternalError{}) - }) -} - -func Test_componentInstallationRepo_Create(t *testing.T) { - t.Run("should create cr on create", func(t *testing.T) { - // given - componentClientMock := newMockComponentRepo(t) - sut := componentInstallationRepo{ - componentClient: componentClientMock, - } - componentInstallation := &ecosystem.ComponentInstallation{ - Name: testComponentName, - ExpectedVersion: testVersion1, - } - expectedCR := &compV1.Component{ - ObjectMeta: metav1.ObjectMeta{ - Name: string(componentInstallation.Name.SimpleName), - Labels: map[string]string{ - ComponentNameLabelKey: string(componentInstallation.Name.SimpleName), - ComponentVersionLabelKey: componentInstallation.ExpectedVersion.String(), - }, - }, - Spec: compV1.ComponentSpec{ - Namespace: string(componentInstallation.Name.Namespace), - Name: string(componentInstallation.Name.SimpleName), - Version: componentInstallation.ExpectedVersion.String(), - }, - } - - componentClientMock.EXPECT().Create(testCtx, expectedCR, metav1.CreateOptions{}).Return(nil, nil) - - // when - err := sut.Create(testCtx, componentInstallation) - - // then - require.NoError(t, err) - }) - - t.Run("should return error on convert component installation error", func(t *testing.T) { - // given - componentClientMock := newMockComponentRepo(t) - sut := componentInstallationRepo{ - componentClient: componentClientMock, - } - componentInstallation := &ecosystem.ComponentInstallation{ - Name: testComponentName, - ExpectedVersion: testVersion1, - DeployConfig: map[string]interface{}{ - "deployNamespace": map[string]string{"no": "string"}, - }, - } - - // when - err := sut.Create(testCtx, componentInstallation) - - // then - require.Error(t, err) - assert.ErrorContains(t, err, "failed to convert component installation \"k8s/my-component\"") - assert.IsType(t, err, &domainservice.InternalError{}) - }) - - t.Run("should return error on patch error", func(t *testing.T) { - // given - componentClientMock := newMockComponentRepo(t) - sut := componentInstallationRepo{ - componentClient: componentClientMock, - } - componentInstallation := &ecosystem.ComponentInstallation{ - Name: testComponentName, - ExpectedVersion: testVersion1, - } - expectedCR := &compV1.Component{ - ObjectMeta: metav1.ObjectMeta{ - Name: string(componentInstallation.Name.SimpleName), - Labels: map[string]string{ - ComponentNameLabelKey: string(componentInstallation.Name.SimpleName), - ComponentVersionLabelKey: componentInstallation.ExpectedVersion.String(), - }, - }, - Spec: compV1.ComponentSpec{ - Namespace: string(componentInstallation.Name.Namespace), - Name: string(componentInstallation.Name.SimpleName), - Version: componentInstallation.ExpectedVersion.String(), - }, - } - - componentClientMock.EXPECT().Create(testCtx, expectedCR, metav1.CreateOptions{}).Return(nil, assert.AnError) - - // when - err := sut.Create(testCtx, componentInstallation) - - // then - require.Error(t, err) - assert.ErrorContains(t, err, "failed to create component CR \"my-component\"") - assert.IsType(t, err, &domainservice.InternalError{}) - }) -} diff --git a/pkg/adapter/kubernetes/componentcr/componentSerializer.go b/pkg/adapter/kubernetes/componentcr/componentSerializer.go deleted file mode 100644 index af14aaea..00000000 --- a/pkg/adapter/kubernetes/componentcr/componentSerializer.go +++ /dev/null @@ -1,191 +0,0 @@ -package componentcr - -import ( - "encoding/json" - "fmt" - "github.com/Masterminds/semver/v3" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/common" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domainservice" - compV1 "github.com/cloudogu/k8s-component-operator/pkg/api/v1" - "gopkg.in/yaml.v3" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - k8syaml "k8s.io/apimachinery/pkg/util/yaml" -) - -const ( - deployConfigKeyDeployNamespace = "deployNamespace" - deployConfigKeyOverwriteConfig = "overwriteConfig" -) - -func parseComponentCR(cr *compV1.Component) (*ecosystem.ComponentInstallation, error) { - if cr == nil { - return nil, domainservice.NewInternalError(nil, "cannot parse component CR as it is nil") - } - - expectedVersion, err := semver.NewVersion(cr.Spec.Version) - if err != nil { - return nil, domainservice.NewInternalError(err, "cannot load component CR as it cannot be parsed correctly") - } - - // ignore error as the actual version could be not set and a nil pointer as the version is exactly what we want then - actualVersion, _ := semver.NewVersion(cr.Status.InstalledVersion) - - persistenceContext := make(map[string]interface{}, 1) - persistenceContext[componentInstallationRepoContextKey] = componentInstallationRepoContext{ - resourceVersion: cr.GetResourceVersion(), - } - - name, err := common.NewQualifiedComponentName(common.ComponentNamespace(cr.Spec.Namespace), common.SimpleComponentName(cr.Name)) - if err != nil { - return nil, err - } - - componentConfig, err := parseDeployConfig(cr) - if err != nil { - return nil, err - } - - return &ecosystem.ComponentInstallation{ - Name: name, - ExpectedVersion: expectedVersion, - ActualVersion: actualVersion, - Status: cr.Status.Status, - Health: ecosystem.HealthStatus(cr.Status.Health), - PersistenceContext: persistenceContext, - DeployConfig: componentConfig, - }, nil -} - -func parseDeployConfig(cr *compV1.Component) (ecosystem.DeployConfig, error) { - componentConfig := ecosystem.DeployConfig{} - if cr.Spec.DeployNamespace != "" { - componentConfig[deployConfigKeyDeployNamespace] = cr.Spec.DeployNamespace - } - - if cr.Spec.ValuesYamlOverwrite != "" { - valuesYamlOverwrite := map[string]interface{}{} - // We need to use k8syaml here because goyaml unmarshals to map[interface{}]interface {} which is not supported setting in a k8s resource. - err := k8syaml.Unmarshal([]byte(cr.Spec.ValuesYamlOverwrite), &valuesYamlOverwrite) - if err != nil { - return nil, domainservice.NewInternalError(err, "failed to unmarshal values yaml overwrite %q", cr.Spec.ValuesYamlOverwrite) - } - componentConfig[deployConfigKeyOverwriteConfig] = valuesYamlOverwrite - } - - return componentConfig, nil -} - -func toComponentCR(componentInstallation *ecosystem.ComponentInstallation) (*compV1.Component, error) { - deployNamespace, err := toDeployNamespace(componentInstallation.DeployConfig) - if err != nil { - return nil, err - } - - valuesYamlOverwrite, err := toValuesYamlOverwrite(componentInstallation.DeployConfig) - if err != nil { - return nil, err - } - - spec := compV1.ComponentSpec{ - Namespace: string(componentInstallation.Name.Namespace), - Name: string(componentInstallation.Name.SimpleName), - Version: componentInstallation.ExpectedVersion.String(), - } - if deployNamespace != "" { - spec.DeployNamespace = deployNamespace - } - if valuesYamlOverwrite != "" { - spec.ValuesYamlOverwrite = valuesYamlOverwrite - } - - return &compV1.Component{ - ObjectMeta: metav1.ObjectMeta{ - Name: string(componentInstallation.Name.SimpleName), - Labels: map[string]string{ - ComponentNameLabelKey: string(componentInstallation.Name.SimpleName), - ComponentVersionLabelKey: componentInstallation.ExpectedVersion.String(), - }, - }, - Spec: spec, - }, nil -} - -func toDeployNamespace(deployConfig ecosystem.DeployConfig) (string, error) { - deployNamespace, found := deployConfig[deployConfigKeyDeployNamespace] - if !found { - return "", nil - } - deployNamespaceStr, ok := deployNamespace.(string) - if !ok { - return "", fmt.Errorf("deployNamespace is not type of string") - } - - return deployNamespaceStr, nil -} - -func toValuesYamlOverwrite(deployConfig ecosystem.DeployConfig) (string, error) { - in, found := deployConfig[deployConfigKeyOverwriteConfig] - if !found { - return "", nil - } - valuesYamlOverwriteBytes, err := yaml.Marshal(in) - if err != nil { - return "", fmt.Errorf("failed to marshal overwrite config %q", in) - } - - return string(valuesYamlOverwriteBytes), nil -} - -type componentCRPatch struct { - Spec componentSpecPatch `json:"spec"` -} - -type componentSpecPatch struct { - Namespace string `json:"namespace"` - Name string `json:"name"` - Version string `json:"version"` - DeployNamespace *string `json:"deployNamespace"` - ValuesYamlOverwrite *string `json:"valuesYamlOverwrite"` -} - -func toComponentCRPatch(component *ecosystem.ComponentInstallation) (*componentCRPatch, error) { - deployNamespace, err := toDeployNamespace(component.DeployConfig) - if err != nil { - return nil, err - } - - valuesYamlOverwrite, err := toValuesYamlOverwrite(component.DeployConfig) - if err != nil { - return nil, err - } - - spec := componentSpecPatch{ - Namespace: string(component.Name.Namespace), - Name: string(component.Name.SimpleName), - Version: component.ExpectedVersion.String(), - } - if deployNamespace != "" { - spec.DeployNamespace = &deployNamespace - } - if valuesYamlOverwrite != "" { - spec.ValuesYamlOverwrite = &valuesYamlOverwrite - } - - return &componentCRPatch{ - Spec: spec, - }, nil -} - -func toComponentCRPatchBytes(component *ecosystem.ComponentInstallation) ([]byte, error) { - crPatch, err := toComponentCRPatch(component) - if err != nil { - return nil, domainservice.NewInternalError(err, "failed to create component CR patch for component %q", component.Name) - } - patch, err := json.Marshal(crPatch) - - if err != nil { - return []byte{}, domainservice.NewInternalError(err, "cannot patch component CR for component %q", component.Name) - } - return patch, nil -} diff --git a/pkg/adapter/kubernetes/componentcr/componentSerializer_test.go b/pkg/adapter/kubernetes/componentcr/componentSerializer_test.go deleted file mode 100644 index 0c523536..00000000 --- a/pkg/adapter/kubernetes/componentcr/componentSerializer_test.go +++ /dev/null @@ -1,344 +0,0 @@ -package componentcr - -import ( - _ "embed" - "fmt" - "github.com/Masterminds/semver/v3" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" - compV1 "github.com/cloudogu/k8s-component-operator/pkg/api/v1" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "gopkg.in/yaml.v2" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "testing" -) - -const ( - testDeployNamespace = "ecosystem" - testStatus = ecosystem.ComponentStatusNotInstalled - testHealthStatus = compV1.AvailableHealthStatus - testResourceVersion = "1" -) - -var ( - //go:embed testdata/testPatch - testPatchBytes []byte - testVersion1, _ = semver.NewVersion("1.0.0-1") -) - -func Test_parseComponentCR(t *testing.T) { - valuesOverwrite := map[string]interface{}{"key": "value", "key1": map[string]string{"key": "value"}} - expectedValuesOverwrite := map[string]interface{}{"key": "value", "key1": map[string]interface{}{"key": "value"}} - valuesOverwriteYAMLBytes, err := yaml.Marshal(valuesOverwrite) - require.NoError(t, err) - - type args struct { - cr *compV1.Component - } - tests := []struct { - name string - args args - want *ecosystem.ComponentInstallation - wantErr assert.ErrorAssertionFunc - }{ - { - name: "success", - args: args{ - cr: &compV1.Component{ - ObjectMeta: metav1.ObjectMeta{ - Name: testComponentNameRaw, - Namespace: testDeployNamespace, - ResourceVersion: testResourceVersion, - }, - Spec: compV1.ComponentSpec{ - Namespace: testNamespace, - Name: testComponentNameRaw, - Version: testVersion1.String(), - DeployNamespace: "longhorn-system", - ValuesYamlOverwrite: string(valuesOverwriteYAMLBytes), - }, - Status: compV1.ComponentStatus{ - Status: testStatus, - Health: testHealthStatus, - InstalledVersion: testVersion1.String(), - }, - }, - }, - want: &ecosystem.ComponentInstallation{ - Name: testComponentName, - ExpectedVersion: testVersion1, - ActualVersion: testVersion1, - Status: testStatus, - Health: ecosystem.HealthStatus(testHealthStatus), - DeployConfig: map[string]interface{}{ - "deployNamespace": "longhorn-system", - "overwriteConfig": expectedValuesOverwrite, - }, - PersistenceContext: map[string]interface{}{ - componentInstallationRepoContextKey: componentInstallationRepoContext{testResourceVersion}, - }, - }, - wantErr: assert.NoError, - }, - { - name: "should return error on nil component", - args: args{ - cr: nil, - }, - wantErr: assert.Error, - }, - { - name: "should return expected version parse error", - args: args{ - cr: &compV1.Component{ - ObjectMeta: metav1.ObjectMeta{ - Name: testComponentNameRaw, - }, - Spec: compV1.ComponentSpec{ - Namespace: testNamespace, - Name: testComponentNameRaw, - Version: "fsdfsd", - }, - }, - }, - wantErr: assert.Error, - }, - { - name: "should return actual version parse error", - args: args{ - cr: &compV1.Component{ - ObjectMeta: metav1.ObjectMeta{ - Name: testComponentNameRaw, - ResourceVersion: testResourceVersion, - }, - Spec: compV1.ComponentSpec{ - Namespace: testNamespace, - Name: testComponentNameRaw, - Version: testVersion1.String(), - }, - Status: compV1.ComponentStatus{ - InstalledVersion: "fsdfsd", - }, - }, - }, - want: &ecosystem.ComponentInstallation{ - Name: testComponentName, - ExpectedVersion: testVersion1, - ActualVersion: nil, - DeployConfig: map[string]interface{}{}, - PersistenceContext: map[string]interface{}{ - componentInstallationRepoContextKey: componentInstallationRepoContext{testResourceVersion}, - }, - }, - wantErr: assert.NoError, - }, - { - name: "should return error on missing resource name", - args: args{ - cr: &compV1.Component{ - ObjectMeta: metav1.ObjectMeta{}, - Spec: compV1.ComponentSpec{ - Namespace: testNamespace, - Name: testComponentNameRaw, - Version: testVersion1.String(), - }, - }, - }, - wantErr: assert.Error, - }, - { - name: "should return error on wrong package config value", - args: args{ - cr: &compV1.Component{ - ObjectMeta: metav1.ObjectMeta{ - Name: "name", - }, - Spec: compV1.ComponentSpec{ - Namespace: testNamespace, - Name: testComponentNameRaw, - Version: testVersion1.String(), - DeployNamespace: "longhorn-system", - ValuesYamlOverwrite: "no yaml object", - }, - }, - }, - wantErr: assert.Error, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := parseComponentCR(tt.args.cr) - if !tt.wantErr(t, err, fmt.Sprintf("parseComponentCR(%v)", tt.args.cr)) { - return - } - assert.Equal(t, tt.want, got) - }) - } -} - -func Test_toComponentCR(t *testing.T) { - type args struct { - componentInstallation *ecosystem.ComponentInstallation - } - tests := []struct { - name string - args args - want *compV1.Component - wantErr assert.ErrorAssertionFunc - }{ - { - name: "success", - args: args{ - componentInstallation: &ecosystem.ComponentInstallation{ - Name: testComponentName, - ExpectedVersion: testVersion1, - Status: testStatus, - Health: ecosystem.HealthStatus(testHealthStatus), - PersistenceContext: map[string]interface{}{ - componentInstallationRepoContextKey: componentInstallationRepoContext{testResourceVersion}, - }, - DeployConfig: map[string]interface{}{ - "deployNamespace": "longhorn-system", - "overwriteConfig": map[string]interface{}{"key": "value"}, - }, - }, - }, - want: &compV1.Component{ - ObjectMeta: metav1.ObjectMeta{ - Name: testComponentNameRaw, - Labels: map[string]string{ - ComponentNameLabelKey: testComponentNameRaw, - ComponentVersionLabelKey: testVersion1.String(), - }, - }, - Spec: compV1.ComponentSpec{ - Namespace: testNamespace, - Name: testComponentNameRaw, - Version: testVersion1.String(), - DeployNamespace: "longhorn-system", - ValuesYamlOverwrite: "key: value\n", - }, - }, - wantErr: assert.NoError, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := toComponentCR(tt.args.componentInstallation) - if !tt.wantErr(t, err, fmt.Sprintf("toComponentCR(%v)", tt.args.componentInstallation)) { - return - } - assert.Equal(t, tt.want, got) - }) - } -} - -func Test_toComponentCRPatch(t *testing.T) { - testDeployNamespace := "longhorn-system" - testDeployConfig := "key: value\n" - type args struct { - component *ecosystem.ComponentInstallation - } - tests := []struct { - name string - args args - want *componentCRPatch - wantErr assert.ErrorAssertionFunc - }{ - { - name: "success", - args: args{ - component: &ecosystem.ComponentInstallation{ - Name: testComponentName, - ExpectedVersion: testVersion1, - DeployConfig: map[string]interface{}{ - "deployNamespace": "longhorn-system", - "overwriteConfig": map[string]interface{}{"key": "value"}, - }, - }, - }, - want: &componentCRPatch{ - Spec: componentSpecPatch{ - Namespace: testNamespace, - Name: testComponentNameRaw, - Version: testVersion1.String(), - DeployNamespace: &testDeployNamespace, - ValuesYamlOverwrite: &testDeployConfig, - }, - }, - wantErr: assert.NoError, - }, - { - name: "should return error on wrong package config type", - args: args{ - component: &ecosystem.ComponentInstallation{ - Name: testComponentName, - ExpectedVersion: testVersion1, - DeployConfig: map[string]interface{}{ - "deployNamespace": map[string]interface{}{"no": "string"}, - }, - }, - }, - wantErr: assert.Error, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := toComponentCRPatch(tt.args.component) - if !tt.wantErr(t, err, fmt.Sprintf("toComponentCRPatch(%v)", tt.args.component)) { - return - } - assert.Equal(t, tt.want, got) - }) - } -} - -func Test_toComponentCRPatchBytes(t *testing.T) { - type args struct { - component *ecosystem.ComponentInstallation - } - tests := []struct { - name string - args args - want []byte - wantErr assert.ErrorAssertionFunc - }{ - { - name: "success", - args: args{ - component: &ecosystem.ComponentInstallation{ - Name: testComponentName, - ExpectedVersion: testVersion1, - DeployConfig: map[string]interface{}{ - "deployNamespace": "longhorn-system", - "overwriteConfig": map[string]interface{}{"key": "value"}, - }, - }, - }, - want: testPatchBytes, - wantErr: assert.NoError, - }, - { - name: "should return error on error creating patch", - args: args{ - component: &ecosystem.ComponentInstallation{ - Name: testComponentName, - ExpectedVersion: testVersion1, - DeployConfig: map[string]interface{}{ - "deployNamespace": map[string]interface{}{"no": "string"}, - }, - }, - }, - wantErr: assert.Error, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := toComponentCRPatchBytes(tt.args.component) - if !tt.wantErr(t, err, fmt.Sprintf("toComponentCRPatchBytes(%v)", tt.args.component)) { - return - } - assert.Equal(t, tt.want, got) - }) - } -} diff --git a/pkg/adapter/kubernetes/componentcr/interfaces.go b/pkg/adapter/kubernetes/componentcr/interfaces.go deleted file mode 100644 index d21a697d..00000000 --- a/pkg/adapter/kubernetes/componentcr/interfaces.go +++ /dev/null @@ -1,11 +0,0 @@ -package componentcr - -import compCli "github.com/cloudogu/k8s-component-operator/pkg/api/ecosystem" - -// used for mocks - -//nolint:unused -//goland:noinspection GoUnusedType -type componentRepo interface { - compCli.ComponentInterface -} diff --git a/pkg/adapter/kubernetes/componentcr/mock_componentRepo_test.go b/pkg/adapter/kubernetes/componentcr/mock_componentRepo_test.go deleted file mode 100644 index 40d6516c..00000000 --- a/pkg/adapter/kubernetes/componentcr/mock_componentRepo_test.go +++ /dev/null @@ -1,1049 +0,0 @@ -// Code generated by mockery v2.53.3. DO NOT EDIT. - -package componentcr - -import ( - context "context" - - mock "github.com/stretchr/testify/mock" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - types "k8s.io/apimachinery/pkg/types" - - v1 "github.com/cloudogu/k8s-component-operator/pkg/api/v1" - - watch "k8s.io/apimachinery/pkg/watch" -) - -// mockComponentRepo is an autogenerated mock type for the componentRepo type -type mockComponentRepo struct { - mock.Mock -} - -type mockComponentRepo_Expecter struct { - mock *mock.Mock -} - -func (_m *mockComponentRepo) EXPECT() *mockComponentRepo_Expecter { - return &mockComponentRepo_Expecter{mock: &_m.Mock} -} - -// AddFinalizer provides a mock function with given fields: ctx, component, finalizer -func (_m *mockComponentRepo) AddFinalizer(ctx context.Context, component *v1.Component, finalizer string) (*v1.Component, error) { - ret := _m.Called(ctx, component, finalizer) - - if len(ret) == 0 { - panic("no return value specified for AddFinalizer") - } - - var r0 *v1.Component - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *v1.Component, string) (*v1.Component, error)); ok { - return rf(ctx, component, finalizer) - } - if rf, ok := ret.Get(0).(func(context.Context, *v1.Component, string) *v1.Component); ok { - r0 = rf(ctx, component, finalizer) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*v1.Component) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, *v1.Component, string) error); ok { - r1 = rf(ctx, component, finalizer) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// mockComponentRepo_AddFinalizer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddFinalizer' -type mockComponentRepo_AddFinalizer_Call struct { - *mock.Call -} - -// AddFinalizer is a helper method to define mock.On call -// - ctx context.Context -// - component *v1.Component -// - finalizer string -func (_e *mockComponentRepo_Expecter) AddFinalizer(ctx interface{}, component interface{}, finalizer interface{}) *mockComponentRepo_AddFinalizer_Call { - return &mockComponentRepo_AddFinalizer_Call{Call: _e.mock.On("AddFinalizer", ctx, component, finalizer)} -} - -func (_c *mockComponentRepo_AddFinalizer_Call) Run(run func(ctx context.Context, component *v1.Component, finalizer string)) *mockComponentRepo_AddFinalizer_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(*v1.Component), args[2].(string)) - }) - return _c -} - -func (_c *mockComponentRepo_AddFinalizer_Call) Return(_a0 *v1.Component, _a1 error) *mockComponentRepo_AddFinalizer_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *mockComponentRepo_AddFinalizer_Call) RunAndReturn(run func(context.Context, *v1.Component, string) (*v1.Component, error)) *mockComponentRepo_AddFinalizer_Call { - _c.Call.Return(run) - return _c -} - -// Create provides a mock function with given fields: ctx, component, opts -func (_m *mockComponentRepo) Create(ctx context.Context, component *v1.Component, opts metav1.CreateOptions) (*v1.Component, error) { - ret := _m.Called(ctx, component, opts) - - if len(ret) == 0 { - panic("no return value specified for Create") - } - - var r0 *v1.Component - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *v1.Component, metav1.CreateOptions) (*v1.Component, error)); ok { - return rf(ctx, component, opts) - } - if rf, ok := ret.Get(0).(func(context.Context, *v1.Component, metav1.CreateOptions) *v1.Component); ok { - r0 = rf(ctx, component, opts) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*v1.Component) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, *v1.Component, metav1.CreateOptions) error); ok { - r1 = rf(ctx, component, opts) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// mockComponentRepo_Create_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Create' -type mockComponentRepo_Create_Call struct { - *mock.Call -} - -// Create is a helper method to define mock.On call -// - ctx context.Context -// - component *v1.Component -// - opts metav1.CreateOptions -func (_e *mockComponentRepo_Expecter) Create(ctx interface{}, component interface{}, opts interface{}) *mockComponentRepo_Create_Call { - return &mockComponentRepo_Create_Call{Call: _e.mock.On("Create", ctx, component, opts)} -} - -func (_c *mockComponentRepo_Create_Call) Run(run func(ctx context.Context, component *v1.Component, opts metav1.CreateOptions)) *mockComponentRepo_Create_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(*v1.Component), args[2].(metav1.CreateOptions)) - }) - return _c -} - -func (_c *mockComponentRepo_Create_Call) Return(_a0 *v1.Component, _a1 error) *mockComponentRepo_Create_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *mockComponentRepo_Create_Call) RunAndReturn(run func(context.Context, *v1.Component, metav1.CreateOptions) (*v1.Component, error)) *mockComponentRepo_Create_Call { - _c.Call.Return(run) - return _c -} - -// Delete provides a mock function with given fields: ctx, name, opts -func (_m *mockComponentRepo) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error { - ret := _m.Called(ctx, name, opts) - - if len(ret) == 0 { - panic("no return value specified for Delete") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string, metav1.DeleteOptions) error); ok { - r0 = rf(ctx, name, opts) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// mockComponentRepo_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete' -type mockComponentRepo_Delete_Call struct { - *mock.Call -} - -// Delete is a helper method to define mock.On call -// - ctx context.Context -// - name string -// - opts metav1.DeleteOptions -func (_e *mockComponentRepo_Expecter) Delete(ctx interface{}, name interface{}, opts interface{}) *mockComponentRepo_Delete_Call { - return &mockComponentRepo_Delete_Call{Call: _e.mock.On("Delete", ctx, name, opts)} -} - -func (_c *mockComponentRepo_Delete_Call) Run(run func(ctx context.Context, name string, opts metav1.DeleteOptions)) *mockComponentRepo_Delete_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string), args[2].(metav1.DeleteOptions)) - }) - return _c -} - -func (_c *mockComponentRepo_Delete_Call) Return(_a0 error) *mockComponentRepo_Delete_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *mockComponentRepo_Delete_Call) RunAndReturn(run func(context.Context, string, metav1.DeleteOptions) error) *mockComponentRepo_Delete_Call { - _c.Call.Return(run) - return _c -} - -// DeleteCollection provides a mock function with given fields: ctx, opts, listOpts -func (_m *mockComponentRepo) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error { - ret := _m.Called(ctx, opts, listOpts) - - if len(ret) == 0 { - panic("no return value specified for DeleteCollection") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, metav1.DeleteOptions, metav1.ListOptions) error); ok { - r0 = rf(ctx, opts, listOpts) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// mockComponentRepo_DeleteCollection_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteCollection' -type mockComponentRepo_DeleteCollection_Call struct { - *mock.Call -} - -// DeleteCollection is a helper method to define mock.On call -// - ctx context.Context -// - opts metav1.DeleteOptions -// - listOpts metav1.ListOptions -func (_e *mockComponentRepo_Expecter) DeleteCollection(ctx interface{}, opts interface{}, listOpts interface{}) *mockComponentRepo_DeleteCollection_Call { - return &mockComponentRepo_DeleteCollection_Call{Call: _e.mock.On("DeleteCollection", ctx, opts, listOpts)} -} - -func (_c *mockComponentRepo_DeleteCollection_Call) Run(run func(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions)) *mockComponentRepo_DeleteCollection_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(metav1.DeleteOptions), args[2].(metav1.ListOptions)) - }) - return _c -} - -func (_c *mockComponentRepo_DeleteCollection_Call) Return(_a0 error) *mockComponentRepo_DeleteCollection_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *mockComponentRepo_DeleteCollection_Call) RunAndReturn(run func(context.Context, metav1.DeleteOptions, metav1.ListOptions) error) *mockComponentRepo_DeleteCollection_Call { - _c.Call.Return(run) - return _c -} - -// Get provides a mock function with given fields: ctx, name, opts -func (_m *mockComponentRepo) Get(ctx context.Context, name string, opts metav1.GetOptions) (*v1.Component, error) { - ret := _m.Called(ctx, name, opts) - - if len(ret) == 0 { - panic("no return value specified for Get") - } - - var r0 *v1.Component - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, metav1.GetOptions) (*v1.Component, error)); ok { - return rf(ctx, name, opts) - } - if rf, ok := ret.Get(0).(func(context.Context, string, metav1.GetOptions) *v1.Component); ok { - r0 = rf(ctx, name, opts) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*v1.Component) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string, metav1.GetOptions) error); ok { - r1 = rf(ctx, name, opts) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// mockComponentRepo_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get' -type mockComponentRepo_Get_Call struct { - *mock.Call -} - -// Get is a helper method to define mock.On call -// - ctx context.Context -// - name string -// - opts metav1.GetOptions -func (_e *mockComponentRepo_Expecter) Get(ctx interface{}, name interface{}, opts interface{}) *mockComponentRepo_Get_Call { - return &mockComponentRepo_Get_Call{Call: _e.mock.On("Get", ctx, name, opts)} -} - -func (_c *mockComponentRepo_Get_Call) Run(run func(ctx context.Context, name string, opts metav1.GetOptions)) *mockComponentRepo_Get_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string), args[2].(metav1.GetOptions)) - }) - return _c -} - -func (_c *mockComponentRepo_Get_Call) Return(_a0 *v1.Component, _a1 error) *mockComponentRepo_Get_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *mockComponentRepo_Get_Call) RunAndReturn(run func(context.Context, string, metav1.GetOptions) (*v1.Component, error)) *mockComponentRepo_Get_Call { - _c.Call.Return(run) - return _c -} - -// List provides a mock function with given fields: ctx, opts -func (_m *mockComponentRepo) List(ctx context.Context, opts metav1.ListOptions) (*v1.ComponentList, error) { - ret := _m.Called(ctx, opts) - - if len(ret) == 0 { - panic("no return value specified for List") - } - - var r0 *v1.ComponentList - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, metav1.ListOptions) (*v1.ComponentList, error)); ok { - return rf(ctx, opts) - } - if rf, ok := ret.Get(0).(func(context.Context, metav1.ListOptions) *v1.ComponentList); ok { - r0 = rf(ctx, opts) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*v1.ComponentList) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, metav1.ListOptions) error); ok { - r1 = rf(ctx, opts) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// mockComponentRepo_List_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'List' -type mockComponentRepo_List_Call struct { - *mock.Call -} - -// List is a helper method to define mock.On call -// - ctx context.Context -// - opts metav1.ListOptions -func (_e *mockComponentRepo_Expecter) List(ctx interface{}, opts interface{}) *mockComponentRepo_List_Call { - return &mockComponentRepo_List_Call{Call: _e.mock.On("List", ctx, opts)} -} - -func (_c *mockComponentRepo_List_Call) Run(run func(ctx context.Context, opts metav1.ListOptions)) *mockComponentRepo_List_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(metav1.ListOptions)) - }) - return _c -} - -func (_c *mockComponentRepo_List_Call) Return(_a0 *v1.ComponentList, _a1 error) *mockComponentRepo_List_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *mockComponentRepo_List_Call) RunAndReturn(run func(context.Context, metav1.ListOptions) (*v1.ComponentList, error)) *mockComponentRepo_List_Call { - _c.Call.Return(run) - return _c -} - -// Patch provides a mock function with given fields: ctx, name, pt, data, opts, subresources -func (_m *mockComponentRepo) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (*v1.Component, error) { - _va := make([]interface{}, len(subresources)) - for _i := range subresources { - _va[_i] = subresources[_i] - } - var _ca []interface{} - _ca = append(_ca, ctx, name, pt, data, opts) - _ca = append(_ca, _va...) - ret := _m.Called(_ca...) - - if len(ret) == 0 { - panic("no return value specified for Patch") - } - - var r0 *v1.Component - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, types.PatchType, []byte, metav1.PatchOptions, ...string) (*v1.Component, error)); ok { - return rf(ctx, name, pt, data, opts, subresources...) - } - if rf, ok := ret.Get(0).(func(context.Context, string, types.PatchType, []byte, metav1.PatchOptions, ...string) *v1.Component); ok { - r0 = rf(ctx, name, pt, data, opts, subresources...) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*v1.Component) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string, types.PatchType, []byte, metav1.PatchOptions, ...string) error); ok { - r1 = rf(ctx, name, pt, data, opts, subresources...) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// mockComponentRepo_Patch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Patch' -type mockComponentRepo_Patch_Call struct { - *mock.Call -} - -// Patch is a helper method to define mock.On call -// - ctx context.Context -// - name string -// - pt types.PatchType -// - data []byte -// - opts metav1.PatchOptions -// - subresources ...string -func (_e *mockComponentRepo_Expecter) Patch(ctx interface{}, name interface{}, pt interface{}, data interface{}, opts interface{}, subresources ...interface{}) *mockComponentRepo_Patch_Call { - return &mockComponentRepo_Patch_Call{Call: _e.mock.On("Patch", - append([]interface{}{ctx, name, pt, data, opts}, subresources...)...)} -} - -func (_c *mockComponentRepo_Patch_Call) Run(run func(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string)) *mockComponentRepo_Patch_Call { - _c.Call.Run(func(args mock.Arguments) { - variadicArgs := make([]string, len(args)-5) - for i, a := range args[5:] { - if a != nil { - variadicArgs[i] = a.(string) - } - } - run(args[0].(context.Context), args[1].(string), args[2].(types.PatchType), args[3].([]byte), args[4].(metav1.PatchOptions), variadicArgs...) - }) - return _c -} - -func (_c *mockComponentRepo_Patch_Call) Return(result *v1.Component, err error) *mockComponentRepo_Patch_Call { - _c.Call.Return(result, err) - return _c -} - -func (_c *mockComponentRepo_Patch_Call) RunAndReturn(run func(context.Context, string, types.PatchType, []byte, metav1.PatchOptions, ...string) (*v1.Component, error)) *mockComponentRepo_Patch_Call { - _c.Call.Return(run) - return _c -} - -// RemoveFinalizer provides a mock function with given fields: ctx, component, finalizer -func (_m *mockComponentRepo) RemoveFinalizer(ctx context.Context, component *v1.Component, finalizer string) (*v1.Component, error) { - ret := _m.Called(ctx, component, finalizer) - - if len(ret) == 0 { - panic("no return value specified for RemoveFinalizer") - } - - var r0 *v1.Component - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *v1.Component, string) (*v1.Component, error)); ok { - return rf(ctx, component, finalizer) - } - if rf, ok := ret.Get(0).(func(context.Context, *v1.Component, string) *v1.Component); ok { - r0 = rf(ctx, component, finalizer) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*v1.Component) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, *v1.Component, string) error); ok { - r1 = rf(ctx, component, finalizer) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// mockComponentRepo_RemoveFinalizer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveFinalizer' -type mockComponentRepo_RemoveFinalizer_Call struct { - *mock.Call -} - -// RemoveFinalizer is a helper method to define mock.On call -// - ctx context.Context -// - component *v1.Component -// - finalizer string -func (_e *mockComponentRepo_Expecter) RemoveFinalizer(ctx interface{}, component interface{}, finalizer interface{}) *mockComponentRepo_RemoveFinalizer_Call { - return &mockComponentRepo_RemoveFinalizer_Call{Call: _e.mock.On("RemoveFinalizer", ctx, component, finalizer)} -} - -func (_c *mockComponentRepo_RemoveFinalizer_Call) Run(run func(ctx context.Context, component *v1.Component, finalizer string)) *mockComponentRepo_RemoveFinalizer_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(*v1.Component), args[2].(string)) - }) - return _c -} - -func (_c *mockComponentRepo_RemoveFinalizer_Call) Return(_a0 *v1.Component, _a1 error) *mockComponentRepo_RemoveFinalizer_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *mockComponentRepo_RemoveFinalizer_Call) RunAndReturn(run func(context.Context, *v1.Component, string) (*v1.Component, error)) *mockComponentRepo_RemoveFinalizer_Call { - _c.Call.Return(run) - return _c -} - -// Update provides a mock function with given fields: ctx, component, opts -func (_m *mockComponentRepo) Update(ctx context.Context, component *v1.Component, opts metav1.UpdateOptions) (*v1.Component, error) { - ret := _m.Called(ctx, component, opts) - - if len(ret) == 0 { - panic("no return value specified for Update") - } - - var r0 *v1.Component - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *v1.Component, metav1.UpdateOptions) (*v1.Component, error)); ok { - return rf(ctx, component, opts) - } - if rf, ok := ret.Get(0).(func(context.Context, *v1.Component, metav1.UpdateOptions) *v1.Component); ok { - r0 = rf(ctx, component, opts) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*v1.Component) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, *v1.Component, metav1.UpdateOptions) error); ok { - r1 = rf(ctx, component, opts) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// mockComponentRepo_Update_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Update' -type mockComponentRepo_Update_Call struct { - *mock.Call -} - -// Update is a helper method to define mock.On call -// - ctx context.Context -// - component *v1.Component -// - opts metav1.UpdateOptions -func (_e *mockComponentRepo_Expecter) Update(ctx interface{}, component interface{}, opts interface{}) *mockComponentRepo_Update_Call { - return &mockComponentRepo_Update_Call{Call: _e.mock.On("Update", ctx, component, opts)} -} - -func (_c *mockComponentRepo_Update_Call) Run(run func(ctx context.Context, component *v1.Component, opts metav1.UpdateOptions)) *mockComponentRepo_Update_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(*v1.Component), args[2].(metav1.UpdateOptions)) - }) - return _c -} - -func (_c *mockComponentRepo_Update_Call) Return(_a0 *v1.Component, _a1 error) *mockComponentRepo_Update_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *mockComponentRepo_Update_Call) RunAndReturn(run func(context.Context, *v1.Component, metav1.UpdateOptions) (*v1.Component, error)) *mockComponentRepo_Update_Call { - _c.Call.Return(run) - return _c -} - -// UpdateExpectedComponentVersion provides a mock function with given fields: ctx, componentName, version -func (_m *mockComponentRepo) UpdateExpectedComponentVersion(ctx context.Context, componentName string, version string) (*v1.Component, error) { - ret := _m.Called(ctx, componentName, version) - - if len(ret) == 0 { - panic("no return value specified for UpdateExpectedComponentVersion") - } - - var r0 *v1.Component - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string) (*v1.Component, error)); ok { - return rf(ctx, componentName, version) - } - if rf, ok := ret.Get(0).(func(context.Context, string, string) *v1.Component); ok { - r0 = rf(ctx, componentName, version) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*v1.Component) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { - r1 = rf(ctx, componentName, version) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// mockComponentRepo_UpdateExpectedComponentVersion_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateExpectedComponentVersion' -type mockComponentRepo_UpdateExpectedComponentVersion_Call struct { - *mock.Call -} - -// UpdateExpectedComponentVersion is a helper method to define mock.On call -// - ctx context.Context -// - componentName string -// - version string -func (_e *mockComponentRepo_Expecter) UpdateExpectedComponentVersion(ctx interface{}, componentName interface{}, version interface{}) *mockComponentRepo_UpdateExpectedComponentVersion_Call { - return &mockComponentRepo_UpdateExpectedComponentVersion_Call{Call: _e.mock.On("UpdateExpectedComponentVersion", ctx, componentName, version)} -} - -func (_c *mockComponentRepo_UpdateExpectedComponentVersion_Call) Run(run func(ctx context.Context, componentName string, version string)) *mockComponentRepo_UpdateExpectedComponentVersion_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string), args[2].(string)) - }) - return _c -} - -func (_c *mockComponentRepo_UpdateExpectedComponentVersion_Call) Return(_a0 *v1.Component, _a1 error) *mockComponentRepo_UpdateExpectedComponentVersion_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *mockComponentRepo_UpdateExpectedComponentVersion_Call) RunAndReturn(run func(context.Context, string, string) (*v1.Component, error)) *mockComponentRepo_UpdateExpectedComponentVersion_Call { - _c.Call.Return(run) - return _c -} - -// UpdateStatus provides a mock function with given fields: ctx, component, opts -func (_m *mockComponentRepo) UpdateStatus(ctx context.Context, component *v1.Component, opts metav1.UpdateOptions) (*v1.Component, error) { - ret := _m.Called(ctx, component, opts) - - if len(ret) == 0 { - panic("no return value specified for UpdateStatus") - } - - var r0 *v1.Component - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *v1.Component, metav1.UpdateOptions) (*v1.Component, error)); ok { - return rf(ctx, component, opts) - } - if rf, ok := ret.Get(0).(func(context.Context, *v1.Component, metav1.UpdateOptions) *v1.Component); ok { - r0 = rf(ctx, component, opts) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*v1.Component) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, *v1.Component, metav1.UpdateOptions) error); ok { - r1 = rf(ctx, component, opts) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// mockComponentRepo_UpdateStatus_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateStatus' -type mockComponentRepo_UpdateStatus_Call struct { - *mock.Call -} - -// UpdateStatus is a helper method to define mock.On call -// - ctx context.Context -// - component *v1.Component -// - opts metav1.UpdateOptions -func (_e *mockComponentRepo_Expecter) UpdateStatus(ctx interface{}, component interface{}, opts interface{}) *mockComponentRepo_UpdateStatus_Call { - return &mockComponentRepo_UpdateStatus_Call{Call: _e.mock.On("UpdateStatus", ctx, component, opts)} -} - -func (_c *mockComponentRepo_UpdateStatus_Call) Run(run func(ctx context.Context, component *v1.Component, opts metav1.UpdateOptions)) *mockComponentRepo_UpdateStatus_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(*v1.Component), args[2].(metav1.UpdateOptions)) - }) - return _c -} - -func (_c *mockComponentRepo_UpdateStatus_Call) Return(_a0 *v1.Component, _a1 error) *mockComponentRepo_UpdateStatus_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *mockComponentRepo_UpdateStatus_Call) RunAndReturn(run func(context.Context, *v1.Component, metav1.UpdateOptions) (*v1.Component, error)) *mockComponentRepo_UpdateStatus_Call { - _c.Call.Return(run) - return _c -} - -// UpdateStatusDeleting provides a mock function with given fields: ctx, component -func (_m *mockComponentRepo) UpdateStatusDeleting(ctx context.Context, component *v1.Component) (*v1.Component, error) { - ret := _m.Called(ctx, component) - - if len(ret) == 0 { - panic("no return value specified for UpdateStatusDeleting") - } - - var r0 *v1.Component - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *v1.Component) (*v1.Component, error)); ok { - return rf(ctx, component) - } - if rf, ok := ret.Get(0).(func(context.Context, *v1.Component) *v1.Component); ok { - r0 = rf(ctx, component) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*v1.Component) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, *v1.Component) error); ok { - r1 = rf(ctx, component) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// mockComponentRepo_UpdateStatusDeleting_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateStatusDeleting' -type mockComponentRepo_UpdateStatusDeleting_Call struct { - *mock.Call -} - -// UpdateStatusDeleting is a helper method to define mock.On call -// - ctx context.Context -// - component *v1.Component -func (_e *mockComponentRepo_Expecter) UpdateStatusDeleting(ctx interface{}, component interface{}) *mockComponentRepo_UpdateStatusDeleting_Call { - return &mockComponentRepo_UpdateStatusDeleting_Call{Call: _e.mock.On("UpdateStatusDeleting", ctx, component)} -} - -func (_c *mockComponentRepo_UpdateStatusDeleting_Call) Run(run func(ctx context.Context, component *v1.Component)) *mockComponentRepo_UpdateStatusDeleting_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(*v1.Component)) - }) - return _c -} - -func (_c *mockComponentRepo_UpdateStatusDeleting_Call) Return(_a0 *v1.Component, _a1 error) *mockComponentRepo_UpdateStatusDeleting_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *mockComponentRepo_UpdateStatusDeleting_Call) RunAndReturn(run func(context.Context, *v1.Component) (*v1.Component, error)) *mockComponentRepo_UpdateStatusDeleting_Call { - _c.Call.Return(run) - return _c -} - -// UpdateStatusInstalled provides a mock function with given fields: ctx, component -func (_m *mockComponentRepo) UpdateStatusInstalled(ctx context.Context, component *v1.Component) (*v1.Component, error) { - ret := _m.Called(ctx, component) - - if len(ret) == 0 { - panic("no return value specified for UpdateStatusInstalled") - } - - var r0 *v1.Component - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *v1.Component) (*v1.Component, error)); ok { - return rf(ctx, component) - } - if rf, ok := ret.Get(0).(func(context.Context, *v1.Component) *v1.Component); ok { - r0 = rf(ctx, component) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*v1.Component) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, *v1.Component) error); ok { - r1 = rf(ctx, component) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// mockComponentRepo_UpdateStatusInstalled_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateStatusInstalled' -type mockComponentRepo_UpdateStatusInstalled_Call struct { - *mock.Call -} - -// UpdateStatusInstalled is a helper method to define mock.On call -// - ctx context.Context -// - component *v1.Component -func (_e *mockComponentRepo_Expecter) UpdateStatusInstalled(ctx interface{}, component interface{}) *mockComponentRepo_UpdateStatusInstalled_Call { - return &mockComponentRepo_UpdateStatusInstalled_Call{Call: _e.mock.On("UpdateStatusInstalled", ctx, component)} -} - -func (_c *mockComponentRepo_UpdateStatusInstalled_Call) Run(run func(ctx context.Context, component *v1.Component)) *mockComponentRepo_UpdateStatusInstalled_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(*v1.Component)) - }) - return _c -} - -func (_c *mockComponentRepo_UpdateStatusInstalled_Call) Return(_a0 *v1.Component, _a1 error) *mockComponentRepo_UpdateStatusInstalled_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *mockComponentRepo_UpdateStatusInstalled_Call) RunAndReturn(run func(context.Context, *v1.Component) (*v1.Component, error)) *mockComponentRepo_UpdateStatusInstalled_Call { - _c.Call.Return(run) - return _c -} - -// UpdateStatusInstalling provides a mock function with given fields: ctx, component -func (_m *mockComponentRepo) UpdateStatusInstalling(ctx context.Context, component *v1.Component) (*v1.Component, error) { - ret := _m.Called(ctx, component) - - if len(ret) == 0 { - panic("no return value specified for UpdateStatusInstalling") - } - - var r0 *v1.Component - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *v1.Component) (*v1.Component, error)); ok { - return rf(ctx, component) - } - if rf, ok := ret.Get(0).(func(context.Context, *v1.Component) *v1.Component); ok { - r0 = rf(ctx, component) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*v1.Component) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, *v1.Component) error); ok { - r1 = rf(ctx, component) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// mockComponentRepo_UpdateStatusInstalling_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateStatusInstalling' -type mockComponentRepo_UpdateStatusInstalling_Call struct { - *mock.Call -} - -// UpdateStatusInstalling is a helper method to define mock.On call -// - ctx context.Context -// - component *v1.Component -func (_e *mockComponentRepo_Expecter) UpdateStatusInstalling(ctx interface{}, component interface{}) *mockComponentRepo_UpdateStatusInstalling_Call { - return &mockComponentRepo_UpdateStatusInstalling_Call{Call: _e.mock.On("UpdateStatusInstalling", ctx, component)} -} - -func (_c *mockComponentRepo_UpdateStatusInstalling_Call) Run(run func(ctx context.Context, component *v1.Component)) *mockComponentRepo_UpdateStatusInstalling_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(*v1.Component)) - }) - return _c -} - -func (_c *mockComponentRepo_UpdateStatusInstalling_Call) Return(_a0 *v1.Component, _a1 error) *mockComponentRepo_UpdateStatusInstalling_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *mockComponentRepo_UpdateStatusInstalling_Call) RunAndReturn(run func(context.Context, *v1.Component) (*v1.Component, error)) *mockComponentRepo_UpdateStatusInstalling_Call { - _c.Call.Return(run) - return _c -} - -// UpdateStatusNotInstalled provides a mock function with given fields: ctx, component -func (_m *mockComponentRepo) UpdateStatusNotInstalled(ctx context.Context, component *v1.Component) (*v1.Component, error) { - ret := _m.Called(ctx, component) - - if len(ret) == 0 { - panic("no return value specified for UpdateStatusNotInstalled") - } - - var r0 *v1.Component - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *v1.Component) (*v1.Component, error)); ok { - return rf(ctx, component) - } - if rf, ok := ret.Get(0).(func(context.Context, *v1.Component) *v1.Component); ok { - r0 = rf(ctx, component) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*v1.Component) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, *v1.Component) error); ok { - r1 = rf(ctx, component) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// mockComponentRepo_UpdateStatusNotInstalled_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateStatusNotInstalled' -type mockComponentRepo_UpdateStatusNotInstalled_Call struct { - *mock.Call -} - -// UpdateStatusNotInstalled is a helper method to define mock.On call -// - ctx context.Context -// - component *v1.Component -func (_e *mockComponentRepo_Expecter) UpdateStatusNotInstalled(ctx interface{}, component interface{}) *mockComponentRepo_UpdateStatusNotInstalled_Call { - return &mockComponentRepo_UpdateStatusNotInstalled_Call{Call: _e.mock.On("UpdateStatusNotInstalled", ctx, component)} -} - -func (_c *mockComponentRepo_UpdateStatusNotInstalled_Call) Run(run func(ctx context.Context, component *v1.Component)) *mockComponentRepo_UpdateStatusNotInstalled_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(*v1.Component)) - }) - return _c -} - -func (_c *mockComponentRepo_UpdateStatusNotInstalled_Call) Return(_a0 *v1.Component, _a1 error) *mockComponentRepo_UpdateStatusNotInstalled_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *mockComponentRepo_UpdateStatusNotInstalled_Call) RunAndReturn(run func(context.Context, *v1.Component) (*v1.Component, error)) *mockComponentRepo_UpdateStatusNotInstalled_Call { - _c.Call.Return(run) - return _c -} - -// UpdateStatusUpgrading provides a mock function with given fields: ctx, component -func (_m *mockComponentRepo) UpdateStatusUpgrading(ctx context.Context, component *v1.Component) (*v1.Component, error) { - ret := _m.Called(ctx, component) - - if len(ret) == 0 { - panic("no return value specified for UpdateStatusUpgrading") - } - - var r0 *v1.Component - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *v1.Component) (*v1.Component, error)); ok { - return rf(ctx, component) - } - if rf, ok := ret.Get(0).(func(context.Context, *v1.Component) *v1.Component); ok { - r0 = rf(ctx, component) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*v1.Component) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, *v1.Component) error); ok { - r1 = rf(ctx, component) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// mockComponentRepo_UpdateStatusUpgrading_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateStatusUpgrading' -type mockComponentRepo_UpdateStatusUpgrading_Call struct { - *mock.Call -} - -// UpdateStatusUpgrading is a helper method to define mock.On call -// - ctx context.Context -// - component *v1.Component -func (_e *mockComponentRepo_Expecter) UpdateStatusUpgrading(ctx interface{}, component interface{}) *mockComponentRepo_UpdateStatusUpgrading_Call { - return &mockComponentRepo_UpdateStatusUpgrading_Call{Call: _e.mock.On("UpdateStatusUpgrading", ctx, component)} -} - -func (_c *mockComponentRepo_UpdateStatusUpgrading_Call) Run(run func(ctx context.Context, component *v1.Component)) *mockComponentRepo_UpdateStatusUpgrading_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(*v1.Component)) - }) - return _c -} - -func (_c *mockComponentRepo_UpdateStatusUpgrading_Call) Return(_a0 *v1.Component, _a1 error) *mockComponentRepo_UpdateStatusUpgrading_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *mockComponentRepo_UpdateStatusUpgrading_Call) RunAndReturn(run func(context.Context, *v1.Component) (*v1.Component, error)) *mockComponentRepo_UpdateStatusUpgrading_Call { - _c.Call.Return(run) - return _c -} - -// Watch provides a mock function with given fields: ctx, opts -func (_m *mockComponentRepo) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { - ret := _m.Called(ctx, opts) - - if len(ret) == 0 { - panic("no return value specified for Watch") - } - - var r0 watch.Interface - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, metav1.ListOptions) (watch.Interface, error)); ok { - return rf(ctx, opts) - } - if rf, ok := ret.Get(0).(func(context.Context, metav1.ListOptions) watch.Interface); ok { - r0 = rf(ctx, opts) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(watch.Interface) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, metav1.ListOptions) error); ok { - r1 = rf(ctx, opts) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// mockComponentRepo_Watch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Watch' -type mockComponentRepo_Watch_Call struct { - *mock.Call -} - -// Watch is a helper method to define mock.On call -// - ctx context.Context -// - opts metav1.ListOptions -func (_e *mockComponentRepo_Expecter) Watch(ctx interface{}, opts interface{}) *mockComponentRepo_Watch_Call { - return &mockComponentRepo_Watch_Call{Call: _e.mock.On("Watch", ctx, opts)} -} - -func (_c *mockComponentRepo_Watch_Call) Run(run func(ctx context.Context, opts metav1.ListOptions)) *mockComponentRepo_Watch_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(metav1.ListOptions)) - }) - return _c -} - -func (_c *mockComponentRepo_Watch_Call) Return(_a0 watch.Interface, _a1 error) *mockComponentRepo_Watch_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *mockComponentRepo_Watch_Call) RunAndReturn(run func(context.Context, metav1.ListOptions) (watch.Interface, error)) *mockComponentRepo_Watch_Call { - _c.Call.Return(run) - return _c -} - -// newMockComponentRepo creates a new instance of mockComponentRepo. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func newMockComponentRepo(t interface { - mock.TestingT - Cleanup(func()) -}) *mockComponentRepo { - mock := &mockComponentRepo{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/adapter/kubernetes/componentcr/testdata/testPatch b/pkg/adapter/kubernetes/componentcr/testdata/testPatch deleted file mode 100644 index 094025be..00000000 --- a/pkg/adapter/kubernetes/componentcr/testdata/testPatch +++ /dev/null @@ -1 +0,0 @@ -{"spec":{"namespace":"k8s","name":"my-component","version":"1.0.0-1","deployNamespace":"longhorn-system","valuesYamlOverwrite":"key: value\n"}} \ No newline at end of file diff --git a/pkg/adapter/kubernetes/dogucr/doguInstallationRepo_test.go b/pkg/adapter/kubernetes/dogucr/doguInstallationRepo_test.go index 554be5bc..1ad5d0af 100644 --- a/pkg/adapter/kubernetes/dogucr/doguInstallationRepo_test.go +++ b/pkg/adapter/kubernetes/dogucr/doguInstallationRepo_test.go @@ -3,6 +3,8 @@ package dogucr import ( "context" "errors" + "testing" + cescommons "github.com/cloudogu/ces-commons-lib/dogu" "github.com/cloudogu/cesapp-lib/core" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" @@ -16,7 +18,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" - "testing" ) var version3214, _ = core.ParseVersion("3.2.1-4") @@ -68,18 +69,20 @@ func Test_doguInstallationRepo_GetByName(t *testing.T) { // then require.NoError(t, err) quantity1 := resource.MustParse("1G") + rewriteTarget := "/" + additionalConfig := "snippet" assert.Equal(t, &ecosystem.DoguInstallation{ Name: postgresDoguName, Version: version3214, - Status: ecosystem.DoguStatusInstalled, + Status: "installed", Health: ecosystem.AvailableHealthStatus, UpgradeConfig: ecosystem.UpgradeConfig{}, PersistenceContext: persistenceContext, - MinVolumeSize: quantity2, + MinVolumeSize: &quantity2, ReverseProxyConfig: ecosystem.ReverseProxyConfig{ MaxBodySize: &quantity1, - RewriteTarget: "/", - AdditionalConfig: "snippet", + RewriteTarget: ecosystem.RewriteTarget(rewriteTarget), + AdditionalConfig: ecosystem.AdditionalConfig(additionalConfig), }, }, dogu) }) @@ -216,13 +219,13 @@ func Test_doguInstallationRepo_GetAll(t *testing.T) { "postgresql": { Name: postgresDoguName, Version: version1231, - MinVolumeSize: volumeQuantity2, + MinVolumeSize: &volumeQuantity2, PersistenceContext: map[string]interface{}{"doguInstallationRepoContext": doguInstallationRepoContext{resourceVersion: ""}}, }, "ldap": { Name: ldapDoguName, Version: version3213, - MinVolumeSize: volumeQuantity3, + MinVolumeSize: &volumeQuantity3, PersistenceContext: map[string]interface{}{"doguInstallationRepoContext": doguInstallationRepoContext{resourceVersion: ""}}, }, } @@ -275,8 +278,14 @@ func Test_doguInstallationRepo_Create(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: string(postgresDoguName.SimpleName), Labels: map[string]string{ - "app": "ces", - "dogu.name": string(postgresDoguName.SimpleName), + "app": "ces", + "k8s.cloudogu.com/app": "ces", + "dogu.name": string(postgresDoguName.SimpleName), + "k8s.cloudogu.com/dogu.name": string(postgresDoguName.SimpleName), + "app.kubernetes.io/name": string(postgresDoguName.SimpleName), + "app.kubernetes.io/version": version3214.Raw, + "app.kubernetes.io/part-of": "ces", + "app.kubernetes.io/managed-by": "k8s-blueprint-operator", }, }, Spec: v2.DoguSpec{ @@ -332,6 +341,7 @@ func Test_doguInstallationRepo_Update(t *testing.T) { "\"dataVolumeSize\":\"\"," + "\"minDataVolumeSize\":\"0\"}," + "\"supportMode\":false," + + "\"pauseReconciliation\":false," + "\"upgradeConfig\":{\"allowNamespaceSwitch\":false,\"forceUpgrade\":false}," + "\"additionalIngressAnnotations\":null," + "\"additionalMounts\":null}" + diff --git a/pkg/adapter/kubernetes/dogucr/doguSerializer.go b/pkg/adapter/kubernetes/dogucr/doguSerializer.go index b9a5879d..54c70171 100644 --- a/pkg/adapter/kubernetes/dogucr/doguSerializer.go +++ b/pkg/adapter/kubernetes/dogucr/doguSerializer.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + cescommons "github.com/cloudogu/ces-commons-lib/dogu" "github.com/cloudogu/cesapp-lib/core" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" @@ -22,6 +23,11 @@ func parseDoguCR(cr *v2.Dogu) (*ecosystem.DoguInstallation, error) { } // parse dogu fields version, versionErr := core.ParseVersion(cr.Spec.Version) + var installedVersion core.Version + var installedVersionErr error + if cr.Status.InstalledVersion != "" { + installedVersion, installedVersionErr = core.ParseVersion(cr.Status.InstalledVersion) + } doguName, nameErr := cescommons.QualifiedNameFromString(cr.Spec.Name) // the dogu-operator has a default of 2Gi if this field is 0 or not set @@ -32,7 +38,7 @@ func parseDoguCR(cr *v2.Dogu) (*ecosystem.DoguInstallation, error) { reverseProxyConfigEntries, proxyErr := parseDoguAdditionalIngressAnnotationsCR(cr.Spec.AdditionalIngressAnnotations) - err := errors.Join(versionErr, nameErr, volumeSizeErr, proxyErr) + err := errors.Join(versionErr, nameErr, volumeSizeErr, proxyErr, installedVersionErr) if err != nil { return nil, &domainservice.InternalError{ WrappedError: err, @@ -45,13 +51,16 @@ func parseDoguCR(cr *v2.Dogu) (*ecosystem.DoguInstallation, error) { persistenceContext[doguInstallationRepoContextKey] = doguInstallationRepoContext{ resourceVersion: cr.GetResourceVersion(), } + return &ecosystem.DoguInstallation{ Name: doguName, Version: version, Status: cr.Status.Status, Health: ecosystem.HealthStatus(cr.Status.Health), + InstalledVersion: installedVersion, + StartedAt: cr.Status.StartedAt, UpgradeConfig: ecosystem.UpgradeConfig{AllowNamespaceSwitch: cr.Spec.UpgradeConfig.AllowNamespaceSwitch}, - MinVolumeSize: minVolumeSize, + MinVolumeSize: &minVolumeSize, ReverseProxyConfig: reverseProxyConfigEntries, PersistenceContext: persistenceContext, AdditionalMounts: parseAdditionalMounts(cr.Spec.AdditionalMounts), @@ -74,8 +83,8 @@ func parseAdditionalMounts(mounts []v2.DataMount) []ecosystem.AdditionalMount { func parseDoguAdditionalIngressAnnotationsCR(annotations v2.IngressAnnotations) (ecosystem.ReverseProxyConfig, error) { reverseProxyConfig := ecosystem.ReverseProxyConfig{} - reverseProxyBodySize, ok := annotations[ecosystem.NginxIngressAnnotationBodySize] - if ok { + reverseProxyBodySize, bodySizeOk := annotations[ecosystem.NginxIngressAnnotationBodySize] + if bodySizeOk { // Sizes for Nginx can be specified in bytes, kilobytes (suffixes k and K) or megabytes (suffixes m and M), for example, “1024”, “8k”, “1m” in Decimal SI. // Since the actual dogu-operator and service-discovery just use this format we can expect that the values for the volume size in are safe to set in the doguinstallation. // Formats “1024”, “8k”, “1m” can be parsed by resource.Quantity @@ -94,13 +103,24 @@ func parseDoguAdditionalIngressAnnotationsCR(annotations v2.IngressAnnotations) } func toDoguCR(dogu *ecosystem.DoguInstallation) *v2.Dogu { + var minVolumeSize = ecosystem.VolumeSize{} + if dogu.MinVolumeSize != nil { + minVolumeSize = *dogu.MinVolumeSize + } + return &v2.Dogu{ TypeMeta: metav1.TypeMeta{}, ObjectMeta: metav1.ObjectMeta{ Name: string(dogu.Name.SimpleName), Labels: map[string]string{ - "app": "ces", - "dogu.name": string(dogu.Name.SimpleName), + "app": "ces", + "k8s.cloudogu.com/app": "ces", + "dogu.name": string(dogu.Name.SimpleName), + "k8s.cloudogu.com/dogu.name": string(dogu.Name.SimpleName), + "app.kubernetes.io/name": string(dogu.Name.SimpleName), + "app.kubernetes.io/version": dogu.Version.String(), + "app.kubernetes.io/part-of": "ces", + "app.kubernetes.io/managed-by": "k8s-blueprint-operator", }, }, Spec: v2.DoguSpec{ @@ -110,9 +130,10 @@ func toDoguCR(dogu *ecosystem.DoguInstallation) *v2.Dogu { // always set MinDataVolumeSize instead of the deprecated DataVolumeSize // the dogu-operator has a default of 2GiB if this field is 0 or not set // we just always set this value, if a new dogu CR is created via blueprint - MinDataVolumeSize: dogu.MinVolumeSize, + MinDataVolumeSize: minVolumeSize, }, - SupportMode: false, + SupportMode: false, + PauseReconciliation: false, // should be always false on installation UpgradeConfig: v2.UpgradeConfig{ AllowNamespaceSwitch: dogu.UpgradeConfig.AllowNamespaceSwitch, ForceUpgrade: false, @@ -138,6 +159,10 @@ func toDoguCRAdditionalMounts(mounts []ecosystem.AdditionalMount) []v2.DataMount } func getNginxIngressAnnotations(config ecosystem.ReverseProxyConfig) map[string]string { + if config.IsEmpty() { + return nil + } + annotations := v2.IngressAnnotations{} maxBodySize := config.MaxBodySize if maxBodySize != nil { @@ -172,6 +197,7 @@ type doguSpecPatch struct { Version string `json:"version"` Resources doguResourcesPatch `json:"resources"` SupportMode bool `json:"supportMode"` + PauseReconciliation bool `json:"pauseReconciliation"` UpgradeConfig upgradeConfigPatch `json:"upgradeConfig"` AdditionalIngressAnnotations map[string]string `json:"additionalIngressAnnotations"` AdditionalMounts []v2.DataMount `json:"additionalMounts"` @@ -191,6 +217,11 @@ type doguResourcesPatch struct { } func toDoguCRPatch(dogu *ecosystem.DoguInstallation) *doguCRPatch { + var minVolumeSize = ecosystem.VolumeSize{} + if dogu.MinVolumeSize != nil { + minVolumeSize = *dogu.MinVolumeSize + } + return &doguCRPatch{ Spec: doguSpecPatch{ Name: dogu.Name.String(), @@ -200,11 +231,12 @@ func toDoguCRPatch(dogu *ecosystem.DoguInstallation) *doguCRPatch { // the dogu-operator has a default of 2Gi if this field is 0 or not set // we just always set this value, if a new dogu CR is created via blueprint DataVolumeSize: "", - MinDataVolumeSize: dogu.MinVolumeSize, + MinDataVolumeSize: minVolumeSize, }, AdditionalIngressAnnotations: getNginxIngressAnnotations(dogu.ReverseProxyConfig), // always set this to false as a dogu cannot start in support mode - SupportMode: false, + SupportMode: false, + PauseReconciliation: dogu.PauseReconciliation, UpgradeConfig: upgradeConfigPatch{ AllowNamespaceSwitch: dogu.UpgradeConfig.AllowNamespaceSwitch, // this is a useful default as long as blueprints itself have no forceUpgrade flag implemented diff --git a/pkg/adapter/kubernetes/dogucr/doguSerializer_test.go b/pkg/adapter/kubernetes/dogucr/doguSerializer_test.go index 20472b3b..18714fcc 100644 --- a/pkg/adapter/kubernetes/dogucr/doguSerializer_test.go +++ b/pkg/adapter/kubernetes/dogucr/doguSerializer_test.go @@ -2,31 +2,41 @@ package dogucr import ( "fmt" + "reflect" + "testing" + "time" + cescommons "github.com/cloudogu/ces-commons-lib/dogu" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" v2 "github.com/cloudogu/k8s-dogu-lib/v2/api/v2" "github.com/stretchr/testify/assert" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "reflect" - "testing" ) -var postgresDoguName = cescommons.QualifiedName{ - Namespace: cescommons.Namespace("official"), - SimpleName: cescommons.SimpleName("postgresql"), -} -var ldapDoguName = cescommons.QualifiedName{ - Namespace: "official", - SimpleName: "ldap", -} -var volSize25G = resource.MustParse("25G") -var defaultVolSize = resource.MustParse(v2.DefaultVolumeSize) +var ( + ldapDoguName = cescommons.QualifiedName{ + Namespace: "official", + SimpleName: "ldap", + } + volSize25G = resource.MustParse("25G") + defaultVolSize = resource.MustParse(v2.DefaultVolumeSize) + postgresDoguName = cescommons.QualifiedName{ + Namespace: cescommons.Namespace("official"), + SimpleName: cescommons.SimpleName("postgresql"), + } + subfolder = "subfolder" + subfolder2 = "secsubfolder" + rewriteTarget = "/" + additionalConfig = "additional" + proxyBodySize = resource.MustParse("1G") +) func Test_parseDoguCR(t *testing.T) { type args struct { cr *v2.Dogu } + pointInTime := metav1.NewTime(time.Date(2024, 9, 23, 10, 0, 0, 0, time.UTC)) tests := []struct { name string args args @@ -56,20 +66,24 @@ func Test_parseDoguCR(t *testing.T) { }, }, Status: v2.DoguStatus{ - Status: v2.DoguStatusInstalled, - Health: v2.AvailableHealthStatus, + Status: v2.DoguStatusInstalled, + Health: v2.AvailableHealthStatus, + InstalledVersion: version3213.Raw, + StartedAt: pointInTime, }, }}, want: &ecosystem.DoguInstallation{ Name: postgresDoguName, Version: version3214, - Status: ecosystem.DoguStatusInstalled, + Status: "installed", Health: ecosystem.AvailableHealthStatus, UpgradeConfig: ecosystem.UpgradeConfig{ AllowNamespaceSwitch: true, }, - MinVolumeSize: defaultVolSize, + MinVolumeSize: &defaultVolSize, PersistenceContext: persistenceContext, + InstalledVersion: version3213, + StartedAt: pointInTime, }, wantErr: false, }, @@ -97,6 +111,66 @@ func Test_parseDoguCR(t *testing.T) { want: nil, wantErr: true, }, + { + name: "cannot parse installed version", + args: args{cr: &v2.Dogu{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Name: "postgresql", + ResourceVersion: "abc", + }, + Spec: v2.DoguSpec{ + Name: "official/postgresql", + Version: version3214.Raw, + Resources: v2.DoguResources{}, + UpgradeConfig: v2.UpgradeConfig{ + AllowNamespaceSwitch: false, + }, + }, + Status: v2.DoguStatus{ + Status: v2.DoguStatusInstalled, + Health: v2.AvailableHealthStatus, + InstalledVersion: "xyvz", + }, + }}, + want: nil, + wantErr: true, + }, + { + name: "accepts empty installed version", + args: args{cr: &v2.Dogu{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Name: "postgresql", + ResourceVersion: "abc", + }, + Spec: v2.DoguSpec{ + Name: "official/postgresql", + Version: version3214.Raw, + Resources: v2.DoguResources{}, + UpgradeConfig: v2.UpgradeConfig{ + AllowNamespaceSwitch: false, + }, + }, + Status: v2.DoguStatus{ + Status: v2.DoguStatusInstalled, + Health: v2.AvailableHealthStatus, + InstalledVersion: "", + }, + }}, + want: &ecosystem.DoguInstallation{ + Name: postgresDoguName, + Version: version3214, + Status: "installed", + Health: ecosystem.AvailableHealthStatus, + UpgradeConfig: ecosystem.UpgradeConfig{ + AllowNamespaceSwitch: false, + }, + MinVolumeSize: &defaultVolSize, + PersistenceContext: persistenceContext, + }, + wantErr: false, + }, { name: "parse additional mounts", args: args{cr: &v2.Dogu{ @@ -129,19 +203,19 @@ func Test_parseDoguCR(t *testing.T) { Name: postgresDoguName, Version: version3214, PersistenceContext: persistenceContext, - MinVolumeSize: defaultVolSize, + MinVolumeSize: &defaultVolSize, AdditionalMounts: []ecosystem.AdditionalMount{ { SourceType: ecosystem.DataSourceConfigMap, Name: "configmap", Volume: "volume", - Subfolder: "subfolder", + Subfolder: subfolder, }, { SourceType: ecosystem.DataSourceSecret, Name: "secret", Volume: "secvolume", - Subfolder: "secsubfolder", + Subfolder: subfolder2, }, }, }, @@ -167,7 +241,7 @@ func Test_parseDoguCR(t *testing.T) { Name: postgresDoguName, Version: version3214, PersistenceContext: persistenceContext, - MinVolumeSize: volSize25G, + MinVolumeSize: &volSize25G, }, wantErr: false, }, @@ -192,7 +266,7 @@ func Test_parseDoguCR(t *testing.T) { Name: postgresDoguName, Version: version3214, PersistenceContext: persistenceContext, - MinVolumeSize: volSize25G, + MinVolumeSize: &volSize25G, }, wantErr: false, }, @@ -216,7 +290,7 @@ func Test_parseDoguCR(t *testing.T) { Name: postgresDoguName, Version: version3214, PersistenceContext: persistenceContext, - MinVolumeSize: volSize25G, + MinVolumeSize: &volSize25G, }, wantErr: false, }, @@ -246,19 +320,26 @@ func Test_toDoguCR(t *testing.T) { dogu: &ecosystem.DoguInstallation{ Name: postgresDoguName, Version: version3214, - Status: ecosystem.DoguStatusInstalled, + Status: "installed", Health: ecosystem.AvailableHealthStatus, UpgradeConfig: ecosystem.UpgradeConfig{ AllowNamespaceSwitch: true, }, + PauseReconciliation: true, }, want: &v2.Dogu{ TypeMeta: metav1.TypeMeta{}, ObjectMeta: metav1.ObjectMeta{ Name: "postgresql", Labels: map[string]string{ - "app": "ces", - "dogu.name": "postgresql", + "app": "ces", + "k8s.cloudogu.com/app": "ces", + "dogu.name": "postgresql", + "k8s.cloudogu.com/dogu.name": "postgresql", + "app.kubernetes.io/name": "postgresql", + "app.kubernetes.io/version": version3214.Raw, + "app.kubernetes.io/part-of": "ces", + "app.kubernetes.io/managed-by": "k8s-blueprint-operator", }, }, Spec: v2.DoguSpec{ @@ -272,6 +353,7 @@ func Test_toDoguCR(t *testing.T) { ForceUpgrade: false, }, AdditionalIngressAnnotations: nil, + PauseReconciliation: false, // should be always false on installation }, Status: v2.DoguStatus{}, }, @@ -286,13 +368,13 @@ func Test_toDoguCR(t *testing.T) { SourceType: ecosystem.DataSourceConfigMap, Name: "configmap", Volume: "volume", - Subfolder: "subfolder", + Subfolder: subfolder, }, { SourceType: ecosystem.DataSourceSecret, Name: "secret", Volume: "secvolume", - Subfolder: "secsubfolder", + Subfolder: subfolder2, }, }, }, @@ -301,8 +383,14 @@ func Test_toDoguCR(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "postgresql", Labels: map[string]string{ - "app": "ces", - "dogu.name": "postgresql", + "app": "ces", + "k8s.cloudogu.com/app": "ces", + "dogu.name": "postgresql", + "k8s.cloudogu.com/dogu.name": "postgresql", + "app.kubernetes.io/name": "postgresql", + "app.kubernetes.io/version": version3214.Raw, + "app.kubernetes.io/part-of": "ces", + "app.kubernetes.io/managed-by": "k8s-blueprint-operator", }, }, Spec: v2.DoguSpec{ @@ -331,20 +419,26 @@ func Test_toDoguCR(t *testing.T) { dogu: &ecosystem.DoguInstallation{ Name: postgresDoguName, Version: version3214, - Status: ecosystem.DoguStatusInstalled, + Status: "installed", Health: ecosystem.AvailableHealthStatus, UpgradeConfig: ecosystem.UpgradeConfig{ AllowNamespaceSwitch: true, }, - MinVolumeSize: volSize25G, + MinVolumeSize: &volSize25G, }, want: &v2.Dogu{ TypeMeta: metav1.TypeMeta{}, ObjectMeta: metav1.ObjectMeta{ Name: "postgresql", Labels: map[string]string{ - "app": "ces", - "dogu.name": "postgresql", + "app": "ces", + "k8s.cloudogu.com/app": "ces", + "dogu.name": "postgresql", + "k8s.cloudogu.com/dogu.name": "postgresql", + "app.kubernetes.io/name": "postgresql", + "app.kubernetes.io/version": version3214.Raw, + "app.kubernetes.io/part-of": "ces", + "app.kubernetes.io/managed-by": "k8s-blueprint-operator", }, }, Spec: v2.DoguSpec{ @@ -383,11 +477,12 @@ func Test_toDoguCRPatch(t *testing.T) { dogu: &ecosystem.DoguInstallation{ Name: postgresDoguName, Version: version3214, - Status: ecosystem.DoguStatusInstalled, + Status: "installed", Health: ecosystem.AvailableHealthStatus, UpgradeConfig: ecosystem.UpgradeConfig{ AllowNamespaceSwitch: true, }, + PauseReconciliation: true, }, want: &doguCRPatch{ Spec: doguSpecPatch{ @@ -396,6 +491,7 @@ func Test_toDoguCRPatch(t *testing.T) { UpgradeConfig: upgradeConfigPatch{ AllowNamespaceSwitch: true, }, + PauseReconciliation: true, }, }, }, @@ -418,22 +514,26 @@ func Test_toDoguCRPatchBytes(t *testing.T) { wantErr assert.ErrorAssertionFunc }{ { - // TODO check ReverseProxy name: "ok", dogu: &ecosystem.DoguInstallation{ Name: postgresDoguName, Version: version3214, - Status: ecosystem.DoguStatusInstalled, + Status: "installed", Health: ecosystem.AvailableHealthStatus, UpgradeConfig: ecosystem.UpgradeConfig{ AllowNamespaceSwitch: true, }, - MinVolumeSize: quantity2, + MinVolumeSize: &quantity2, + ReverseProxyConfig: ecosystem.ReverseProxyConfig{ + MaxBodySize: &proxyBodySize, + RewriteTarget: ecosystem.RewriteTarget(rewriteTarget), + AdditionalConfig: ecosystem.AdditionalConfig(additionalConfig), + }, AdditionalMounts: []ecosystem.AdditionalMount{ - {SourceType: ecosystem.DataSourceConfigMap, Name: "test", Volume: "volume", Subfolder: "subfolder"}, + {SourceType: ecosystem.DataSourceConfigMap, Name: "test", Volume: "volume", Subfolder: subfolder}, }, }, - want: "{\"spec\":{\"name\":\"official/postgresql\",\"version\":\"3.2.1-4\",\"resources\":{\"dataVolumeSize\":\"\",\"minDataVolumeSize\":\"2Gi\"},\"supportMode\":false,\"upgradeConfig\":{\"allowNamespaceSwitch\":true,\"forceUpgrade\":false},\"additionalIngressAnnotations\":null,\"additionalMounts\":[{\"sourceType\":\"ConfigMap\",\"name\":\"test\",\"volume\":\"volume\",\"subfolder\":\"subfolder\"}]}}", + want: "{\"spec\":{\"name\":\"official/postgresql\",\"version\":\"3.2.1-4\",\"resources\":{\"dataVolumeSize\":\"\",\"minDataVolumeSize\":\"2Gi\"},\"supportMode\":false,\"pauseReconciliation\":false,\"upgradeConfig\":{\"allowNamespaceSwitch\":true,\"forceUpgrade\":false},\"additionalIngressAnnotations\":{\"nginx.ingress.kubernetes.io/configuration-snippet\":\"additional\",\"nginx.ingress.kubernetes.io/proxy-body-size\":\"1G\",\"nginx.ingress.kubernetes.io/rewrite-target\":\"/\"},\"additionalMounts\":[{\"sourceType\":\"ConfigMap\",\"name\":\"test\",\"volume\":\"volume\",\"subfolder\":\"subfolder\"}]}}", wantErr: assert.NoError, }, { @@ -441,16 +541,16 @@ func Test_toDoguCRPatchBytes(t *testing.T) { dogu: &ecosystem.DoguInstallation{ Name: postgresDoguName, Version: version3214, - Status: ecosystem.DoguStatusInstalled, + Status: "installed", Health: ecosystem.AvailableHealthStatus, UpgradeConfig: ecosystem.UpgradeConfig{ AllowNamespaceSwitch: true, }, AdditionalMounts: []ecosystem.AdditionalMount{ - {SourceType: ecosystem.DataSourceConfigMap, Name: "test", Volume: "volume", Subfolder: "subfolder"}, + {SourceType: ecosystem.DataSourceConfigMap, Name: "test", Volume: "volume", Subfolder: subfolder}, }, }, - want: "{\"spec\":{\"name\":\"official/postgresql\",\"version\":\"3.2.1-4\",\"resources\":{\"dataVolumeSize\":\"\",\"minDataVolumeSize\":\"0\"},\"supportMode\":false,\"upgradeConfig\":{\"allowNamespaceSwitch\":true,\"forceUpgrade\":false},\"additionalIngressAnnotations\":null,\"additionalMounts\":[{\"sourceType\":\"ConfigMap\",\"name\":\"test\",\"volume\":\"volume\",\"subfolder\":\"subfolder\"}]}}", + want: "{\"spec\":{\"name\":\"official/postgresql\",\"version\":\"3.2.1-4\",\"resources\":{\"dataVolumeSize\":\"\",\"minDataVolumeSize\":\"0\"},\"supportMode\":false,\"pauseReconciliation\":false,\"upgradeConfig\":{\"allowNamespaceSwitch\":true,\"forceUpgrade\":false},\"additionalIngressAnnotations\":null,\"additionalMounts\":[{\"sourceType\":\"ConfigMap\",\"name\":\"test\",\"volume\":\"volume\",\"subfolder\":\"subfolder\"}]}}", wantErr: assert.NoError, }, } @@ -510,13 +610,13 @@ func Test_parseDoguAdditionalIngressAnnotationsCR(t *testing.T) { annotations: v2.IngressAnnotations{ "nginx.ingress.kubernetes.io/proxy-body-size": "1G", "nginx.ingress.kubernetes.io/rewrite-target": "/", - "nginx.ingress.kubernetes.io/configuration-snippet": "snippet", + "nginx.ingress.kubernetes.io/configuration-snippet": "additional", }, }, want: ecosystem.ReverseProxyConfig{ MaxBodySize: &quantity1, - RewriteTarget: "/", - AdditionalConfig: "snippet", + RewriteTarget: ecosystem.RewriteTarget(rewriteTarget), + AdditionalConfig: ecosystem.AdditionalConfig(additionalConfig), }, wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { return err == nil @@ -562,8 +662,8 @@ func Test_getNginxIngressAnnotations1(t *testing.T) { name: "should parse config", args: args{config: ecosystem.ReverseProxyConfig{ MaxBodySize: &quantity, - RewriteTarget: "/", - AdditionalConfig: "additional", + RewriteTarget: ecosystem.RewriteTarget(rewriteTarget), + AdditionalConfig: ecosystem.AdditionalConfig(additionalConfig), }}, want: map[string]string{ "nginx.ingress.kubernetes.io/proxy-body-size": "1M", diff --git a/pkg/adapter/kubernetes/healthConfig/health.go b/pkg/adapter/kubernetes/healthConfig/health.go deleted file mode 100644 index 61ad81c5..00000000 --- a/pkg/adapter/kubernetes/healthConfig/health.go +++ /dev/null @@ -1,135 +0,0 @@ -package healthconfig - -import ( - "context" - "errors" - "fmt" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/common" - "time" - - "k8s.io/api/core/v1" - k8sErr "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/yaml" - corev1 "k8s.io/client-go/kubernetes/typed/core/v1" - - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domainservice" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/util" -) - -const ( - healthConfigMapName = "k8s-blueprint-operator-health-config" - componentHealthConfigKey = "components" - waitHealthConfigKey = "wait" -) - -var defaultHealthConfig = healthConfig{ - Components: componentHealthConfig{ - Required: nil, - }, - Wait: waitHealthConfig{ - Timeout: duration{10 * time.Minute}, - Interval: duration{10 * time.Second}, - }, -} - -type HealthConfigProvider struct { - cmClient configMapInterface -} - -func NewHealthConfigProvider(cmClient corev1.ConfigMapInterface) *HealthConfigProvider { - return &HealthConfigProvider{cmClient: cmClient} -} - -func (h *HealthConfigProvider) GetWaitConfig(ctx context.Context) (ecosystem.WaitConfig, error) { - config, err := h.getAll(ctx) - if err != nil { - return ecosystem.WaitConfig{}, err - } - - return convertToWaitConfigDomain(config.Wait), nil -} - -func convertToWaitConfigDomain(config waitHealthConfig) ecosystem.WaitConfig { - return ecosystem.WaitConfig{ - Timeout: config.Timeout.Duration, - Interval: config.Interval.Duration, - } -} - -func (h *HealthConfigProvider) GetRequiredComponents(ctx context.Context) ([]ecosystem.RequiredComponent, error) { - config, err := h.getAll(ctx) - if err != nil { - return nil, err - } - - return util.Map(config.Components.Required, convertToRequiredComponentDomain), nil -} - -func convertToRequiredComponentDomain(component requiredComponent) ecosystem.RequiredComponent { - return ecosystem.RequiredComponent{Name: common.SimpleComponentName(component.Name)} -} - -func (h *HealthConfigProvider) getAll(ctx context.Context) (healthConfig, error) { - configMap, err := h.cmClient.Get(ctx, healthConfigMapName, metav1.GetOptions{}) - if k8sErr.IsNotFound(err) { - return defaultHealthConfig, nil - } else if err != nil { - return healthConfig{}, &domainservice.InternalError{ - WrappedError: err, - Message: fmt.Sprintf("failed to get config map %q", healthConfigMapName), - } - } - - components, componentsErr := parseComponentConfig(configMap) - wait, waitErr := parseWaitConfig(configMap) - - return healthConfig{ - Components: components, - Wait: wait, - }, errors.Join(componentsErr, waitErr) -} - -func parseWaitConfig(configMap *v1.ConfigMap) (waitHealthConfig, error) { - waitConfigStr, exists := configMap.Data[waitHealthConfigKey] - if !exists { - return defaultHealthConfig.Wait, nil - } - - var wait waitHealthConfig - err := yaml.Unmarshal([]byte(waitConfigStr), &wait) - if err != nil { - return waitHealthConfig{}, &domainservice.InternalError{ - WrappedError: err, - Message: "failed to parse wait health config", - } - } - - if wait.Interval.Duration == 0 { - wait.Interval = defaultHealthConfig.Wait.Interval - } - - if wait.Timeout.Duration == 0 { - wait.Timeout = defaultHealthConfig.Wait.Timeout - } - - return wait, nil -} - -func parseComponentConfig(configMap *v1.ConfigMap) (componentHealthConfig, error) { - componentHealthConfigStr, exists := configMap.Data[componentHealthConfigKey] - if !exists { - return defaultHealthConfig.Components, nil - } - - var components componentHealthConfig - err := yaml.Unmarshal([]byte(componentHealthConfigStr), &components) - if err != nil { - return componentHealthConfig{}, &domainservice.InternalError{ - WrappedError: err, - Message: "failed to parse component health config", - } - } - return components, nil -} diff --git a/pkg/adapter/kubernetes/healthConfig/healthSerializer.go b/pkg/adapter/kubernetes/healthConfig/healthSerializer.go deleted file mode 100644 index 1446453f..00000000 --- a/pkg/adapter/kubernetes/healthConfig/healthSerializer.go +++ /dev/null @@ -1,58 +0,0 @@ -package healthconfig - -import ( - "encoding/json" - "fmt" - "time" -) - -type healthConfig struct { - // Components contains configuration concerning the health-check of components. - Components componentHealthConfig `yaml:"components,omitempty" json:"components,omitempty"` - // Wait contains configuration concerning how the stand-by-period for the ecosystem to become healthy. - Wait waitHealthConfig `yaml:"wait,omitempty" json:"wait,omitempty"` -} - -type componentHealthConfig struct { - // Required is a list of components that have to be installed for the health-check to succeed. - Required []requiredComponent `yaml:"required,omitempty" json:"required,omitempty"` -} - -type requiredComponent struct { - // Name identifies the component. - Name string `yaml:"name" json:"name"` -} - -type waitHealthConfig struct { - // Timeout is the maximum time to wait for the ecosystem to become healthy. - Timeout duration `yaml:"timeout,omitempty" json:"timeout,omitempty"` - // Interval is the time to wait between health checks. - Interval duration `yaml:"interval,omitempty" json:"interval,omitempty"` -} - -// duration is a wrapper for time.Duration so that it can be marshalled and unmarshalled to JSON -// using time.Duration's string formatting. -type duration struct { - time.Duration -} - -// UnmarshalJSON expects either a JSON string formatted to be parseable by time.ParseDuration or -// a JSON integer number that represents the duration in nanoseconds. -func (d *duration) UnmarshalJSON(b []byte) (err error) { - if b[0] == '"' { - sd := string(b[1 : len(b)-1]) - d.Duration, err = time.ParseDuration(sd) - return err - } - - var id int64 - id, err = json.Number(b).Int64() - d.Duration = time.Duration(id) - - return err -} - -// MarshalJSON returns the duration as a JSON string formatted with time.Duration's string formatting -func (d *duration) MarshalJSON() ([]byte, error) { - return []byte(fmt.Sprintf(`"%s"`, d.String())), nil -} diff --git a/pkg/adapter/kubernetes/healthConfig/health_test.go b/pkg/adapter/kubernetes/healthConfig/health_test.go deleted file mode 100644 index 6bd468d0..00000000 --- a/pkg/adapter/kubernetes/healthConfig/health_test.go +++ /dev/null @@ -1,276 +0,0 @@ -package healthconfig - -import ( - "context" - "math/rand" - "testing" - "time" - - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" - - "github.com/stretchr/testify/assert" - - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" -) - -var testCtx = context.Background() - -func TestHealthConfigRepository_GetRequiredComponents(t *testing.T) { - notFoundErr := errors.NewNotFound(schema.GroupResource{ - Group: "v1", - Resource: "ConfigMap", - }, "k8s-blueprint-operator-health-config") - tests := []struct { - name string - cmClientFn func(t *testing.T) configMapInterface - want []ecosystem.RequiredComponent - wantErr assert.ErrorAssertionFunc - }{ - { - name: "should return default on not found error", - cmClientFn: func(t *testing.T) configMapInterface { - cmMock := newMockConfigMapInterface(t) - cmMock.EXPECT().Get(testCtx, "k8s-blueprint-operator-health-config", metav1.GetOptions{}). - Return(nil, notFoundErr) - return cmMock - }, - want: make([]ecosystem.RequiredComponent, 0), - wantErr: assert.NoError, - }, - { - name: "should fail to get configmap due to other error", - cmClientFn: func(t *testing.T) configMapInterface { - cmMock := newMockConfigMapInterface(t) - cmMock.EXPECT().Get(testCtx, "k8s-blueprint-operator-health-config", metav1.GetOptions{}). - Return(nil, assert.AnError) - return cmMock - }, - want: nil, - wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { - return assert.ErrorIs(t, err, assert.AnError, i) && - assert.ErrorContains(t, err, "failed to get config map \"k8s-blueprint-operator-health-config\"", i) - }, - }, - { - name: "should return default if config key does not exist", - cmClientFn: func(t *testing.T) configMapInterface { - cmMock := newMockConfigMapInterface(t) - configMap := &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{Name: "k8s-blueprint-operator-health-config"}, - Data: map[string]string{}, - } - cmMock.EXPECT().Get(testCtx, "k8s-blueprint-operator-health-config", metav1.GetOptions{}). - Return(configMap, nil) - return cmMock - }, - want: []ecosystem.RequiredComponent{}, - wantErr: assert.NoError, - }, - { - name: "should fail to parse component config", - cmClientFn: func(t *testing.T) configMapInterface { - cmMock := newMockConfigMapInterface(t) - configMap := &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{Name: "k8s-blueprint-operator-health-config"}, - Data: map[string]string{"components": `{"required": [{]}`}, - } - cmMock.EXPECT().Get(testCtx, "k8s-blueprint-operator-health-config", metav1.GetOptions{}). - Return(configMap, nil) - return cmMock - }, - want: nil, - wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { - return assert.ErrorContains(t, err, "failed to parse component health config", i) - }, - }, - { - name: "should succeed", - cmClientFn: func(t *testing.T) configMapInterface { - cmMock := newMockConfigMapInterface(t) - configMap := &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{Name: "k8s-blueprint-operator-health-config"}, - Data: map[string]string{"components": `{"required": [{"name": "k8s-dogu-operator"}, {"name": "k8s-etcd"}]}`}, - } - cmMock.EXPECT().Get(testCtx, "k8s-blueprint-operator-health-config", metav1.GetOptions{}). - Return(configMap, nil) - return cmMock - }, - want: []ecosystem.RequiredComponent{{Name: "k8s-dogu-operator"}, {Name: "k8s-etcd"}}, - wantErr: assert.NoError, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - h := &HealthConfigProvider{ - cmClient: tt.cmClientFn(t), - } - got, err := h.GetRequiredComponents(testCtx) - tt.wantErr(t, err) - assert.Equal(t, tt.want, got) - }) - } -} - -func TestHealthConfigRepository_GetWaitConfig(t *testing.T) { - notFoundErr := errors.NewNotFound(schema.GroupResource{ - Group: "v1", - Resource: "ConfigMap", - }, "k8s-blueprint-operator-health-config") - tests := []struct { - name string - cmClientFn func(t *testing.T) configMapInterface - want ecosystem.WaitConfig - wantErr assert.ErrorAssertionFunc - }{ - { - name: "should return default on not found error", - cmClientFn: func(t *testing.T) configMapInterface { - cmMock := newMockConfigMapInterface(t) - cmMock.EXPECT().Get(testCtx, "k8s-blueprint-operator-health-config", metav1.GetOptions{}). - Return(nil, notFoundErr) - return cmMock - }, - want: ecosystem.WaitConfig{ - Timeout: 10 * time.Minute, - Interval: 10 * time.Second, - }, - wantErr: assert.NoError, - }, - { - name: "should fail to get config map due to other error", - cmClientFn: func(t *testing.T) configMapInterface { - cmMock := newMockConfigMapInterface(t) - cmMock.EXPECT().Get(testCtx, "k8s-blueprint-operator-health-config", metav1.GetOptions{}). - Return(nil, assert.AnError) - return cmMock - }, - want: ecosystem.WaitConfig{}, - wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { - return assert.ErrorIs(t, err, assert.AnError, i) && - assert.ErrorContains(t, err, "failed to get config map \"k8s-blueprint-operator-health-config\"", i) - }, - }, - { - name: "should return default if config key does not exist", - cmClientFn: func(t *testing.T) configMapInterface { - cmMock := newMockConfigMapInterface(t) - configMap := &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{Name: "k8s-blueprint-operator-health-config"}, - Data: map[string]string{}, - } - cmMock.EXPECT().Get(testCtx, "k8s-blueprint-operator-health-config", metav1.GetOptions{}). - Return(configMap, nil) - return cmMock - }, - want: ecosystem.WaitConfig{ - Timeout: 10 * time.Minute, - Interval: 10 * time.Second, - }, - wantErr: assert.NoError, - }, - { - name: "should fail to parse wait config", - cmClientFn: func(t *testing.T) configMapInterface { - cmMock := newMockConfigMapInterface(t) - configMap := &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{Name: "k8s-blueprint-operator-health-config"}, - Data: map[string]string{"wait": `{"timeout": {[[{{}`}, - } - cmMock.EXPECT().Get(testCtx, "k8s-blueprint-operator-health-config", metav1.GetOptions{}). - Return(configMap, nil) - return cmMock - }, - want: ecosystem.WaitConfig{}, - wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { - return assert.ErrorContains(t, err, "failed to parse wait health config", i) - }, - }, - { - name: "should return default timeout if empty", - cmClientFn: func(t *testing.T) configMapInterface { - cmMock := newMockConfigMapInterface(t) - configMap := &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{Name: "k8s-blueprint-operator-health-config"}, - Data: map[string]string{"wait": `{"interval": 2}`}, - } - cmMock.EXPECT().Get(testCtx, "k8s-blueprint-operator-health-config", metav1.GetOptions{}). - Return(configMap, nil) - return cmMock - }, - want: ecosystem.WaitConfig{ - Timeout: 10 * time.Minute, - Interval: 2, - }, - wantErr: assert.NoError, - }, - { - name: "should return default interval if empty", - cmClientFn: func(t *testing.T) configMapInterface { - cmMock := newMockConfigMapInterface(t) - configMap := &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{Name: "k8s-blueprint-operator-health-config"}, - Data: map[string]string{"wait": `{"timeout": "50s"}`}, - } - cmMock.EXPECT().Get(testCtx, "k8s-blueprint-operator-health-config", metav1.GetOptions{}). - Return(configMap, nil) - return cmMock - }, - want: ecosystem.WaitConfig{ - Timeout: 50 * time.Second, - Interval: 10 * time.Second, - }, - wantErr: assert.NoError, - }, - { - name: "should succeed", - cmClientFn: func(t *testing.T) configMapInterface { - cmMock := newMockConfigMapInterface(t) - configMap := &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{Name: "k8s-blueprint-operator-health-config"}, - Data: map[string]string{"wait": `{"timeout": "120s", "interval": "13s"}`}, - } - cmMock.EXPECT().Get(testCtx, "k8s-blueprint-operator-health-config", metav1.GetOptions{}). - Return(configMap, nil) - return cmMock - }, - want: ecosystem.WaitConfig{ - Timeout: 120 * time.Second, - Interval: 13 * time.Second, - }, - wantErr: assert.NoError, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - h := &HealthConfigProvider{ - cmClient: tt.cmClientFn(t), - } - got, err := h.GetWaitConfig(testCtx) - tt.wantErr(t, err) - assert.Equal(t, tt.want, got) - }) - } -} - -func TestNewHealthConfigRepository(t *testing.T) { - mock := newMockConfigMapInterface(t) - provider := NewHealthConfigProvider(mock) - assert.Same(t, mock, provider.cmClient) -} - -func Test_duration_Marshal_Unmarshal_JSON(t *testing.T) { - // we provide a seed to produce always the exact same sequence of values - rng := rand.New(rand.NewSource(42)) - for i := 0; i < 100; i++ { - d1 := duration{time.Duration(rng.Int63())} - json, err := d1.MarshalJSON() - assert.NoErrorf(t, err, "failed to marshal duration %q to json", d1) - - var d2 duration - err = d2.UnmarshalJSON(json) - assert.NoErrorf(t, err, "failed to unmarshal json %q (original: %q)", string(json), d1) - } -} diff --git a/pkg/adapter/kubernetes/healthConfig/interfaces.go b/pkg/adapter/kubernetes/healthConfig/interfaces.go deleted file mode 100644 index f00b3266..00000000 --- a/pkg/adapter/kubernetes/healthConfig/interfaces.go +++ /dev/null @@ -1,7 +0,0 @@ -package healthconfig - -import corev1 "k8s.io/client-go/kubernetes/typed/core/v1" - -type configMapInterface interface { - corev1.ConfigMapInterface -} diff --git a/pkg/adapter/kubernetes/healthConfig/mock_configMapInterface_test.go b/pkg/adapter/kubernetes/healthConfig/mock_configMapInterface_test.go deleted file mode 100644 index 3bcde17b..00000000 --- a/pkg/adapter/kubernetes/healthConfig/mock_configMapInterface_test.go +++ /dev/null @@ -1,577 +0,0 @@ -// Code generated by mockery v2.53.3. DO NOT EDIT. - -package healthconfig - -import ( - context "context" - - corev1 "k8s.io/api/core/v1" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - mock "github.com/stretchr/testify/mock" - - types "k8s.io/apimachinery/pkg/types" - - v1 "k8s.io/client-go/applyconfigurations/core/v1" - - watch "k8s.io/apimachinery/pkg/watch" -) - -// mockConfigMapInterface is an autogenerated mock type for the configMapInterface type -type mockConfigMapInterface struct { - mock.Mock -} - -type mockConfigMapInterface_Expecter struct { - mock *mock.Mock -} - -func (_m *mockConfigMapInterface) EXPECT() *mockConfigMapInterface_Expecter { - return &mockConfigMapInterface_Expecter{mock: &_m.Mock} -} - -// Apply provides a mock function with given fields: ctx, configMap, opts -func (_m *mockConfigMapInterface) Apply(ctx context.Context, configMap *v1.ConfigMapApplyConfiguration, opts metav1.ApplyOptions) (*corev1.ConfigMap, error) { - ret := _m.Called(ctx, configMap, opts) - - if len(ret) == 0 { - panic("no return value specified for Apply") - } - - var r0 *corev1.ConfigMap - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *v1.ConfigMapApplyConfiguration, metav1.ApplyOptions) (*corev1.ConfigMap, error)); ok { - return rf(ctx, configMap, opts) - } - if rf, ok := ret.Get(0).(func(context.Context, *v1.ConfigMapApplyConfiguration, metav1.ApplyOptions) *corev1.ConfigMap); ok { - r0 = rf(ctx, configMap, opts) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*corev1.ConfigMap) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, *v1.ConfigMapApplyConfiguration, metav1.ApplyOptions) error); ok { - r1 = rf(ctx, configMap, opts) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// mockConfigMapInterface_Apply_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Apply' -type mockConfigMapInterface_Apply_Call struct { - *mock.Call -} - -// Apply is a helper method to define mock.On call -// - ctx context.Context -// - configMap *v1.ConfigMapApplyConfiguration -// - opts metav1.ApplyOptions -func (_e *mockConfigMapInterface_Expecter) Apply(ctx interface{}, configMap interface{}, opts interface{}) *mockConfigMapInterface_Apply_Call { - return &mockConfigMapInterface_Apply_Call{Call: _e.mock.On("Apply", ctx, configMap, opts)} -} - -func (_c *mockConfigMapInterface_Apply_Call) Run(run func(ctx context.Context, configMap *v1.ConfigMapApplyConfiguration, opts metav1.ApplyOptions)) *mockConfigMapInterface_Apply_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(*v1.ConfigMapApplyConfiguration), args[2].(metav1.ApplyOptions)) - }) - return _c -} - -func (_c *mockConfigMapInterface_Apply_Call) Return(result *corev1.ConfigMap, err error) *mockConfigMapInterface_Apply_Call { - _c.Call.Return(result, err) - return _c -} - -func (_c *mockConfigMapInterface_Apply_Call) RunAndReturn(run func(context.Context, *v1.ConfigMapApplyConfiguration, metav1.ApplyOptions) (*corev1.ConfigMap, error)) *mockConfigMapInterface_Apply_Call { - _c.Call.Return(run) - return _c -} - -// Create provides a mock function with given fields: ctx, configMap, opts -func (_m *mockConfigMapInterface) Create(ctx context.Context, configMap *corev1.ConfigMap, opts metav1.CreateOptions) (*corev1.ConfigMap, error) { - ret := _m.Called(ctx, configMap, opts) - - if len(ret) == 0 { - panic("no return value specified for Create") - } - - var r0 *corev1.ConfigMap - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *corev1.ConfigMap, metav1.CreateOptions) (*corev1.ConfigMap, error)); ok { - return rf(ctx, configMap, opts) - } - if rf, ok := ret.Get(0).(func(context.Context, *corev1.ConfigMap, metav1.CreateOptions) *corev1.ConfigMap); ok { - r0 = rf(ctx, configMap, opts) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*corev1.ConfigMap) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, *corev1.ConfigMap, metav1.CreateOptions) error); ok { - r1 = rf(ctx, configMap, opts) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// mockConfigMapInterface_Create_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Create' -type mockConfigMapInterface_Create_Call struct { - *mock.Call -} - -// Create is a helper method to define mock.On call -// - ctx context.Context -// - configMap *corev1.ConfigMap -// - opts metav1.CreateOptions -func (_e *mockConfigMapInterface_Expecter) Create(ctx interface{}, configMap interface{}, opts interface{}) *mockConfigMapInterface_Create_Call { - return &mockConfigMapInterface_Create_Call{Call: _e.mock.On("Create", ctx, configMap, opts)} -} - -func (_c *mockConfigMapInterface_Create_Call) Run(run func(ctx context.Context, configMap *corev1.ConfigMap, opts metav1.CreateOptions)) *mockConfigMapInterface_Create_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(*corev1.ConfigMap), args[2].(metav1.CreateOptions)) - }) - return _c -} - -func (_c *mockConfigMapInterface_Create_Call) Return(_a0 *corev1.ConfigMap, _a1 error) *mockConfigMapInterface_Create_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *mockConfigMapInterface_Create_Call) RunAndReturn(run func(context.Context, *corev1.ConfigMap, metav1.CreateOptions) (*corev1.ConfigMap, error)) *mockConfigMapInterface_Create_Call { - _c.Call.Return(run) - return _c -} - -// Delete provides a mock function with given fields: ctx, name, opts -func (_m *mockConfigMapInterface) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error { - ret := _m.Called(ctx, name, opts) - - if len(ret) == 0 { - panic("no return value specified for Delete") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string, metav1.DeleteOptions) error); ok { - r0 = rf(ctx, name, opts) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// mockConfigMapInterface_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete' -type mockConfigMapInterface_Delete_Call struct { - *mock.Call -} - -// Delete is a helper method to define mock.On call -// - ctx context.Context -// - name string -// - opts metav1.DeleteOptions -func (_e *mockConfigMapInterface_Expecter) Delete(ctx interface{}, name interface{}, opts interface{}) *mockConfigMapInterface_Delete_Call { - return &mockConfigMapInterface_Delete_Call{Call: _e.mock.On("Delete", ctx, name, opts)} -} - -func (_c *mockConfigMapInterface_Delete_Call) Run(run func(ctx context.Context, name string, opts metav1.DeleteOptions)) *mockConfigMapInterface_Delete_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string), args[2].(metav1.DeleteOptions)) - }) - return _c -} - -func (_c *mockConfigMapInterface_Delete_Call) Return(_a0 error) *mockConfigMapInterface_Delete_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *mockConfigMapInterface_Delete_Call) RunAndReturn(run func(context.Context, string, metav1.DeleteOptions) error) *mockConfigMapInterface_Delete_Call { - _c.Call.Return(run) - return _c -} - -// DeleteCollection provides a mock function with given fields: ctx, opts, listOpts -func (_m *mockConfigMapInterface) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error { - ret := _m.Called(ctx, opts, listOpts) - - if len(ret) == 0 { - panic("no return value specified for DeleteCollection") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, metav1.DeleteOptions, metav1.ListOptions) error); ok { - r0 = rf(ctx, opts, listOpts) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// mockConfigMapInterface_DeleteCollection_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteCollection' -type mockConfigMapInterface_DeleteCollection_Call struct { - *mock.Call -} - -// DeleteCollection is a helper method to define mock.On call -// - ctx context.Context -// - opts metav1.DeleteOptions -// - listOpts metav1.ListOptions -func (_e *mockConfigMapInterface_Expecter) DeleteCollection(ctx interface{}, opts interface{}, listOpts interface{}) *mockConfigMapInterface_DeleteCollection_Call { - return &mockConfigMapInterface_DeleteCollection_Call{Call: _e.mock.On("DeleteCollection", ctx, opts, listOpts)} -} - -func (_c *mockConfigMapInterface_DeleteCollection_Call) Run(run func(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions)) *mockConfigMapInterface_DeleteCollection_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(metav1.DeleteOptions), args[2].(metav1.ListOptions)) - }) - return _c -} - -func (_c *mockConfigMapInterface_DeleteCollection_Call) Return(_a0 error) *mockConfigMapInterface_DeleteCollection_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *mockConfigMapInterface_DeleteCollection_Call) RunAndReturn(run func(context.Context, metav1.DeleteOptions, metav1.ListOptions) error) *mockConfigMapInterface_DeleteCollection_Call { - _c.Call.Return(run) - return _c -} - -// Get provides a mock function with given fields: ctx, name, opts -func (_m *mockConfigMapInterface) Get(ctx context.Context, name string, opts metav1.GetOptions) (*corev1.ConfigMap, error) { - ret := _m.Called(ctx, name, opts) - - if len(ret) == 0 { - panic("no return value specified for Get") - } - - var r0 *corev1.ConfigMap - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, metav1.GetOptions) (*corev1.ConfigMap, error)); ok { - return rf(ctx, name, opts) - } - if rf, ok := ret.Get(0).(func(context.Context, string, metav1.GetOptions) *corev1.ConfigMap); ok { - r0 = rf(ctx, name, opts) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*corev1.ConfigMap) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string, metav1.GetOptions) error); ok { - r1 = rf(ctx, name, opts) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// mockConfigMapInterface_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get' -type mockConfigMapInterface_Get_Call struct { - *mock.Call -} - -// Get is a helper method to define mock.On call -// - ctx context.Context -// - name string -// - opts metav1.GetOptions -func (_e *mockConfigMapInterface_Expecter) Get(ctx interface{}, name interface{}, opts interface{}) *mockConfigMapInterface_Get_Call { - return &mockConfigMapInterface_Get_Call{Call: _e.mock.On("Get", ctx, name, opts)} -} - -func (_c *mockConfigMapInterface_Get_Call) Run(run func(ctx context.Context, name string, opts metav1.GetOptions)) *mockConfigMapInterface_Get_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string), args[2].(metav1.GetOptions)) - }) - return _c -} - -func (_c *mockConfigMapInterface_Get_Call) Return(_a0 *corev1.ConfigMap, _a1 error) *mockConfigMapInterface_Get_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *mockConfigMapInterface_Get_Call) RunAndReturn(run func(context.Context, string, metav1.GetOptions) (*corev1.ConfigMap, error)) *mockConfigMapInterface_Get_Call { - _c.Call.Return(run) - return _c -} - -// List provides a mock function with given fields: ctx, opts -func (_m *mockConfigMapInterface) List(ctx context.Context, opts metav1.ListOptions) (*corev1.ConfigMapList, error) { - ret := _m.Called(ctx, opts) - - if len(ret) == 0 { - panic("no return value specified for List") - } - - var r0 *corev1.ConfigMapList - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, metav1.ListOptions) (*corev1.ConfigMapList, error)); ok { - return rf(ctx, opts) - } - if rf, ok := ret.Get(0).(func(context.Context, metav1.ListOptions) *corev1.ConfigMapList); ok { - r0 = rf(ctx, opts) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*corev1.ConfigMapList) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, metav1.ListOptions) error); ok { - r1 = rf(ctx, opts) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// mockConfigMapInterface_List_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'List' -type mockConfigMapInterface_List_Call struct { - *mock.Call -} - -// List is a helper method to define mock.On call -// - ctx context.Context -// - opts metav1.ListOptions -func (_e *mockConfigMapInterface_Expecter) List(ctx interface{}, opts interface{}) *mockConfigMapInterface_List_Call { - return &mockConfigMapInterface_List_Call{Call: _e.mock.On("List", ctx, opts)} -} - -func (_c *mockConfigMapInterface_List_Call) Run(run func(ctx context.Context, opts metav1.ListOptions)) *mockConfigMapInterface_List_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(metav1.ListOptions)) - }) - return _c -} - -func (_c *mockConfigMapInterface_List_Call) Return(_a0 *corev1.ConfigMapList, _a1 error) *mockConfigMapInterface_List_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *mockConfigMapInterface_List_Call) RunAndReturn(run func(context.Context, metav1.ListOptions) (*corev1.ConfigMapList, error)) *mockConfigMapInterface_List_Call { - _c.Call.Return(run) - return _c -} - -// Patch provides a mock function with given fields: ctx, name, pt, data, opts, subresources -func (_m *mockConfigMapInterface) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (*corev1.ConfigMap, error) { - _va := make([]interface{}, len(subresources)) - for _i := range subresources { - _va[_i] = subresources[_i] - } - var _ca []interface{} - _ca = append(_ca, ctx, name, pt, data, opts) - _ca = append(_ca, _va...) - ret := _m.Called(_ca...) - - if len(ret) == 0 { - panic("no return value specified for Patch") - } - - var r0 *corev1.ConfigMap - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, types.PatchType, []byte, metav1.PatchOptions, ...string) (*corev1.ConfigMap, error)); ok { - return rf(ctx, name, pt, data, opts, subresources...) - } - if rf, ok := ret.Get(0).(func(context.Context, string, types.PatchType, []byte, metav1.PatchOptions, ...string) *corev1.ConfigMap); ok { - r0 = rf(ctx, name, pt, data, opts, subresources...) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*corev1.ConfigMap) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string, types.PatchType, []byte, metav1.PatchOptions, ...string) error); ok { - r1 = rf(ctx, name, pt, data, opts, subresources...) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// mockConfigMapInterface_Patch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Patch' -type mockConfigMapInterface_Patch_Call struct { - *mock.Call -} - -// Patch is a helper method to define mock.On call -// - ctx context.Context -// - name string -// - pt types.PatchType -// - data []byte -// - opts metav1.PatchOptions -// - subresources ...string -func (_e *mockConfigMapInterface_Expecter) Patch(ctx interface{}, name interface{}, pt interface{}, data interface{}, opts interface{}, subresources ...interface{}) *mockConfigMapInterface_Patch_Call { - return &mockConfigMapInterface_Patch_Call{Call: _e.mock.On("Patch", - append([]interface{}{ctx, name, pt, data, opts}, subresources...)...)} -} - -func (_c *mockConfigMapInterface_Patch_Call) Run(run func(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string)) *mockConfigMapInterface_Patch_Call { - _c.Call.Run(func(args mock.Arguments) { - variadicArgs := make([]string, len(args)-5) - for i, a := range args[5:] { - if a != nil { - variadicArgs[i] = a.(string) - } - } - run(args[0].(context.Context), args[1].(string), args[2].(types.PatchType), args[3].([]byte), args[4].(metav1.PatchOptions), variadicArgs...) - }) - return _c -} - -func (_c *mockConfigMapInterface_Patch_Call) Return(result *corev1.ConfigMap, err error) *mockConfigMapInterface_Patch_Call { - _c.Call.Return(result, err) - return _c -} - -func (_c *mockConfigMapInterface_Patch_Call) RunAndReturn(run func(context.Context, string, types.PatchType, []byte, metav1.PatchOptions, ...string) (*corev1.ConfigMap, error)) *mockConfigMapInterface_Patch_Call { - _c.Call.Return(run) - return _c -} - -// Update provides a mock function with given fields: ctx, configMap, opts -func (_m *mockConfigMapInterface) Update(ctx context.Context, configMap *corev1.ConfigMap, opts metav1.UpdateOptions) (*corev1.ConfigMap, error) { - ret := _m.Called(ctx, configMap, opts) - - if len(ret) == 0 { - panic("no return value specified for Update") - } - - var r0 *corev1.ConfigMap - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *corev1.ConfigMap, metav1.UpdateOptions) (*corev1.ConfigMap, error)); ok { - return rf(ctx, configMap, opts) - } - if rf, ok := ret.Get(0).(func(context.Context, *corev1.ConfigMap, metav1.UpdateOptions) *corev1.ConfigMap); ok { - r0 = rf(ctx, configMap, opts) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*corev1.ConfigMap) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, *corev1.ConfigMap, metav1.UpdateOptions) error); ok { - r1 = rf(ctx, configMap, opts) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// mockConfigMapInterface_Update_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Update' -type mockConfigMapInterface_Update_Call struct { - *mock.Call -} - -// Update is a helper method to define mock.On call -// - ctx context.Context -// - configMap *corev1.ConfigMap -// - opts metav1.UpdateOptions -func (_e *mockConfigMapInterface_Expecter) Update(ctx interface{}, configMap interface{}, opts interface{}) *mockConfigMapInterface_Update_Call { - return &mockConfigMapInterface_Update_Call{Call: _e.mock.On("Update", ctx, configMap, opts)} -} - -func (_c *mockConfigMapInterface_Update_Call) Run(run func(ctx context.Context, configMap *corev1.ConfigMap, opts metav1.UpdateOptions)) *mockConfigMapInterface_Update_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(*corev1.ConfigMap), args[2].(metav1.UpdateOptions)) - }) - return _c -} - -func (_c *mockConfigMapInterface_Update_Call) Return(_a0 *corev1.ConfigMap, _a1 error) *mockConfigMapInterface_Update_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *mockConfigMapInterface_Update_Call) RunAndReturn(run func(context.Context, *corev1.ConfigMap, metav1.UpdateOptions) (*corev1.ConfigMap, error)) *mockConfigMapInterface_Update_Call { - _c.Call.Return(run) - return _c -} - -// Watch provides a mock function with given fields: ctx, opts -func (_m *mockConfigMapInterface) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { - ret := _m.Called(ctx, opts) - - if len(ret) == 0 { - panic("no return value specified for Watch") - } - - var r0 watch.Interface - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, metav1.ListOptions) (watch.Interface, error)); ok { - return rf(ctx, opts) - } - if rf, ok := ret.Get(0).(func(context.Context, metav1.ListOptions) watch.Interface); ok { - r0 = rf(ctx, opts) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(watch.Interface) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, metav1.ListOptions) error); ok { - r1 = rf(ctx, opts) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// mockConfigMapInterface_Watch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Watch' -type mockConfigMapInterface_Watch_Call struct { - *mock.Call -} - -// Watch is a helper method to define mock.On call -// - ctx context.Context -// - opts metav1.ListOptions -func (_e *mockConfigMapInterface_Expecter) Watch(ctx interface{}, opts interface{}) *mockConfigMapInterface_Watch_Call { - return &mockConfigMapInterface_Watch_Call{Call: _e.mock.On("Watch", ctx, opts)} -} - -func (_c *mockConfigMapInterface_Watch_Call) Run(run func(ctx context.Context, opts metav1.ListOptions)) *mockConfigMapInterface_Watch_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(metav1.ListOptions)) - }) - return _c -} - -func (_c *mockConfigMapInterface_Watch_Call) Return(_a0 watch.Interface, _a1 error) *mockConfigMapInterface_Watch_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *mockConfigMapInterface_Watch_Call) RunAndReturn(run func(context.Context, metav1.ListOptions) (watch.Interface, error)) *mockConfigMapInterface_Watch_Call { - _c.Call.Return(run) - return _c -} - -// newMockConfigMapInterface creates a new instance of mockConfigMapInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func newMockConfigMapInterface(t interface { - mock.TestingT - Cleanup(func()) -}) *mockConfigMapInterface { - mock := &mockConfigMapInterface{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/adapter/kubernetes/restartcr/doguRestartRepo.go b/pkg/adapter/kubernetes/restartcr/doguRestartRepo.go deleted file mode 100644 index da860a77..00000000 --- a/pkg/adapter/kubernetes/restartcr/doguRestartRepo.go +++ /dev/null @@ -1,36 +0,0 @@ -package restartcr - -import ( - "context" - "errors" - "fmt" - cescommons "github.com/cloudogu/ces-commons-lib/dogu" - v2 "github.com/cloudogu/k8s-dogu-lib/v2/api/v2" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -type doguRestartRepository struct { - restartInterface DoguRestartInterface -} - -func NewDoguRestartRepository(restartInterface DoguRestartInterface) *doguRestartRepository { - return &doguRestartRepository{restartInterface: restartInterface} -} - -func (d doguRestartRepository) RestartAll(ctx context.Context, names []cescommons.SimpleName) error { - var createErrors []error - for _, doguName := range names { - _, err := d.restartInterface.Create(ctx, &v2.DoguRestart{ - ObjectMeta: metav1.ObjectMeta{ - GenerateName: fmt.Sprintf("%s-", string(doguName)), - }, - Spec: v2.DoguRestartSpec{ - DoguName: string(doguName), - }, - }, metav1.CreateOptions{}) - if err != nil { - createErrors = append(createErrors, err) - } - } - return errors.Join(createErrors...) -} diff --git a/pkg/adapter/kubernetes/restartcr/doguRestartRepo_test.go b/pkg/adapter/kubernetes/restartcr/doguRestartRepo_test.go deleted file mode 100644 index 2581d66a..00000000 --- a/pkg/adapter/kubernetes/restartcr/doguRestartRepo_test.go +++ /dev/null @@ -1,67 +0,0 @@ -package restartcr - -import ( - "context" - cescommons "github.com/cloudogu/ces-commons-lib/dogu" - v2 "github.com/cloudogu/k8s-dogu-lib/v2/api/v2" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "testing" -) - -func Test_doguRestartRepository_RestartAll(t *testing.T) { - t.Run("no error on restart all", func(t *testing.T) { - // given - testContext := context.Background() - testDoguSimpleName := cescommons.SimpleName("testdogu") - dogusThatNeedARestart := []cescommons.SimpleName{testDoguSimpleName} - mockDoguRestartInterface := NewMockDoguRestartInterface(t) - expectedDoguRestartToCreate := &v2.DoguRestart{ObjectMeta: metav1.ObjectMeta{GenerateName: "testdogu-"}, Spec: v2.DoguRestartSpec{DoguName: "testdogu"}} - - mockDoguRestartInterface.EXPECT().Create(testContext, expectedDoguRestartToCreate, metav1.CreateOptions{}).Return(nil, nil) - - restartRepository := NewDoguRestartRepository(mockDoguRestartInterface) - - // when - err := restartRepository.RestartAll(testContext, dogusThatNeedARestart) - - // then - require.NoError(t, err) - }) - - t.Run("no error on empty restart all", func(t *testing.T) { - // given - testContext := context.Background() - dogusThatNeedARestart := []cescommons.SimpleName{} - mockDoguRestartInterface := NewMockDoguRestartInterface(t) - - restartRepository := NewDoguRestartRepository(mockDoguRestartInterface) - - // when - err := restartRepository.RestartAll(testContext, dogusThatNeedARestart) - - // then - require.NoError(t, err) - }) - - t.Run("fail on error at create", func(t *testing.T) { - // given - testContext := context.Background() - testDoguSimpleName := cescommons.SimpleName("testdogu") - dogusThatNeedARestart := []cescommons.SimpleName{testDoguSimpleName} - mockDoguRestartInterface := NewMockDoguRestartInterface(t) - expectedDoguRestartToCreate := &v2.DoguRestart{ObjectMeta: metav1.ObjectMeta{GenerateName: "testdogu-"}, Spec: v2.DoguRestartSpec{DoguName: "testdogu"}} - - mockDoguRestartInterface.EXPECT().Create(testContext, expectedDoguRestartToCreate, metav1.CreateOptions{}).Return(nil, assert.AnError) - - restartRepository := NewDoguRestartRepository(mockDoguRestartInterface) - - // when - err := restartRepository.RestartAll(testContext, dogusThatNeedARestart) - - // then - require.Error(t, err) - assert.ErrorIs(t, err, assert.AnError) - }) -} diff --git a/pkg/adapter/kubernetes/restartcr/interfaces.go b/pkg/adapter/kubernetes/restartcr/interfaces.go deleted file mode 100644 index 3e10865a..00000000 --- a/pkg/adapter/kubernetes/restartcr/interfaces.go +++ /dev/null @@ -1,9 +0,0 @@ -package restartcr - -import ( - ecosystemclient "github.com/cloudogu/k8s-dogu-lib/v2/client" -) - -type DoguRestartInterface interface { - ecosystemclient.DoguRestartInterface -} diff --git a/pkg/adapter/kubernetes/restartcr/mock_DoguRestartInterface_test.go b/pkg/adapter/kubernetes/restartcr/mock_DoguRestartInterface_test.go deleted file mode 100644 index 78b4e3bf..00000000 --- a/pkg/adapter/kubernetes/restartcr/mock_DoguRestartInterface_test.go +++ /dev/null @@ -1,696 +0,0 @@ -// Code generated by mockery v2.53.3. DO NOT EDIT. - -package restartcr - -import ( - context "context" - - mock "github.com/stretchr/testify/mock" - types "k8s.io/apimachinery/pkg/types" - - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - v2 "github.com/cloudogu/k8s-dogu-lib/v2/api/v2" - - watch "k8s.io/apimachinery/pkg/watch" -) - -// MockDoguRestartInterface is an autogenerated mock type for the DoguRestartInterface type -type MockDoguRestartInterface struct { - mock.Mock -} - -type MockDoguRestartInterface_Expecter struct { - mock *mock.Mock -} - -func (_m *MockDoguRestartInterface) EXPECT() *MockDoguRestartInterface_Expecter { - return &MockDoguRestartInterface_Expecter{mock: &_m.Mock} -} - -// Create provides a mock function with given fields: ctx, dogu, opts -func (_m *MockDoguRestartInterface) Create(ctx context.Context, dogu *v2.DoguRestart, opts v1.CreateOptions) (*v2.DoguRestart, error) { - ret := _m.Called(ctx, dogu, opts) - - if len(ret) == 0 { - panic("no return value specified for Create") - } - - var r0 *v2.DoguRestart - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *v2.DoguRestart, v1.CreateOptions) (*v2.DoguRestart, error)); ok { - return rf(ctx, dogu, opts) - } - if rf, ok := ret.Get(0).(func(context.Context, *v2.DoguRestart, v1.CreateOptions) *v2.DoguRestart); ok { - r0 = rf(ctx, dogu, opts) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*v2.DoguRestart) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, *v2.DoguRestart, v1.CreateOptions) error); ok { - r1 = rf(ctx, dogu, opts) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockDoguRestartInterface_Create_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Create' -type MockDoguRestartInterface_Create_Call struct { - *mock.Call -} - -// Create is a helper method to define mock.On call -// - ctx context.Context -// - dogu *v2.DoguRestart -// - opts v1.CreateOptions -func (_e *MockDoguRestartInterface_Expecter) Create(ctx interface{}, dogu interface{}, opts interface{}) *MockDoguRestartInterface_Create_Call { - return &MockDoguRestartInterface_Create_Call{Call: _e.mock.On("Create", ctx, dogu, opts)} -} - -func (_c *MockDoguRestartInterface_Create_Call) Run(run func(ctx context.Context, dogu *v2.DoguRestart, opts v1.CreateOptions)) *MockDoguRestartInterface_Create_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(*v2.DoguRestart), args[2].(v1.CreateOptions)) - }) - return _c -} - -func (_c *MockDoguRestartInterface_Create_Call) Return(_a0 *v2.DoguRestart, _a1 error) *MockDoguRestartInterface_Create_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockDoguRestartInterface_Create_Call) RunAndReturn(run func(context.Context, *v2.DoguRestart, v1.CreateOptions) (*v2.DoguRestart, error)) *MockDoguRestartInterface_Create_Call { - _c.Call.Return(run) - return _c -} - -// Delete provides a mock function with given fields: ctx, name, opts -func (_m *MockDoguRestartInterface) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { - ret := _m.Called(ctx, name, opts) - - if len(ret) == 0 { - panic("no return value specified for Delete") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string, v1.DeleteOptions) error); ok { - r0 = rf(ctx, name, opts) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// MockDoguRestartInterface_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete' -type MockDoguRestartInterface_Delete_Call struct { - *mock.Call -} - -// Delete is a helper method to define mock.On call -// - ctx context.Context -// - name string -// - opts v1.DeleteOptions -func (_e *MockDoguRestartInterface_Expecter) Delete(ctx interface{}, name interface{}, opts interface{}) *MockDoguRestartInterface_Delete_Call { - return &MockDoguRestartInterface_Delete_Call{Call: _e.mock.On("Delete", ctx, name, opts)} -} - -func (_c *MockDoguRestartInterface_Delete_Call) Run(run func(ctx context.Context, name string, opts v1.DeleteOptions)) *MockDoguRestartInterface_Delete_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string), args[2].(v1.DeleteOptions)) - }) - return _c -} - -func (_c *MockDoguRestartInterface_Delete_Call) Return(_a0 error) *MockDoguRestartInterface_Delete_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockDoguRestartInterface_Delete_Call) RunAndReturn(run func(context.Context, string, v1.DeleteOptions) error) *MockDoguRestartInterface_Delete_Call { - _c.Call.Return(run) - return _c -} - -// DeleteCollection provides a mock function with given fields: ctx, opts, listOpts -func (_m *MockDoguRestartInterface) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { - ret := _m.Called(ctx, opts, listOpts) - - if len(ret) == 0 { - panic("no return value specified for DeleteCollection") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, v1.DeleteOptions, v1.ListOptions) error); ok { - r0 = rf(ctx, opts, listOpts) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// MockDoguRestartInterface_DeleteCollection_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteCollection' -type MockDoguRestartInterface_DeleteCollection_Call struct { - *mock.Call -} - -// DeleteCollection is a helper method to define mock.On call -// - ctx context.Context -// - opts v1.DeleteOptions -// - listOpts v1.ListOptions -func (_e *MockDoguRestartInterface_Expecter) DeleteCollection(ctx interface{}, opts interface{}, listOpts interface{}) *MockDoguRestartInterface_DeleteCollection_Call { - return &MockDoguRestartInterface_DeleteCollection_Call{Call: _e.mock.On("DeleteCollection", ctx, opts, listOpts)} -} - -func (_c *MockDoguRestartInterface_DeleteCollection_Call) Run(run func(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions)) *MockDoguRestartInterface_DeleteCollection_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(v1.DeleteOptions), args[2].(v1.ListOptions)) - }) - return _c -} - -func (_c *MockDoguRestartInterface_DeleteCollection_Call) Return(_a0 error) *MockDoguRestartInterface_DeleteCollection_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockDoguRestartInterface_DeleteCollection_Call) RunAndReturn(run func(context.Context, v1.DeleteOptions, v1.ListOptions) error) *MockDoguRestartInterface_DeleteCollection_Call { - _c.Call.Return(run) - return _c -} - -// Get provides a mock function with given fields: ctx, name, opts -func (_m *MockDoguRestartInterface) Get(ctx context.Context, name string, opts v1.GetOptions) (*v2.DoguRestart, error) { - ret := _m.Called(ctx, name, opts) - - if len(ret) == 0 { - panic("no return value specified for Get") - } - - var r0 *v2.DoguRestart - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, v1.GetOptions) (*v2.DoguRestart, error)); ok { - return rf(ctx, name, opts) - } - if rf, ok := ret.Get(0).(func(context.Context, string, v1.GetOptions) *v2.DoguRestart); ok { - r0 = rf(ctx, name, opts) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*v2.DoguRestart) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string, v1.GetOptions) error); ok { - r1 = rf(ctx, name, opts) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockDoguRestartInterface_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get' -type MockDoguRestartInterface_Get_Call struct { - *mock.Call -} - -// Get is a helper method to define mock.On call -// - ctx context.Context -// - name string -// - opts v1.GetOptions -func (_e *MockDoguRestartInterface_Expecter) Get(ctx interface{}, name interface{}, opts interface{}) *MockDoguRestartInterface_Get_Call { - return &MockDoguRestartInterface_Get_Call{Call: _e.mock.On("Get", ctx, name, opts)} -} - -func (_c *MockDoguRestartInterface_Get_Call) Run(run func(ctx context.Context, name string, opts v1.GetOptions)) *MockDoguRestartInterface_Get_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string), args[2].(v1.GetOptions)) - }) - return _c -} - -func (_c *MockDoguRestartInterface_Get_Call) Return(_a0 *v2.DoguRestart, _a1 error) *MockDoguRestartInterface_Get_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockDoguRestartInterface_Get_Call) RunAndReturn(run func(context.Context, string, v1.GetOptions) (*v2.DoguRestart, error)) *MockDoguRestartInterface_Get_Call { - _c.Call.Return(run) - return _c -} - -// List provides a mock function with given fields: ctx, opts -func (_m *MockDoguRestartInterface) List(ctx context.Context, opts v1.ListOptions) (*v2.DoguRestartList, error) { - ret := _m.Called(ctx, opts) - - if len(ret) == 0 { - panic("no return value specified for List") - } - - var r0 *v2.DoguRestartList - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, v1.ListOptions) (*v2.DoguRestartList, error)); ok { - return rf(ctx, opts) - } - if rf, ok := ret.Get(0).(func(context.Context, v1.ListOptions) *v2.DoguRestartList); ok { - r0 = rf(ctx, opts) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*v2.DoguRestartList) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, v1.ListOptions) error); ok { - r1 = rf(ctx, opts) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockDoguRestartInterface_List_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'List' -type MockDoguRestartInterface_List_Call struct { - *mock.Call -} - -// List is a helper method to define mock.On call -// - ctx context.Context -// - opts v1.ListOptions -func (_e *MockDoguRestartInterface_Expecter) List(ctx interface{}, opts interface{}) *MockDoguRestartInterface_List_Call { - return &MockDoguRestartInterface_List_Call{Call: _e.mock.On("List", ctx, opts)} -} - -func (_c *MockDoguRestartInterface_List_Call) Run(run func(ctx context.Context, opts v1.ListOptions)) *MockDoguRestartInterface_List_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(v1.ListOptions)) - }) - return _c -} - -func (_c *MockDoguRestartInterface_List_Call) Return(_a0 *v2.DoguRestartList, _a1 error) *MockDoguRestartInterface_List_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockDoguRestartInterface_List_Call) RunAndReturn(run func(context.Context, v1.ListOptions) (*v2.DoguRestartList, error)) *MockDoguRestartInterface_List_Call { - _c.Call.Return(run) - return _c -} - -// Patch provides a mock function with given fields: ctx, name, pt, data, opts, subresources -func (_m *MockDoguRestartInterface) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (*v2.DoguRestart, error) { - _va := make([]interface{}, len(subresources)) - for _i := range subresources { - _va[_i] = subresources[_i] - } - var _ca []interface{} - _ca = append(_ca, ctx, name, pt, data, opts) - _ca = append(_ca, _va...) - ret := _m.Called(_ca...) - - if len(ret) == 0 { - panic("no return value specified for Patch") - } - - var r0 *v2.DoguRestart - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, types.PatchType, []byte, v1.PatchOptions, ...string) (*v2.DoguRestart, error)); ok { - return rf(ctx, name, pt, data, opts, subresources...) - } - if rf, ok := ret.Get(0).(func(context.Context, string, types.PatchType, []byte, v1.PatchOptions, ...string) *v2.DoguRestart); ok { - r0 = rf(ctx, name, pt, data, opts, subresources...) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*v2.DoguRestart) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string, types.PatchType, []byte, v1.PatchOptions, ...string) error); ok { - r1 = rf(ctx, name, pt, data, opts, subresources...) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockDoguRestartInterface_Patch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Patch' -type MockDoguRestartInterface_Patch_Call struct { - *mock.Call -} - -// Patch is a helper method to define mock.On call -// - ctx context.Context -// - name string -// - pt types.PatchType -// - data []byte -// - opts v1.PatchOptions -// - subresources ...string -func (_e *MockDoguRestartInterface_Expecter) Patch(ctx interface{}, name interface{}, pt interface{}, data interface{}, opts interface{}, subresources ...interface{}) *MockDoguRestartInterface_Patch_Call { - return &MockDoguRestartInterface_Patch_Call{Call: _e.mock.On("Patch", - append([]interface{}{ctx, name, pt, data, opts}, subresources...)...)} -} - -func (_c *MockDoguRestartInterface_Patch_Call) Run(run func(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string)) *MockDoguRestartInterface_Patch_Call { - _c.Call.Run(func(args mock.Arguments) { - variadicArgs := make([]string, len(args)-5) - for i, a := range args[5:] { - if a != nil { - variadicArgs[i] = a.(string) - } - } - run(args[0].(context.Context), args[1].(string), args[2].(types.PatchType), args[3].([]byte), args[4].(v1.PatchOptions), variadicArgs...) - }) - return _c -} - -func (_c *MockDoguRestartInterface_Patch_Call) Return(result *v2.DoguRestart, err error) *MockDoguRestartInterface_Patch_Call { - _c.Call.Return(result, err) - return _c -} - -func (_c *MockDoguRestartInterface_Patch_Call) RunAndReturn(run func(context.Context, string, types.PatchType, []byte, v1.PatchOptions, ...string) (*v2.DoguRestart, error)) *MockDoguRestartInterface_Patch_Call { - _c.Call.Return(run) - return _c -} - -// Update provides a mock function with given fields: ctx, dogu, opts -func (_m *MockDoguRestartInterface) Update(ctx context.Context, dogu *v2.DoguRestart, opts v1.UpdateOptions) (*v2.DoguRestart, error) { - ret := _m.Called(ctx, dogu, opts) - - if len(ret) == 0 { - panic("no return value specified for Update") - } - - var r0 *v2.DoguRestart - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *v2.DoguRestart, v1.UpdateOptions) (*v2.DoguRestart, error)); ok { - return rf(ctx, dogu, opts) - } - if rf, ok := ret.Get(0).(func(context.Context, *v2.DoguRestart, v1.UpdateOptions) *v2.DoguRestart); ok { - r0 = rf(ctx, dogu, opts) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*v2.DoguRestart) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, *v2.DoguRestart, v1.UpdateOptions) error); ok { - r1 = rf(ctx, dogu, opts) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockDoguRestartInterface_Update_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Update' -type MockDoguRestartInterface_Update_Call struct { - *mock.Call -} - -// Update is a helper method to define mock.On call -// - ctx context.Context -// - dogu *v2.DoguRestart -// - opts v1.UpdateOptions -func (_e *MockDoguRestartInterface_Expecter) Update(ctx interface{}, dogu interface{}, opts interface{}) *MockDoguRestartInterface_Update_Call { - return &MockDoguRestartInterface_Update_Call{Call: _e.mock.On("Update", ctx, dogu, opts)} -} - -func (_c *MockDoguRestartInterface_Update_Call) Run(run func(ctx context.Context, dogu *v2.DoguRestart, opts v1.UpdateOptions)) *MockDoguRestartInterface_Update_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(*v2.DoguRestart), args[2].(v1.UpdateOptions)) - }) - return _c -} - -func (_c *MockDoguRestartInterface_Update_Call) Return(_a0 *v2.DoguRestart, _a1 error) *MockDoguRestartInterface_Update_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockDoguRestartInterface_Update_Call) RunAndReturn(run func(context.Context, *v2.DoguRestart, v1.UpdateOptions) (*v2.DoguRestart, error)) *MockDoguRestartInterface_Update_Call { - _c.Call.Return(run) - return _c -} - -// UpdateSpecWithRetry provides a mock function with given fields: ctx, doguRestart, modifySpecFn, opts -func (_m *MockDoguRestartInterface) UpdateSpecWithRetry(ctx context.Context, doguRestart *v2.DoguRestart, modifySpecFn func(v2.DoguRestartSpec) v2.DoguRestartSpec, opts v1.UpdateOptions) (*v2.DoguRestart, error) { - ret := _m.Called(ctx, doguRestart, modifySpecFn, opts) - - if len(ret) == 0 { - panic("no return value specified for UpdateSpecWithRetry") - } - - var r0 *v2.DoguRestart - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *v2.DoguRestart, func(v2.DoguRestartSpec) v2.DoguRestartSpec, v1.UpdateOptions) (*v2.DoguRestart, error)); ok { - return rf(ctx, doguRestart, modifySpecFn, opts) - } - if rf, ok := ret.Get(0).(func(context.Context, *v2.DoguRestart, func(v2.DoguRestartSpec) v2.DoguRestartSpec, v1.UpdateOptions) *v2.DoguRestart); ok { - r0 = rf(ctx, doguRestart, modifySpecFn, opts) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*v2.DoguRestart) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, *v2.DoguRestart, func(v2.DoguRestartSpec) v2.DoguRestartSpec, v1.UpdateOptions) error); ok { - r1 = rf(ctx, doguRestart, modifySpecFn, opts) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockDoguRestartInterface_UpdateSpecWithRetry_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateSpecWithRetry' -type MockDoguRestartInterface_UpdateSpecWithRetry_Call struct { - *mock.Call -} - -// UpdateSpecWithRetry is a helper method to define mock.On call -// - ctx context.Context -// - doguRestart *v2.DoguRestart -// - modifySpecFn func(v2.DoguRestartSpec) v2.DoguRestartSpec -// - opts v1.UpdateOptions -func (_e *MockDoguRestartInterface_Expecter) UpdateSpecWithRetry(ctx interface{}, doguRestart interface{}, modifySpecFn interface{}, opts interface{}) *MockDoguRestartInterface_UpdateSpecWithRetry_Call { - return &MockDoguRestartInterface_UpdateSpecWithRetry_Call{Call: _e.mock.On("UpdateSpecWithRetry", ctx, doguRestart, modifySpecFn, opts)} -} - -func (_c *MockDoguRestartInterface_UpdateSpecWithRetry_Call) Run(run func(ctx context.Context, doguRestart *v2.DoguRestart, modifySpecFn func(v2.DoguRestartSpec) v2.DoguRestartSpec, opts v1.UpdateOptions)) *MockDoguRestartInterface_UpdateSpecWithRetry_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(*v2.DoguRestart), args[2].(func(v2.DoguRestartSpec) v2.DoguRestartSpec), args[3].(v1.UpdateOptions)) - }) - return _c -} - -func (_c *MockDoguRestartInterface_UpdateSpecWithRetry_Call) Return(result *v2.DoguRestart, err error) *MockDoguRestartInterface_UpdateSpecWithRetry_Call { - _c.Call.Return(result, err) - return _c -} - -func (_c *MockDoguRestartInterface_UpdateSpecWithRetry_Call) RunAndReturn(run func(context.Context, *v2.DoguRestart, func(v2.DoguRestartSpec) v2.DoguRestartSpec, v1.UpdateOptions) (*v2.DoguRestart, error)) *MockDoguRestartInterface_UpdateSpecWithRetry_Call { - _c.Call.Return(run) - return _c -} - -// UpdateStatus provides a mock function with given fields: ctx, dogu, opts -func (_m *MockDoguRestartInterface) UpdateStatus(ctx context.Context, dogu *v2.DoguRestart, opts v1.UpdateOptions) (*v2.DoguRestart, error) { - ret := _m.Called(ctx, dogu, opts) - - if len(ret) == 0 { - panic("no return value specified for UpdateStatus") - } - - var r0 *v2.DoguRestart - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *v2.DoguRestart, v1.UpdateOptions) (*v2.DoguRestart, error)); ok { - return rf(ctx, dogu, opts) - } - if rf, ok := ret.Get(0).(func(context.Context, *v2.DoguRestart, v1.UpdateOptions) *v2.DoguRestart); ok { - r0 = rf(ctx, dogu, opts) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*v2.DoguRestart) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, *v2.DoguRestart, v1.UpdateOptions) error); ok { - r1 = rf(ctx, dogu, opts) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockDoguRestartInterface_UpdateStatus_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateStatus' -type MockDoguRestartInterface_UpdateStatus_Call struct { - *mock.Call -} - -// UpdateStatus is a helper method to define mock.On call -// - ctx context.Context -// - dogu *v2.DoguRestart -// - opts v1.UpdateOptions -func (_e *MockDoguRestartInterface_Expecter) UpdateStatus(ctx interface{}, dogu interface{}, opts interface{}) *MockDoguRestartInterface_UpdateStatus_Call { - return &MockDoguRestartInterface_UpdateStatus_Call{Call: _e.mock.On("UpdateStatus", ctx, dogu, opts)} -} - -func (_c *MockDoguRestartInterface_UpdateStatus_Call) Run(run func(ctx context.Context, dogu *v2.DoguRestart, opts v1.UpdateOptions)) *MockDoguRestartInterface_UpdateStatus_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(*v2.DoguRestart), args[2].(v1.UpdateOptions)) - }) - return _c -} - -func (_c *MockDoguRestartInterface_UpdateStatus_Call) Return(_a0 *v2.DoguRestart, _a1 error) *MockDoguRestartInterface_UpdateStatus_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockDoguRestartInterface_UpdateStatus_Call) RunAndReturn(run func(context.Context, *v2.DoguRestart, v1.UpdateOptions) (*v2.DoguRestart, error)) *MockDoguRestartInterface_UpdateStatus_Call { - _c.Call.Return(run) - return _c -} - -// UpdateStatusWithRetry provides a mock function with given fields: ctx, doguRestart, modifyStatusFn, opts -func (_m *MockDoguRestartInterface) UpdateStatusWithRetry(ctx context.Context, doguRestart *v2.DoguRestart, modifyStatusFn func(v2.DoguRestartStatus) v2.DoguRestartStatus, opts v1.UpdateOptions) (*v2.DoguRestart, error) { - ret := _m.Called(ctx, doguRestart, modifyStatusFn, opts) - - if len(ret) == 0 { - panic("no return value specified for UpdateStatusWithRetry") - } - - var r0 *v2.DoguRestart - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *v2.DoguRestart, func(v2.DoguRestartStatus) v2.DoguRestartStatus, v1.UpdateOptions) (*v2.DoguRestart, error)); ok { - return rf(ctx, doguRestart, modifyStatusFn, opts) - } - if rf, ok := ret.Get(0).(func(context.Context, *v2.DoguRestart, func(v2.DoguRestartStatus) v2.DoguRestartStatus, v1.UpdateOptions) *v2.DoguRestart); ok { - r0 = rf(ctx, doguRestart, modifyStatusFn, opts) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*v2.DoguRestart) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, *v2.DoguRestart, func(v2.DoguRestartStatus) v2.DoguRestartStatus, v1.UpdateOptions) error); ok { - r1 = rf(ctx, doguRestart, modifyStatusFn, opts) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockDoguRestartInterface_UpdateStatusWithRetry_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateStatusWithRetry' -type MockDoguRestartInterface_UpdateStatusWithRetry_Call struct { - *mock.Call -} - -// UpdateStatusWithRetry is a helper method to define mock.On call -// - ctx context.Context -// - doguRestart *v2.DoguRestart -// - modifyStatusFn func(v2.DoguRestartStatus) v2.DoguRestartStatus -// - opts v1.UpdateOptions -func (_e *MockDoguRestartInterface_Expecter) UpdateStatusWithRetry(ctx interface{}, doguRestart interface{}, modifyStatusFn interface{}, opts interface{}) *MockDoguRestartInterface_UpdateStatusWithRetry_Call { - return &MockDoguRestartInterface_UpdateStatusWithRetry_Call{Call: _e.mock.On("UpdateStatusWithRetry", ctx, doguRestart, modifyStatusFn, opts)} -} - -func (_c *MockDoguRestartInterface_UpdateStatusWithRetry_Call) Run(run func(ctx context.Context, doguRestart *v2.DoguRestart, modifyStatusFn func(v2.DoguRestartStatus) v2.DoguRestartStatus, opts v1.UpdateOptions)) *MockDoguRestartInterface_UpdateStatusWithRetry_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(*v2.DoguRestart), args[2].(func(v2.DoguRestartStatus) v2.DoguRestartStatus), args[3].(v1.UpdateOptions)) - }) - return _c -} - -func (_c *MockDoguRestartInterface_UpdateStatusWithRetry_Call) Return(result *v2.DoguRestart, err error) *MockDoguRestartInterface_UpdateStatusWithRetry_Call { - _c.Call.Return(result, err) - return _c -} - -func (_c *MockDoguRestartInterface_UpdateStatusWithRetry_Call) RunAndReturn(run func(context.Context, *v2.DoguRestart, func(v2.DoguRestartStatus) v2.DoguRestartStatus, v1.UpdateOptions) (*v2.DoguRestart, error)) *MockDoguRestartInterface_UpdateStatusWithRetry_Call { - _c.Call.Return(run) - return _c -} - -// Watch provides a mock function with given fields: ctx, opts -func (_m *MockDoguRestartInterface) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { - ret := _m.Called(ctx, opts) - - if len(ret) == 0 { - panic("no return value specified for Watch") - } - - var r0 watch.Interface - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, v1.ListOptions) (watch.Interface, error)); ok { - return rf(ctx, opts) - } - if rf, ok := ret.Get(0).(func(context.Context, v1.ListOptions) watch.Interface); ok { - r0 = rf(ctx, opts) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(watch.Interface) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, v1.ListOptions) error); ok { - r1 = rf(ctx, opts) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockDoguRestartInterface_Watch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Watch' -type MockDoguRestartInterface_Watch_Call struct { - *mock.Call -} - -// Watch is a helper method to define mock.On call -// - ctx context.Context -// - opts v1.ListOptions -func (_e *MockDoguRestartInterface_Expecter) Watch(ctx interface{}, opts interface{}) *MockDoguRestartInterface_Watch_Call { - return &MockDoguRestartInterface_Watch_Call{Call: _e.mock.On("Watch", ctx, opts)} -} - -func (_c *MockDoguRestartInterface_Watch_Call) Run(run func(ctx context.Context, opts v1.ListOptions)) *MockDoguRestartInterface_Watch_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(v1.ListOptions)) - }) - return _c -} - -func (_c *MockDoguRestartInterface_Watch_Call) Return(_a0 watch.Interface, _a1 error) *MockDoguRestartInterface_Watch_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockDoguRestartInterface_Watch_Call) RunAndReturn(run func(context.Context, v1.ListOptions) (watch.Interface, error)) *MockDoguRestartInterface_Watch_Call { - _c.Call.Return(run) - return _c -} - -// NewMockDoguRestartInterface creates a new instance of MockDoguRestartInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewMockDoguRestartInterface(t interface { - mock.TestingT - Cleanup(func()) -}) *MockDoguRestartInterface { - mock := &MockDoguRestartInterface{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/adapter/kubernetes/sensitiveconfigref/secretRefReader.go b/pkg/adapter/kubernetes/sensitiveconfigref/secretRefReader.go index 7a76f560..b772310f 100644 --- a/pkg/adapter/kubernetes/sensitiveconfigref/secretRefReader.go +++ b/pkg/adapter/kubernetes/sensitiveconfigref/secretRefReader.go @@ -4,13 +4,14 @@ import ( "context" "errors" "fmt" + "iter" + "maps" + "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/common" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domainservice" - "iter" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "maps" ) type SecretRefReader struct { @@ -23,7 +24,7 @@ func NewSecretRefReader(secretClient secretClient) *SecretRefReader { } } -func (reader *SecretRefReader) GetValues(ctx context.Context, refs map[common.SensitiveDoguConfigKey]domain.SensitiveValueRef) (map[common.SensitiveDoguConfigKey]common.SensitiveDoguConfigValue, error) { +func (reader *SecretRefReader) GetValues(ctx context.Context, refs map[common.DoguConfigKey]domain.SensitiveValueRef) (map[common.DoguConfigKey]common.SensitiveDoguConfigValue, error) { secretsByName, secretErrors := reader.loadNeededSecrets(ctx, maps.Values(refs)) sensitiveConfig, keyErrors := reader.loadKeysFromSecrets(refs, secretsByName) @@ -38,11 +39,11 @@ func (reader *SecretRefReader) GetValues(ctx context.Context, refs map[common.Se } func (reader *SecretRefReader) loadKeysFromSecrets( - refs map[common.SensitiveDoguConfigKey]domain.SensitiveValueRef, + refs map[common.DoguConfigKey]domain.SensitiveValueRef, secretsByName map[string]*v1.Secret, -) (map[common.SensitiveDoguConfigKey]common.SensitiveDoguConfigValue, error) { +) (map[common.DoguConfigKey]common.SensitiveDoguConfigValue, error) { var errs []error - loadedConfig := map[common.SensitiveDoguConfigKey]common.SensitiveDoguConfigValue{} + loadedConfig := map[common.DoguConfigKey]common.SensitiveDoguConfigValue{} for configKey, ref := range refs { secret, found := secretsByName[ref.SecretName] diff --git a/pkg/adapter/kubernetes/sensitiveconfigref/secretRefReader_test.go b/pkg/adapter/kubernetes/sensitiveconfigref/secretRefReader_test.go index 7ad5f2dc..29f474ea 100644 --- a/pkg/adapter/kubernetes/sensitiveconfigref/secretRefReader_test.go +++ b/pkg/adapter/kubernetes/sensitiveconfigref/secretRefReader_test.go @@ -2,6 +2,8 @@ package sensitiveconfigref import ( "context" + "testing" + "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/common" "github.com/stretchr/testify/assert" @@ -10,7 +12,6 @@ import ( k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" - "testing" ) var testCtx = context.TODO() @@ -21,11 +22,11 @@ var ( }, Data: map[string][]byte{"username": []byte("user1"), "password": []byte("123456")}, } - redmineCredentialsUsernameKey = common.SensitiveDoguConfigKey{ + redmineCredentialsUsernameKey = common.DoguConfigKey{ DoguName: "redmine", Key: "credentials/username", } - redmineCredentialsPasswordKey = common.SensitiveDoguConfigKey{ + redmineCredentialsPasswordKey = common.DoguConfigKey{ DoguName: "redmine", Key: "credentials/password", } @@ -36,11 +37,11 @@ var ( }, Data: map[string][]byte{"username": []byte("user2"), "password": []byte("789123")}, } - ldapCredentialsUsernameKey = common.SensitiveDoguConfigKey{ + ldapCredentialsUsernameKey = common.DoguConfigKey{ DoguName: "ldap", Key: "credentials/username", } - ldapCredentialsPasswordKey = common.SensitiveDoguConfigKey{ + ldapCredentialsPasswordKey = common.DoguConfigKey{ DoguName: "ldap", Key: "credentials/password", } @@ -51,9 +52,9 @@ func TestSecretRefReader_GetValues(t *testing.T) { secretMock := newMockSecretClient(t) refReader := NewSecretRefReader(secretMock) - result, err := refReader.GetValues(testCtx, map[common.SensitiveDoguConfigKey]domain.SensitiveValueRef{}) + result, err := refReader.GetValues(testCtx, map[common.DoguConfigKey]domain.SensitiveValueRef{}) require.NoError(t, err) - assert.Equal(t, map[common.SensitiveDoguConfigKey]common.SensitiveDoguConfigValue{}, result) + assert.Equal(t, map[common.DoguConfigKey]common.SensitiveDoguConfigValue{}, result) }) t.Run("nothing to load with nil input", func(t *testing.T) { secretMock := newMockSecretClient(t) @@ -61,7 +62,7 @@ func TestSecretRefReader_GetValues(t *testing.T) { result, err := refReader.GetValues(testCtx, nil) require.NoError(t, err) - assert.Equal(t, map[common.SensitiveDoguConfigKey]common.SensitiveDoguConfigValue{}, result) + assert.Equal(t, map[common.DoguConfigKey]common.SensitiveDoguConfigValue{}, result) }) t.Run("load secrets with keys", func(t *testing.T) { secretMock := newMockSecretClient(t) @@ -75,7 +76,7 @@ func TestSecretRefReader_GetValues(t *testing.T) { refReader := NewSecretRefReader(secretMock) result, err := refReader.GetValues(testCtx, - map[common.SensitiveDoguConfigKey]domain.SensitiveValueRef{ + map[common.DoguConfigKey]domain.SensitiveValueRef{ redmineCredentialsUsernameKey: { SecretName: redmineSecret.Name, SecretKey: "username", @@ -95,7 +96,7 @@ func TestSecretRefReader_GetValues(t *testing.T) { }, ) require.NoError(t, err) - assert.Equal(t, map[common.SensitiveDoguConfigKey]common.SensitiveDoguConfigValue{ + assert.Equal(t, map[common.DoguConfigKey]common.SensitiveDoguConfigValue{ redmineCredentialsUsernameKey: "user1", redmineCredentialsPasswordKey: "123456", ldapCredentialsUsernameKey: "user2", @@ -119,7 +120,7 @@ func TestSecretRefReader_GetValues(t *testing.T) { refReader := NewSecretRefReader(secretMock) _, err := refReader.GetValues(testCtx, - map[common.SensitiveDoguConfigKey]domain.SensitiveValueRef{ + map[common.DoguConfigKey]domain.SensitiveValueRef{ redmineCredentialsUsernameKey: { SecretName: redmineSecret.Name, SecretKey: "username", diff --git a/pkg/adapter/reconciler/blueprint_controller.go b/pkg/adapter/reconciler/blueprint_controller.go index a4c08f99..b50a80a8 100644 --- a/pkg/adapter/reconciler/blueprint_controller.go +++ b/pkg/adapter/reconciler/blueprint_controller.go @@ -3,12 +3,12 @@ package reconciler import ( "context" "errors" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/application" + "fmt" + "time" + "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" - "strings" - "time" "github.com/go-logr/logr" ctrl "sigs.k8s.io/controller-runtime" @@ -30,9 +30,8 @@ func NewBlueprintReconciler( return &BlueprintReconciler{blueprintChangeHandler: blueprintChangeHandler} } -// +kubebuilder:rbac:groups=k8s.cloudogu.com,resources=blueprints,verbs=get;list;watch;update;patch +// +kubebuilder:rbac:groups=k8s.cloudogu.com,resources=blueprints,verbs=get;watch;update;patch // +kubebuilder:rbac:groups=k8s.cloudogu.com,resources=blueprints/status,verbs=get;update;patch -// +kubebuilder:rbac:groups=k8s.cloudogu.com,resources=blueprints/finalizers,verbs=update // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. @@ -44,7 +43,13 @@ func (r *BlueprintReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( WithName("BlueprintReconciler.Reconcile"). WithValues("resourceName", req.Name) - err := r.blueprintChangeHandler.HandleChange(ctx, req.Name) + err := r.blueprintChangeHandler.CheckForMultipleBlueprintResources(ctx) + + if err != nil { + return decideRequeueForError(logger, err) + } + + err = r.blueprintChangeHandler.HandleUntilApplied(ctx, req.Name) if err != nil { return decideRequeueForError(logger, err) @@ -61,6 +66,9 @@ func decideRequeueForError(logger logr.Logger, err error) (ctrl.Result, error) { var notFoundError *domainservice.NotFoundError var invalidBlueprintError *domain.InvalidBlueprintError var healthError *domain.UnhealthyEcosystemError + var stateDiffNotEmptyError *domain.StateDiffNotEmptyError + var multipleBlueprintsError *domain.MultipleBlueprintsError + var dogusNotUpToDateError *domain.DogusNotUpToDateError switch { case errors.As(err, &internalError): errLogger.Error(err, "An internal error occurred and can maybe be fixed by retrying it later") @@ -69,20 +77,33 @@ func decideRequeueForError(logger logr.Logger, err error) (ctrl.Result, error) { errLogger.Info("A concurrent update happened in conflict to the processing of the blueprint spec. A retry could fix this issue") return ctrl.Result{RequeueAfter: 1 * time.Second}, nil // no error as this would lead to the ignorance of our own retry params case errors.As(err, ¬FoundError): - if strings.Contains(err.Error(), application.REFERENCED_CONFIG_NOT_FOUND) { - // retry in this case because maybe the user will create the secret in a few seconds. - // we want to be declarative, so our API should not care about the order - errLogger.Error(err, "Referenced config not found. Retry later") - return ctrl.Result{RequeueAfter: 10 * time.Second}, nil + if notFoundError.DoNotRetry { + // do not retry in this case, because if f.e. the blueprint is not found, nothing will bring it back, except the + // user, and this would trigger the reconciler by itself. + logger.Error(err, "Did not find resource and a retry is not expected to fix this issue. There will be no further automatic evaluation.") + return ctrl.Result{}, nil } - errLogger.Error(err, "Blueprint was not found, so maybe it was deleted in the meantime. No further evaluation will happen") - return ctrl.Result{}, nil + logger.Error(err, "Resource was not found, so maybe it was deleted in the meantime. Retry later") + return ctrl.Result{RequeueAfter: 10 * time.Second}, nil case errors.As(err, &invalidBlueprintError): errLogger.Info("Blueprint is invalid, therefore there will be no further evaluation.") return ctrl.Result{}, nil case errors.As(err, &healthError): + // really normal case errLogger.Info("Ecosystem is unhealthy. Retry later") return ctrl.Result{RequeueAfter: 10 * time.Second}, nil + case errors.As(err, &stateDiffNotEmptyError): + errLogger.Info("requeue until state diff is empty") + // fast requeue here since state diff has to be determined again + return ctrl.Result{RequeueAfter: 1 * time.Second}, nil + case errors.As(err, &multipleBlueprintsError): + errLogger.Error(err, "Ecosystem contains multiple blueprints - delete all except one. Retry later") + // fast requeue here since state diff has to be determined again + return ctrl.Result{RequeueAfter: 10 * time.Second}, nil + case errors.As(err, &dogusNotUpToDateError): + // really normal case + errLogger.Info(fmt.Sprintf("Dogus are not up to date yet. Retry later: %s", err.Error())) + return ctrl.Result{RequeueAfter: 10 * time.Second}, nil default: errLogger.Error(err, "An unknown error type occurred. Retry with default backoff") return ctrl.Result{}, err // automatic requeue because of non-nil err diff --git a/pkg/adapter/reconciler/blueprint_controller_test.go b/pkg/adapter/reconciler/blueprint_controller_test.go index 3b7d2c97..86fd44c0 100644 --- a/pkg/adapter/reconciler/blueprint_controller_test.go +++ b/pkg/adapter/reconciler/blueprint_controller_test.go @@ -4,12 +4,12 @@ import ( "context" "errors" "fmt" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/application" + "testing" + "time" + "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domainservice" "github.com/go-logr/logr" - "testing" - "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -84,7 +84,8 @@ func TestBlueprintReconciler_Reconcile(t *testing.T) { changeHandlerMock := NewMockBlueprintChangeHandler(t) sut := &BlueprintReconciler{blueprintChangeHandler: changeHandlerMock} - changeHandlerMock.EXPECT().HandleChange(testCtx, testBlueprint).Return(nil) + changeHandlerMock.EXPECT().CheckForMultipleBlueprintResources(testCtx).Return(nil) + changeHandlerMock.EXPECT().HandleUntilApplied(testCtx, testBlueprint).Return(nil) // when actual, err := sut.Reconcile(testCtx, request) @@ -92,13 +93,30 @@ func TestBlueprintReconciler_Reconcile(t *testing.T) { require.NoError(t, err) assert.Equal(t, ctrl.Result{}, actual) }) - t.Run("should fail", func(t *testing.T) { + + t.Run("should succeed", func(t *testing.T) { // given request := ctrl.Request{NamespacedName: types.NamespacedName{Name: testBlueprint}} changeHandlerMock := NewMockBlueprintChangeHandler(t) sut := &BlueprintReconciler{blueprintChangeHandler: changeHandlerMock} - changeHandlerMock.EXPECT().HandleChange(testCtx, testBlueprint).Return(errors.New("test")) + changeHandlerMock.EXPECT().CheckForMultipleBlueprintResources(testCtx).Return(assert.AnError) + // when + _, err := sut.Reconcile(testCtx, request) + + // then + require.Error(t, err) + assert.ErrorIs(t, err, assert.AnError) + }) + + t.Run("should fail on HandleUntilApplied error", func(t *testing.T) { + // given + request := ctrl.Request{NamespacedName: types.NamespacedName{Name: testBlueprint}} + changeHandlerMock := NewMockBlueprintChangeHandler(t) + sut := &BlueprintReconciler{blueprintChangeHandler: changeHandlerMock} + + changeHandlerMock.EXPECT().CheckForMultipleBlueprintResources(testCtx).Return(nil) + changeHandlerMock.EXPECT().HandleUntilApplied(testCtx, testBlueprint).Return(errors.New("test")) // when _, err := sut.Reconcile(testCtx, request) @@ -144,7 +162,7 @@ func Test_decideRequeueForError(t *testing.T) { assert.Equal(t, ctrl.Result{RequeueAfter: 1 * time.Second}, actual) assert.Contains(t, logSinkMock.output, "0: A concurrent update happened in conflict to the processing of the blueprint spec. A retry could fix this issue") }) - t.Run("should catch wrapped NotFoundError, issue a log line and do not requeue", func(t *testing.T) { + t.Run("should catch wrapped NotFoundError, issue a log line and requeue", func(t *testing.T) { // given logSinkMock := newTrivialTestLogSink() testLogger := logr.New(logSinkMock) @@ -160,27 +178,46 @@ func Test_decideRequeueForError(t *testing.T) { // then require.NoError(t, err) - assert.Equal(t, ctrl.Result{}, actual) - assert.Contains(t, logSinkMock.output, "0: Blueprint was not found, so maybe it was deleted in the meantime. No further evaluation will happen") + assert.Equal(t, ctrl.Result{RequeueAfter: 10 * time.Second}, actual) + assert.Contains(t, logSinkMock.output, "0: Resource was not found, so maybe it was deleted in the meantime. Retry later") }) - t.Run("NotFoundError, should retry if referenced config is missing", func(t *testing.T) { + t.Run("should catch wrapped MultipleBlueprintsError, issue a error log line and requeue", func(t *testing.T) { + // given + logSinkMock := newTrivialTestLogSink() + testLogger := logr.New(logSinkMock) + + intermediateErr := &domain.MultipleBlueprintsError{ + Message: "multiple blueprints found", + } + errorChain := fmt.Errorf("could not do the thing: %w", intermediateErr) + + // when + actual, err := decideRequeueForError(testLogger, errorChain) + + // then + require.NoError(t, err) + assert.Equal(t, ctrl.Result{RequeueAfter: 10 * time.Second}, actual) + assert.Contains(t, logSinkMock.output, "0: Ecosystem contains multiple blueprints - delete all except one. Retry later") + }) + t.Run("NotFoundError, should not retry if DoNotRetry-Flag is set", func(t *testing.T) { // given logSinkMock := newTrivialTestLogSink() testLogger := logr.New(logSinkMock) intermediateErr := &domainservice.NotFoundError{ WrappedError: assert.AnError, - Message: "secret xyz does not exist", + Message: "Blueprint does not exist", + DoNotRetry: true, } - errorChain := fmt.Errorf("%s: %w", application.REFERENCED_CONFIG_NOT_FOUND, intermediateErr) + errorChain := fmt.Errorf("could not do the thing: %w", intermediateErr) // when actual, err := decideRequeueForError(testLogger, errorChain) // then require.NoError(t, err) - assert.Equal(t, ctrl.Result{RequeueAfter: 10 * time.Second}, actual) - assert.Contains(t, logSinkMock.output, "0: Referenced config not found. Retry later") + assert.Equal(t, ctrl.Result{}, actual) + assert.Contains(t, logSinkMock.output, "0: Did not find resource and a retry is not expected to fix this issue. There will be no further automatic evaluation.") }) t.Run("should catch wrapped InvalidBlueprintError, issue a log line and do not requeue", func(t *testing.T) { // given @@ -201,6 +238,24 @@ func Test_decideRequeueForError(t *testing.T) { assert.Equal(t, ctrl.Result{}, actual) assert.Contains(t, logSinkMock.output, "0: Blueprint is invalid, therefore there will be no further evaluation.") }) + t.Run("should catch wrapped StateDiffNotEmptyError, issue a log line and requeue timely", func(t *testing.T) { + // given + logSinkMock := newTrivialTestLogSink() + testLogger := logr.New(logSinkMock) + + intermediateErr := &domain.StateDiffNotEmptyError{ + Message: "a generic oh-noez", + } + errorChain := fmt.Errorf("could not do the thing: %w", intermediateErr) + + // when + actual, err := decideRequeueForError(testLogger, errorChain) + + // then + require.NoError(t, err) + assert.Equal(t, ctrl.Result{RequeueAfter: 1 * time.Second}, actual) + assert.Contains(t, logSinkMock.output, "0: requeue until state diff is empty") + }) t.Run("should catch general errors, issue a log line and return requeue with error", func(t *testing.T) { // given logSinkMock := newTrivialTestLogSink() diff --git a/pkg/adapter/reconciler/interfaces.go b/pkg/adapter/reconciler/interfaces.go index a684cd1e..aacc651c 100644 --- a/pkg/adapter/reconciler/interfaces.go +++ b/pkg/adapter/reconciler/interfaces.go @@ -2,6 +2,7 @@ package reconciler import ( "context" + "sigs.k8s.io/controller-runtime/pkg/manager" ) @@ -14,5 +15,6 @@ type controllerManager interface { } type BlueprintChangeHandler interface { - HandleChange(ctx context.Context, blueprintId string) error + HandleUntilApplied(ctx context.Context, blueprintId string) error + CheckForMultipleBlueprintResources(ctx context.Context) error } diff --git a/pkg/adapter/reconciler/mock_BlueprintChangeHandler_test.go b/pkg/adapter/reconciler/mock_BlueprintChangeHandler_test.go index 7aedce6c..6a7b007f 100644 --- a/pkg/adapter/reconciler/mock_BlueprintChangeHandler_test.go +++ b/pkg/adapter/reconciler/mock_BlueprintChangeHandler_test.go @@ -21,12 +21,58 @@ func (_m *MockBlueprintChangeHandler) EXPECT() *MockBlueprintChangeHandler_Expec return &MockBlueprintChangeHandler_Expecter{mock: &_m.Mock} } -// HandleChange provides a mock function with given fields: ctx, blueprintId -func (_m *MockBlueprintChangeHandler) HandleChange(ctx context.Context, blueprintId string) error { +// CheckForMultipleBlueprintResources provides a mock function with given fields: ctx +func (_m *MockBlueprintChangeHandler) CheckForMultipleBlueprintResources(ctx context.Context) error { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for CheckForMultipleBlueprintResources") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(ctx) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockBlueprintChangeHandler_CheckForMultipleBlueprintResources_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CheckForMultipleBlueprintResources' +type MockBlueprintChangeHandler_CheckForMultipleBlueprintResources_Call struct { + *mock.Call +} + +// CheckForMultipleBlueprintResources is a helper method to define mock.On call +// - ctx context.Context +func (_e *MockBlueprintChangeHandler_Expecter) CheckForMultipleBlueprintResources(ctx interface{}) *MockBlueprintChangeHandler_CheckForMultipleBlueprintResources_Call { + return &MockBlueprintChangeHandler_CheckForMultipleBlueprintResources_Call{Call: _e.mock.On("CheckForMultipleBlueprintResources", ctx)} +} + +func (_c *MockBlueprintChangeHandler_CheckForMultipleBlueprintResources_Call) Run(run func(ctx context.Context)) *MockBlueprintChangeHandler_CheckForMultipleBlueprintResources_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *MockBlueprintChangeHandler_CheckForMultipleBlueprintResources_Call) Return(_a0 error) *MockBlueprintChangeHandler_CheckForMultipleBlueprintResources_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockBlueprintChangeHandler_CheckForMultipleBlueprintResources_Call) RunAndReturn(run func(context.Context) error) *MockBlueprintChangeHandler_CheckForMultipleBlueprintResources_Call { + _c.Call.Return(run) + return _c +} + +// HandleUntilApplied provides a mock function with given fields: ctx, blueprintId +func (_m *MockBlueprintChangeHandler) HandleUntilApplied(ctx context.Context, blueprintId string) error { ret := _m.Called(ctx, blueprintId) if len(ret) == 0 { - panic("no return value specified for HandleChange") + panic("no return value specified for HandleUntilApplied") } var r0 error @@ -39,31 +85,31 @@ func (_m *MockBlueprintChangeHandler) HandleChange(ctx context.Context, blueprin return r0 } -// MockBlueprintChangeHandler_HandleChange_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'HandleChange' -type MockBlueprintChangeHandler_HandleChange_Call struct { +// MockBlueprintChangeHandler_HandleUntilApplied_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'HandleUntilApplied' +type MockBlueprintChangeHandler_HandleUntilApplied_Call struct { *mock.Call } -// HandleChange is a helper method to define mock.On call +// HandleUntilApplied is a helper method to define mock.On call // - ctx context.Context // - blueprintId string -func (_e *MockBlueprintChangeHandler_Expecter) HandleChange(ctx interface{}, blueprintId interface{}) *MockBlueprintChangeHandler_HandleChange_Call { - return &MockBlueprintChangeHandler_HandleChange_Call{Call: _e.mock.On("HandleChange", ctx, blueprintId)} +func (_e *MockBlueprintChangeHandler_Expecter) HandleUntilApplied(ctx interface{}, blueprintId interface{}) *MockBlueprintChangeHandler_HandleUntilApplied_Call { + return &MockBlueprintChangeHandler_HandleUntilApplied_Call{Call: _e.mock.On("HandleUntilApplied", ctx, blueprintId)} } -func (_c *MockBlueprintChangeHandler_HandleChange_Call) Run(run func(ctx context.Context, blueprintId string)) *MockBlueprintChangeHandler_HandleChange_Call { +func (_c *MockBlueprintChangeHandler_HandleUntilApplied_Call) Run(run func(ctx context.Context, blueprintId string)) *MockBlueprintChangeHandler_HandleUntilApplied_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(string)) }) return _c } -func (_c *MockBlueprintChangeHandler_HandleChange_Call) Return(_a0 error) *MockBlueprintChangeHandler_HandleChange_Call { +func (_c *MockBlueprintChangeHandler_HandleUntilApplied_Call) Return(_a0 error) *MockBlueprintChangeHandler_HandleUntilApplied_Call { _c.Call.Return(_a0) return _c } -func (_c *MockBlueprintChangeHandler_HandleChange_Call) RunAndReturn(run func(context.Context, string) error) *MockBlueprintChangeHandler_HandleChange_Call { +func (_c *MockBlueprintChangeHandler_HandleUntilApplied_Call) RunAndReturn(run func(context.Context, string) error) *MockBlueprintChangeHandler_HandleUntilApplied_Call { _c.Call.Return(run) return _c } diff --git a/pkg/application/applyBlueprintSpecUseCase.go b/pkg/application/applyBlueprintSpecUseCase.go deleted file mode 100644 index 712f9cb8..00000000 --- a/pkg/application/applyBlueprintSpecUseCase.go +++ /dev/null @@ -1,235 +0,0 @@ -package application - -import ( - "context" - "errors" - "fmt" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" - "sigs.k8s.io/controller-runtime/pkg/log" -) - -// ApplyBlueprintSpecUseCase contains all use cases which are needed for or around applying -// the new ecosystem state after the determining the state diff. -type ApplyBlueprintSpecUseCase struct { - repo blueprintSpecRepository - doguInstallUseCase doguInstallationUseCase - healthUseCase ecosystemHealthUseCase - componentInstallUseCase componentInstallationUseCase -} - -func NewApplyBlueprintSpecUseCase( - repo blueprintSpecRepository, - doguInstallUseCase doguInstallationUseCase, - healthUseCase ecosystemHealthUseCase, - componentInstallUseCase componentInstallationUseCase, -) *ApplyBlueprintSpecUseCase { - return &ApplyBlueprintSpecUseCase{ - repo: repo, - doguInstallUseCase: doguInstallUseCase, - healthUseCase: healthUseCase, - componentInstallUseCase: componentInstallUseCase, - } -} - -// CheckEcosystemHealthUpfront checks the ecosystem health before applying the blueprint and sets the related status in the blueprint. -// Returns domain.UnhealthyEcosystemError if the ecosystem is currently unhealthy or -// returns domainservice.ConflictError if there was a concurrent update to the blueprint spec or -// returns a domainservice.InternalError if there was an unspecified error while collecting or modifying the ecosystem state or -// There is no error, if the ecosystem is unhealthy as this gets reflected in the blueprint spec status. -func (useCase *ApplyBlueprintSpecUseCase) CheckEcosystemHealthUpfront(ctx context.Context, blueprintId string) error { - logger := log.FromContext(ctx).WithName("ApplyBlueprintSpecUseCase.CheckEcosystemHealthUpfront"). - WithValues("blueprintId", blueprintId) - - logger.Info("getting blueprint spec for checking ecosystem health") - blueprintSpec, err := useCase.repo.GetById(ctx, blueprintId) - if err != nil { - return fmt.Errorf("cannot load blueprint spec %q to check ecosystem health: %w", blueprintId, err) - } - - healthResult, err := useCase.healthUseCase.CheckEcosystemHealth(ctx, blueprintSpec.Config.IgnoreDoguHealth, blueprintSpec.Config.IgnoreComponentHealth) - if err != nil { - return fmt.Errorf("cannot check ecosystem health upfront of applying the blueprint %q: %w", blueprintId, err) - } - healthErr := blueprintSpec.CheckEcosystemHealthUpfront(healthResult) - // persist blueprint even with error, because it will set conditions - err = useCase.repo.Update(ctx, blueprintSpec) - if err != nil { - // healthErr can be ignored here. We have a more serious problem if we cannot persist the blueprint - // the health check will be repeated anyway - return fmt.Errorf("cannot save blueprint spec %q after checking the ecosystem health: %w", blueprintId, err) - } - - return healthErr -} - -// CheckEcosystemHealthAfterwards waits for a healthy ecosystem health after applying the blueprint and sets the related status in the blueprint. -// Returns domain.UnhealthyEcosystemError if the ecosystem is currently unhealthy or -// returns domainservice.ConflictError if there was a concurrent update to the blueprint spec or -// returns a domainservice.InternalError if there was an unspecified error while collecting or modifying the ecosystem state. -// There is no error, if the ecosystem is unhealthy as this gets reflected in the blueprint spec status. -func (useCase *ApplyBlueprintSpecUseCase) CheckEcosystemHealthAfterwards(ctx context.Context, blueprintId string) error { - logger := log.FromContext(ctx).WithName("ApplyBlueprintSpecUseCase.CheckEcosystemHealthAfterwards"). - WithValues("blueprintId", blueprintId) - - logger.Info("getting blueprint spec for checking ecosystem health afterwards") - blueprintSpec, err := useCase.repo.GetById(ctx, blueprintId) - if err != nil { - return fmt.Errorf("cannot load blueprint spec %q to check ecosystem health: %w", blueprintId, err) - } - - // do not ignore the health states of dogus and components here, as we want to set the blueprint status according to the result. - // The blueprint is already executed here. - healthResult, err := useCase.healthUseCase.CheckEcosystemHealth(ctx, false, false) - if err != nil { - return fmt.Errorf("cannot check ecosystem health after applying the blueprint %q: %w", blueprintId, err) - } - healthErr := blueprintSpec.CheckEcosystemHealthAfterwards(healthResult) - - err = useCase.repo.Update(ctx, blueprintSpec) - if err != nil { - // healthErr can be ignored here. We have a more serious problem if we cannot persist the blueprint - // the health check will be repeated anyway - return fmt.Errorf("cannot save blueprint spec %q after checking the ecosystem health: %w", blueprintId, err) - } - - return healthErr -} - -// PreProcessBlueprintApplication prepares the environment for applying the blueprint. -// returns a domainservice.InternalError on any error. -func (useCase *ApplyBlueprintSpecUseCase) PreProcessBlueprintApplication(ctx context.Context, blueprintId string) error { - logger := log.FromContext(ctx).WithName("ApplyBlueprintSpecUseCase.PreProcessBlueprintApplication"). - WithValues("blueprintId", blueprintId) - - blueprintSpec, err := useCase.repo.GetById(ctx, blueprintId) - if err != nil { - return fmt.Errorf("cannot load blueprint spec %q for preprocessing: %w", blueprintId, err) - } - - if !blueprintSpec.ShouldBeApplied() { - logger.Info("stop as blueprint should not be applied") - } - - blueprintSpec.CompletePreProcessing() - - err = useCase.repo.Update(ctx, blueprintSpec) - if err != nil { - return fmt.Errorf("cannot save blueprint spec %q after preprocessing: %w", blueprintId, err) - } - - return nil -} - -// PostProcessBlueprintApplication makes changes to the environment after applying the blueprint, e.g. censoring the state diff. -// returns a domainservice.InternalError on any error. -func (useCase *ApplyBlueprintSpecUseCase) PostProcessBlueprintApplication(ctx context.Context, blueprintId string) error { - logger := log.FromContext(ctx).WithName("ApplyBlueprintSpecUseCase.PostProcessBlueprintApplication"). - WithValues("blueprintId", blueprintId) - - blueprintSpec, err := useCase.repo.GetById(ctx, blueprintId) - if err != nil { - return fmt.Errorf("cannot load blueprint spec %q while post-processing blueprint application: %w", blueprintId, err) - } - - logger.Info("censor sensitive data") - blueprintSpec.CensorSensitiveData() - - blueprintSpec.CompletePostProcessing() - - err = useCase.repo.Update(ctx, blueprintSpec) - if err != nil { - return fmt.Errorf("cannot update blueprint spec %q while post-processing blueprint application: %w", blueprintId, err) - } - - return nil -} - -// ApplyBlueprintSpec applies the expected state to the ecosystem. It will stop if any unexpected error happens and sets blueprint status. -// Returns domainservice.ConflictError if there was a concurrent update to the blueprint spec or other resources or -// returns a domainservice.InternalError if there was an unspecified error while collecting or modifying the ecosystem state. -// There is no error, if the ecosystem is unhealthy as this gets reflected in the blueprint spec status. -func (useCase *ApplyBlueprintSpecUseCase) ApplyBlueprintSpec(ctx context.Context, blueprintId string) error { - logger := log.FromContext(ctx).WithName("ApplyBlueprintSpecUseCase.ApplyBlueprintSpec"). - WithValues("blueprintId", blueprintId) - - blueprintSpec, err := useCase.repo.GetById(ctx, blueprintId) - if err != nil { - return fmt.Errorf("cannot load blueprint to apply blueprint spec: %w", err) - } - - logger.Info("start applying blueprint to the cluster") - err = useCase.startApplying(ctx, blueprintSpec) - if err != nil { - return err - } - - applyError := useCase.componentInstallUseCase.ApplyComponentStates(ctx, blueprintId) - if applyError != nil { - return useCase.handleApplyFailedError(ctx, blueprintSpec, applyError) - } - - _, err = useCase.componentInstallUseCase.WaitForHealthyComponents(ctx) - if err != nil { - return useCase.handleApplyFailedError(ctx, blueprintSpec, err) - } - - applyError = useCase.doguInstallUseCase.ApplyDoguStates(ctx, blueprintId) - if applyError != nil { - return useCase.handleApplyFailedError(ctx, blueprintSpec, applyError) - } - - // FIXME: this health check is blocking. I think we need to split the apply logic into multiple steps to - // we have to wait for all dogus to be healthy - // otherwise service account creation might fail because dogus are restarted right after this step - _, err = useCase.doguInstallUseCase.WaitForHealthyDogus(ctx) - if err != nil { - return useCase.handleApplyFailedError(ctx, blueprintSpec, err) - } - - logger.Info("blueprint successfully applied to the cluster") - return useCase.markBlueprintApplied(ctx, blueprintSpec) -} - -func (useCase *ApplyBlueprintSpecUseCase) handleApplyFailedError(ctx context.Context, blueprintSpec *domain.BlueprintSpec, applyError error) error { - err := useCase.markBlueprintApplicationFailed(ctx, blueprintSpec, applyError) - if err != nil { - return err - } - return applyError -} - -func (useCase *ApplyBlueprintSpecUseCase) startApplying(ctx context.Context, blueprintSpec *domain.BlueprintSpec) error { - blueprintSpec.StartApplying() - err := useCase.repo.Update(ctx, blueprintSpec) - if err != nil { - return fmt.Errorf("cannot mark blueprint as in progress: %w", err) - } - return nil -} - -// markBlueprintApplicationFailed marks the blueprint application as failed. -// Returns the error which leads to the failed blueprint needs to be provided. -func (useCase *ApplyBlueprintSpecUseCase) markBlueprintApplicationFailed(ctx context.Context, blueprintSpec *domain.BlueprintSpec, err error) error { - logger := log.FromContext(ctx). - WithName("ApplyBlueprintSpecUseCase.markBlueprintApplicationFailed"). - WithValues("blueprintId", blueprintSpec.Id) - - blueprintSpec.MarkBlueprintApplicationFailed(err) - repoErr := useCase.repo.Update(ctx, blueprintSpec) - - if repoErr != nil { - repoErr = errors.Join(repoErr, err) - logger.Error(repoErr, "cannot mark blueprint as failed") - return fmt.Errorf("cannot mark blueprint as failed while handling %q status: %w", blueprintSpec.Status, repoErr) - } - return nil -} - -func (useCase *ApplyBlueprintSpecUseCase) markBlueprintApplied(ctx context.Context, blueprintSpec *domain.BlueprintSpec) error { - blueprintSpec.MarkBlueprintApplied() - err := useCase.repo.Update(ctx, blueprintSpec) - if err != nil { - return fmt.Errorf("cannot mark blueprint as waiting for a healthy ecosystem: %w", err) - } - return nil -} diff --git a/pkg/application/applyBlueprintSpecUseCase_test.go b/pkg/application/applyBlueprintSpecUseCase_test.go deleted file mode 100644 index a99c1297..00000000 --- a/pkg/application/applyBlueprintSpecUseCase_test.go +++ /dev/null @@ -1,560 +0,0 @@ -package application - -import ( - "context" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - "testing" -) - -func TestNewApplyBlueprintSpecUseCase(t *testing.T) { - repoMock := newMockBlueprintSpecRepository(t) - installUseCaseMock := newMockDoguInstallationUseCase(t) - componentInstallUseCaseMock := newMockComponentInstallationUseCase(t) - healthMock := newMockEcosystemHealthUseCase(t) - - sut := NewApplyBlueprintSpecUseCase(repoMock, installUseCaseMock, healthMock, componentInstallUseCaseMock) - - assert.Equal(t, installUseCaseMock, sut.doguInstallUseCase) - assert.Equal(t, componentInstallUseCaseMock, sut.componentInstallUseCase) - assert.Equal(t, repoMock, sut.repo) - assert.Equal(t, healthMock, sut.healthUseCase) -} - -func TestApplyBlueprintSpecUseCase_PreProcessBlueprintApplication(t *testing.T) { - t.Run("ok", func(t *testing.T) { - spec := &domain.BlueprintSpec{ - Status: domain.StatusPhaseEcosystemHealthyUpfront, - } - - repoMock := newMockBlueprintSpecRepository(t) - repoMock.EXPECT().GetById(testCtx, blueprintId).Return(spec, nil) - repoMock.EXPECT().Update(testCtx, spec).Return(nil) - useCase := NewApplyBlueprintSpecUseCase(repoMock, nil, nil, nil) - - err := useCase.PreProcessBlueprintApplication(testCtx, blueprintId) - - require.NoError(t, err) - assert.Equal(t, domain.StatusPhaseBlueprintApplicationPreProcessed, spec.Status) - }) - t.Run("repo error while loading", func(t *testing.T) { - repoMock := newMockBlueprintSpecRepository(t) - repoMock.EXPECT().GetById(testCtx, blueprintId).Return(nil, assert.AnError) - useCase := NewApplyBlueprintSpecUseCase(repoMock, nil, nil, nil) - - err := useCase.PreProcessBlueprintApplication(testCtx, blueprintId) - - require.ErrorIs(t, err, assert.AnError) - }) - t.Run("repo error while saving", func(t *testing.T) { - spec := &domain.BlueprintSpec{ - Status: domain.StatusPhaseEcosystemHealthyUpfront, - } - - repoMock := newMockBlueprintSpecRepository(t) - repoMock.EXPECT().GetById(testCtx, blueprintId).Return(spec, nil) - repoMock.EXPECT().Update(testCtx, spec).Return(assert.AnError) - useCase := NewApplyBlueprintSpecUseCase(repoMock, nil, nil, nil) - - err := useCase.PreProcessBlueprintApplication(testCtx, blueprintId) - - require.ErrorIs(t, err, assert.AnError) - }) - t.Run("do nothing on dry run", func(t *testing.T) { - spec := &domain.BlueprintSpec{ - Status: domain.StatusPhaseEcosystemHealthyUpfront, - Config: domain.BlueprintConfiguration{DryRun: true}, - } - - repoMock := newMockBlueprintSpecRepository(t) - repoMock.EXPECT().GetById(testCtx, blueprintId).Return(spec, nil) - repoMock.EXPECT().Update(testCtx, spec).Return(nil) - useCase := NewApplyBlueprintSpecUseCase(repoMock, nil, nil, nil) - - err := useCase.PreProcessBlueprintApplication(testCtx, blueprintId) - - require.NoError(t, err) - require.Equal(t, 1, len(spec.Events)) - assert.Equal(t, domain.BlueprintDryRunEvent{}, spec.Events[0]) - }) -} - -func TestApplyBlueprintSpecUseCase_markInProgress(t *testing.T) { - t.Run("ok", func(t *testing.T) { - spec := &domain.BlueprintSpec{ - Status: domain.StatusPhaseEcosystemHealthyUpfront, - } - - repoMock := newMockBlueprintSpecRepository(t) - repoMock.EXPECT().Update(testCtx, spec).Return(nil) - installUseCaseMock := newMockDoguInstallationUseCase(t) - useCase := ApplyBlueprintSpecUseCase{repo: repoMock, doguInstallUseCase: installUseCaseMock} - - err := useCase.startApplying(testCtx, spec) - - require.NoError(t, err) - assert.Equal(t, domain.StatusPhaseInProgress, spec.Status) - }) -} - -func TestApplyBlueprintSpecUseCase_markBlueprintApplicationFailed(t *testing.T) { - t.Run("ok", func(t *testing.T) { - spec := &domain.BlueprintSpec{ - Status: domain.StatusPhaseInProgress, - } - - repoMock := newMockBlueprintSpecRepository(t) - repoMock.EXPECT().Update(testCtx, spec).Return(nil) - installUseCaseMock := newMockDoguInstallationUseCase(t) - useCase := ApplyBlueprintSpecUseCase{repo: repoMock, doguInstallUseCase: installUseCaseMock} - - err := useCase.markBlueprintApplicationFailed(testCtx, spec, assert.AnError) - - require.NoError(t, err) - assert.Equal(t, domain.StatusPhaseBlueprintApplicationFailed, spec.Status) - }) - - t.Run("repo error", func(t *testing.T) { - spec := &domain.BlueprintSpec{ - Status: domain.StatusPhaseInProgress, - } - - repoMock := newMockBlueprintSpecRepository(t) - repoMock.EXPECT().Update(testCtx, spec).Return(assert.AnError) - installUseCaseMock := newMockDoguInstallationUseCase(t) - useCase := ApplyBlueprintSpecUseCase{repo: repoMock, doguInstallUseCase: installUseCaseMock} - - err := useCase.markBlueprintApplicationFailed(testCtx, spec, assert.AnError) - - require.ErrorIs(t, err, assert.AnError) - assert.Equal(t, domain.StatusPhaseBlueprintApplicationFailed, spec.Status) - }) -} - -func TestApplyBlueprintSpecUseCase_markBlueprintApplied(t *testing.T) { - t.Run("ok", func(t *testing.T) { - spec := &domain.BlueprintSpec{ - Status: domain.StatusPhaseInProgress, - } - - repoMock := newMockBlueprintSpecRepository(t) - repoMock.EXPECT().Update(testCtx, spec).Return(nil) - installUseCaseMock := newMockDoguInstallationUseCase(t) - useCase := ApplyBlueprintSpecUseCase{repo: repoMock, doguInstallUseCase: installUseCaseMock} - - err := useCase.markBlueprintApplied(testCtx, spec) - - require.NoError(t, err) - assert.Equal(t, domain.StatusPhaseBlueprintApplied, spec.Status) - }) - - t.Run("repo error", func(t *testing.T) { - spec := &domain.BlueprintSpec{ - Status: domain.StatusPhaseInProgress, - } - - repoMock := newMockBlueprintSpecRepository(t) - repoMock.EXPECT().Update(testCtx, spec).Return(assert.AnError) - installUseCaseMock := newMockDoguInstallationUseCase(t) - useCase := ApplyBlueprintSpecUseCase{repo: repoMock, doguInstallUseCase: installUseCaseMock} - - err := useCase.markBlueprintApplied(testCtx, spec) - - require.ErrorIs(t, err, assert.AnError) - assert.Equal(t, domain.StatusPhaseBlueprintApplied, spec.Status) - }) -} - -func TestApplyBlueprintSpecUseCase_ApplyBlueprintSpec(t *testing.T) { - statusTransitions := map[int]domain.StatusPhase{ - 1: domain.StatusPhaseInProgress, - 2: domain.StatusPhaseBlueprintApplied, - } - t.Run("ok", func(t *testing.T) { - spec := &domain.BlueprintSpec{ - Status: domain.StatusPhaseEcosystemHealthyUpfront, - } - repoMock := newMockBlueprintSpecRepository(t) - repoMock.EXPECT().GetById(testCtx, "blueprintId").Return(spec, nil) - var counter = 0 - repoMock.EXPECT().Update(testCtx, spec).RunAndReturn(func(ctx context.Context, spec *domain.BlueprintSpec) error { - counter++ - assert.Equal(t, statusTransitions[counter], spec.Status) - return nil - }).Times(2) - - installUseCaseMock := newMockDoguInstallationUseCase(t) - installUseCaseMock.EXPECT().ApplyDoguStates(testCtx, "blueprintId").Return(nil) - installUseCaseMock.EXPECT().WaitForHealthyDogus(testCtx).Return(ecosystem.DoguHealthResult{}, nil) - componentInstallUseCase := newMockComponentInstallationUseCase(t) - componentInstallUseCase.EXPECT().ApplyComponentStates(testCtx, "blueprintId").Return(nil) - componentInstallUseCase.EXPECT().WaitForHealthyComponents(testCtx).Return(ecosystem.ComponentHealthResult{}, nil) - - useCase := ApplyBlueprintSpecUseCase{repo: repoMock, doguInstallUseCase: installUseCaseMock, componentInstallUseCase: componentInstallUseCase} - - err := useCase.ApplyBlueprintSpec(testCtx, "blueprintId") - - require.NoError(t, err) - assert.Equal(t, domain.StatusPhaseBlueprintApplied, spec.Status) - }) - t.Run("error waiting for dogu health", func(t *testing.T) { - spec := &domain.BlueprintSpec{ - Status: domain.StatusPhaseEcosystemHealthyUpfront, - } - repoMock := newMockBlueprintSpecRepository(t) - repoMock.EXPECT().GetById(testCtx, "blueprintId").Return(spec, nil) - repoMock.EXPECT().Update(testCtx, spec).Return(nil).Times(2) - - installUseCaseMock := newMockDoguInstallationUseCase(t) - installUseCaseMock.EXPECT().ApplyDoguStates(testCtx, "blueprintId").Return(nil) - installUseCaseMock.EXPECT().WaitForHealthyDogus(testCtx).Return(ecosystem.DoguHealthResult{}, assert.AnError) - componentInstallUseCase := newMockComponentInstallationUseCase(t) - componentInstallUseCase.EXPECT().ApplyComponentStates(testCtx, "blueprintId").Return(nil) - componentInstallUseCase.EXPECT().WaitForHealthyComponents(testCtx).Return(ecosystem.ComponentHealthResult{}, nil) - - useCase := ApplyBlueprintSpecUseCase{repo: repoMock, doguInstallUseCase: installUseCaseMock, componentInstallUseCase: componentInstallUseCase} - - err := useCase.ApplyBlueprintSpec(testCtx, "blueprintId") - - require.ErrorIs(t, err, assert.AnError) - assert.Equal(t, domain.StatusPhaseBlueprintApplicationFailed, spec.Status) - }) - - t.Run("cannot load spec", func(t *testing.T) { - repoMock := newMockBlueprintSpecRepository(t) - repoMock.EXPECT().GetById(testCtx, "blueprintId").Return(nil, assert.AnError) - - useCase := ApplyBlueprintSpecUseCase{repo: repoMock, doguInstallUseCase: nil} - - err := useCase.ApplyBlueprintSpec(testCtx, "blueprintId") - - assert.ErrorIs(t, err, assert.AnError) - assert.ErrorContains(t, err, "cannot load blueprint to apply blueprint spec") - }) - - t.Run("fail to mark in progress", func(t *testing.T) { - spec := &domain.BlueprintSpec{ - Status: domain.StatusPhaseEcosystemHealthyUpfront, - } - repoMock := newMockBlueprintSpecRepository(t) - repoMock.EXPECT().GetById(testCtx, "blueprintId").Return(spec, nil) - repoMock.EXPECT().Update(testCtx, spec).Return(assert.AnError) - - useCase := ApplyBlueprintSpecUseCase{repo: repoMock, doguInstallUseCase: nil} - - err := useCase.ApplyBlueprintSpec(testCtx, "blueprintId") - - require.ErrorIs(t, err, assert.AnError) - assert.Equal(t, domain.StatusPhaseInProgress, spec.Status) - }) - - t.Run("fail to apply component state", func(t *testing.T) { - spec := &domain.BlueprintSpec{ - Status: domain.StatusPhaseEcosystemHealthyUpfront, - } - repoMock := newMockBlueprintSpecRepository(t) - repoMock.EXPECT().GetById(testCtx, "blueprintId").Return(spec, nil) - repoMock.EXPECT().Update(testCtx, spec).Return(nil).Times(2) - - componentInstallUseCaseMock := newMockComponentInstallationUseCase(t) - componentInstallUseCaseMock.EXPECT().ApplyComponentStates(testCtx, "blueprintId").Return(assert.AnError) - useCase := ApplyBlueprintSpecUseCase{repo: repoMock, doguInstallUseCase: nil, componentInstallUseCase: componentInstallUseCaseMock} - - err := useCase.ApplyBlueprintSpec(testCtx, "blueprintId") - - require.ErrorIs(t, err, assert.AnError) - assert.Equal(t, domain.StatusPhaseBlueprintApplicationFailed, spec.Status) - }) - - t.Run("fail to wait for component health", func(t *testing.T) { - spec := &domain.BlueprintSpec{ - Status: domain.StatusPhaseEcosystemHealthyUpfront, - } - repoMock := newMockBlueprintSpecRepository(t) - repoMock.EXPECT().GetById(testCtx, "blueprintId").Return(spec, nil) - repoMock.EXPECT().Update(testCtx, spec).Return(nil).Times(2) - - componentInstallUseCaseMock := newMockComponentInstallationUseCase(t) - componentInstallUseCaseMock.EXPECT().ApplyComponentStates(testCtx, "blueprintId").Return(nil) - componentInstallUseCaseMock.EXPECT().WaitForHealthyComponents(testCtx).Return(ecosystem.ComponentHealthResult{}, assert.AnError) - useCase := ApplyBlueprintSpecUseCase{repo: repoMock, doguInstallUseCase: nil, componentInstallUseCase: componentInstallUseCaseMock} - - err := useCase.ApplyBlueprintSpec(testCtx, "blueprintId") - - require.ErrorIs(t, err, assert.AnError) - assert.Equal(t, domain.StatusPhaseBlueprintApplicationFailed, spec.Status) - }) - - t.Run("fail to apply dogu state", func(t *testing.T) { - spec := &domain.BlueprintSpec{ - Status: domain.StatusPhaseEcosystemHealthyUpfront, - } - repoMock := newMockBlueprintSpecRepository(t) - repoMock.EXPECT().GetById(testCtx, "blueprintId").Return(spec, nil) - repoMock.EXPECT().Update(testCtx, spec).Return(nil).Times(2) - - componentInstallUseCaseMock := newMockComponentInstallationUseCase(t) - componentInstallUseCaseMock.EXPECT().ApplyComponentStates(testCtx, "blueprintId").Return(nil) - componentInstallUseCaseMock.EXPECT().WaitForHealthyComponents(testCtx).Return(ecosystem.ComponentHealthResult{}, nil) - installUseCaseMock := newMockDoguInstallationUseCase(t) - installUseCaseMock.EXPECT().ApplyDoguStates(testCtx, "blueprintId").Return(assert.AnError) - useCase := ApplyBlueprintSpecUseCase{repo: repoMock, doguInstallUseCase: installUseCaseMock, componentInstallUseCase: componentInstallUseCaseMock} - - err := useCase.ApplyBlueprintSpec(testCtx, "blueprintId") - - require.ErrorIs(t, err, assert.AnError) - assert.Equal(t, domain.StatusPhaseBlueprintApplicationFailed, spec.Status) - }) - - t.Run("fail to apply state and fail to mark execution failed", func(t *testing.T) { - spec := &domain.BlueprintSpec{ - Status: domain.StatusPhaseEcosystemHealthyUpfront, - } - repoMock := newMockBlueprintSpecRepository(t) - repoMock.EXPECT().GetById(testCtx, "blueprintId").Return(spec, nil) - counter := 0 - repoMock.EXPECT().Update(testCtx, spec).RunAndReturn(func(ctx context.Context, spec *domain.BlueprintSpec) error { - counter++ - if counter == 1 { - return nil - } else { - return assert.AnError - } - }) - - componentInstallUseCaseMock := newMockComponentInstallationUseCase(t) - componentInstallUseCaseMock.EXPECT().ApplyComponentStates(testCtx, "blueprintId").Return(nil) - componentInstallUseCaseMock.EXPECT().WaitForHealthyComponents(testCtx).Return(ecosystem.ComponentHealthResult{}, nil) - installUseCaseMock := newMockDoguInstallationUseCase(t) - installUseCaseMock.EXPECT().ApplyDoguStates(testCtx, "blueprintId").Return(assert.AnError) - useCase := ApplyBlueprintSpecUseCase{repo: repoMock, doguInstallUseCase: installUseCaseMock, componentInstallUseCase: componentInstallUseCaseMock} - - err := useCase.ApplyBlueprintSpec(testCtx, "blueprintId") - - require.ErrorIs(t, err, assert.AnError) - assert.Equal(t, domain.StatusPhaseBlueprintApplicationFailed, spec.Status) - }) -} - -func TestApplyBlueprintSpecUseCase_CheckEcosystemHealthUpfront(t *testing.T) { - t.Run("should fail to get blueprint spec", func(t *testing.T) { - // given - repoMock := newMockBlueprintSpecRepository(t) - repoMock.EXPECT().GetById(testCtx, blueprintId).Return(nil, assert.AnError) - - sut := NewApplyBlueprintSpecUseCase(repoMock, nil, nil, nil) - - // when - err := sut.CheckEcosystemHealthUpfront(testCtx, blueprintId) - - // then - require.Error(t, err) - assert.ErrorIs(t, err, assert.AnError) - assert.ErrorContains(t, err, "cannot load blueprint spec \"blueprint1\" to check ecosystem health") - }) - t.Run("should fail to get health result", func(t *testing.T) { - // given - repoMock := newMockBlueprintSpecRepository(t) - repoMock.EXPECT().GetById(testCtx, blueprintId).Return(&domain.BlueprintSpec{}, nil) - - healthMock := newMockEcosystemHealthUseCase(t) - healthMock.EXPECT().CheckEcosystemHealth(testCtx, false, false).Return(ecosystem.HealthResult{}, assert.AnError) - - sut := NewApplyBlueprintSpecUseCase(repoMock, nil, healthMock, nil) - - // when - err := sut.CheckEcosystemHealthUpfront(testCtx, blueprintId) - - // then - require.Error(t, err) - assert.ErrorIs(t, err, assert.AnError) - assert.ErrorContains(t, err, "cannot check ecosystem health upfront of applying the blueprint \"blueprint1\"") - }) - t.Run("should fail to update blueprint spec", func(t *testing.T) { - // given - blueprintSpec := &domain.BlueprintSpec{} - repoMock := newMockBlueprintSpecRepository(t) - repoMock.EXPECT().GetById(testCtx, blueprintId).Return(blueprintSpec, nil) - repoMock.EXPECT().Update(testCtx, blueprintSpec).Return(assert.AnError) - - healthMock := newMockEcosystemHealthUseCase(t) - healthMock.EXPECT().CheckEcosystemHealth(mock.Anything, false, false).Return(ecosystem.HealthResult{}, nil) - - sut := NewApplyBlueprintSpecUseCase(repoMock, nil, healthMock, nil) - - // when - err := sut.CheckEcosystemHealthUpfront(testCtx, blueprintId) - - // then - require.Error(t, err) - assert.ErrorIs(t, err, assert.AnError) - assert.ErrorContains(t, err, "cannot save blueprint spec \"blueprint1\" after checking the ecosystem health") - }) - t.Run("should succeed, ignoring dogu and component health", func(t *testing.T) { - // given - blueprintSpec := &domain.BlueprintSpec{Config: domain.BlueprintConfiguration{ - IgnoreDoguHealth: true, - IgnoreComponentHealth: true, - }} - repoMock := newMockBlueprintSpecRepository(t) - repoMock.EXPECT().GetById(testCtx, blueprintId).Return(blueprintSpec, nil) - repoMock.EXPECT().Update(testCtx, blueprintSpec).Return(nil) - - healthMock := newMockEcosystemHealthUseCase(t) - healthMock.EXPECT().CheckEcosystemHealth(mock.Anything, true, true).Return(ecosystem.HealthResult{}, nil) - - sut := NewApplyBlueprintSpecUseCase(repoMock, nil, healthMock, nil) - - // when - err := sut.CheckEcosystemHealthUpfront(testCtx, blueprintId) - - // then - require.NoError(t, err) - }) - t.Run("should succeed", func(t *testing.T) { - // given - blueprintSpec := &domain.BlueprintSpec{} - repoMock := newMockBlueprintSpecRepository(t) - repoMock.EXPECT().GetById(testCtx, blueprintId).Return(blueprintSpec, nil) - repoMock.EXPECT().Update(testCtx, blueprintSpec).Return(nil) - - healthMock := newMockEcosystemHealthUseCase(t) - healthMock.EXPECT().CheckEcosystemHealth(mock.Anything, false, false).Return(ecosystem.HealthResult{}, nil) - - sut := NewApplyBlueprintSpecUseCase(repoMock, nil, healthMock, nil) - - // when - err := sut.CheckEcosystemHealthUpfront(testCtx, blueprintId) - - // then - require.NoError(t, err) - }) -} - -func TestApplyBlueprintSpecUseCase_CheckEcosystemHealthAfterwards(t *testing.T) { - t.Run("should fail to get blueprint spec", func(t *testing.T) { - // given - repoMock := newMockBlueprintSpecRepository(t) - repoMock.EXPECT().GetById(testCtx, blueprintId).Return(nil, assert.AnError) - - sut := NewApplyBlueprintSpecUseCase(repoMock, nil, nil, nil) - - // when - err := sut.CheckEcosystemHealthAfterwards(testCtx, blueprintId) - - // then - require.Error(t, err) - assert.ErrorIs(t, err, assert.AnError) - assert.ErrorContains(t, err, "cannot load blueprint spec \"blueprint1\" to check ecosystem health") - }) - - t.Run("should fail to get health result", func(t *testing.T) { - // given - repoMock := newMockBlueprintSpecRepository(t) - repoMock.EXPECT().GetById(testCtx, blueprintId).Return(&domain.BlueprintSpec{}, nil) - - healthMock := newMockEcosystemHealthUseCase(t) - healthMock.EXPECT().CheckEcosystemHealth(testCtx, false, false).Return(ecosystem.HealthResult{}, assert.AnError) - - sut := NewApplyBlueprintSpecUseCase(repoMock, nil, healthMock, nil) - - // when - err := sut.CheckEcosystemHealthAfterwards(testCtx, blueprintId) - - // then - require.Error(t, err) - assert.ErrorIs(t, err, assert.AnError) - assert.ErrorContains(t, err, "cannot check ecosystem health after applying the blueprint \"blueprint1\"") - }) - - t.Run("should fail to update blueprint spec", func(t *testing.T) { - // given - blueprintSpec := &domain.BlueprintSpec{} - repoMock := newMockBlueprintSpecRepository(t) - repoMock.EXPECT().GetById(testCtx, blueprintId).Return(blueprintSpec, nil) - repoMock.EXPECT().Update(testCtx, blueprintSpec).Return(assert.AnError) - - healthMock := newMockEcosystemHealthUseCase(t) - healthMock.EXPECT().CheckEcosystemHealth(testCtx, false, false).Return(ecosystem.HealthResult{}, nil) - - sut := NewApplyBlueprintSpecUseCase(repoMock, nil, healthMock, nil) - - // when - err := sut.CheckEcosystemHealthAfterwards(testCtx, blueprintId) - - // then - require.Error(t, err) - assert.ErrorIs(t, err, assert.AnError) - assert.ErrorContains(t, err, "cannot save blueprint spec \"blueprint1\" after checking the ecosystem health") - }) - - t.Run("should succeed", func(t *testing.T) { - // given - blueprintSpec := &domain.BlueprintSpec{} - repoMock := newMockBlueprintSpecRepository(t) - repoMock.EXPECT().GetById(testCtx, blueprintId).Return(blueprintSpec, nil) - repoMock.EXPECT().Update(testCtx, blueprintSpec).Return(nil) - - healthMock := newMockEcosystemHealthUseCase(t) - healthMock.EXPECT().CheckEcosystemHealth(testCtx, false, false).Return(ecosystem.HealthResult{}, nil) - - sut := NewApplyBlueprintSpecUseCase(repoMock, nil, healthMock, nil) - - // when - err := sut.CheckEcosystemHealthAfterwards(testCtx, blueprintId) - - // then - require.NoError(t, err) - assert.Equal(t, domain.StatusPhaseEcosystemHealthyAfterwards, blueprintSpec.Status) - }) -} - -func TestApplyBlueprintSpecUseCase_PostProcessBlueprintApplication(t *testing.T) { - t.Run("ok", func(t *testing.T) { - spec := &domain.BlueprintSpec{ - Status: domain.StatusPhaseEcosystemHealthyAfterwards, - } - - repoMock := newMockBlueprintSpecRepository(t) - repoMock.EXPECT().GetById(testCtx, blueprintId).Return(spec, nil) - repoMock.EXPECT().Update(testCtx, spec).Return(nil) - useCase := NewApplyBlueprintSpecUseCase(repoMock, nil, nil, nil) - - err := useCase.PostProcessBlueprintApplication(testCtx, blueprintId) - - require.NoError(t, err) - - assert.Equal(t, domain.StatusPhaseCompleted, spec.Status) - assert.Len(t, spec.Events, 2) - assert.Contains(t, spec.Events, domain.SensitiveConfigDataCensoredEvent{}, spec.Events) - assert.Contains(t, spec.Events, domain.CompletedEvent{}, spec.Events) - }) - - t.Run("repo error while loading", func(t *testing.T) { - repoMock := newMockBlueprintSpecRepository(t) - repoMock.EXPECT().GetById(testCtx, blueprintId).Return(nil, assert.AnError) - useCase := NewApplyBlueprintSpecUseCase(repoMock, nil, nil, nil) - - err := useCase.PostProcessBlueprintApplication(testCtx, blueprintId) - - require.ErrorIs(t, err, assert.AnError) - }) - - t.Run("repo error while saving", func(t *testing.T) { - spec := &domain.BlueprintSpec{ - Status: domain.StatusPhaseEcosystemHealthyAfterwards, - } - - repoMock := newMockBlueprintSpecRepository(t) - repoMock.EXPECT().GetById(testCtx, blueprintId).Return(spec, nil) - repoMock.EXPECT().Update(testCtx, spec).Return(assert.AnError) - useCase := NewApplyBlueprintSpecUseCase(repoMock, nil, nil, nil) - - err := useCase.PostProcessBlueprintApplication(testCtx, blueprintId) - - require.ErrorIs(t, err, assert.AnError) - }) -} diff --git a/pkg/application/applyDogusUseCase.go b/pkg/application/applyDogusUseCase.go new file mode 100644 index 00000000..bb427eb4 --- /dev/null +++ b/pkg/application/applyDogusUseCase.go @@ -0,0 +1,44 @@ +package application + +import ( + "context" + "errors" + "fmt" + + "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" +) + +// ApplyDogusUseCase can handle dogu installations, updates and deletions. +type ApplyDogusUseCase struct { + repo blueprintSpecRepository + doguInstallUseCase doguInstallationUseCase +} + +func NewApplyDogusUseCase( + repo blueprintSpecRepository, + doguInstallUseCase doguInstallationUseCase, +) *ApplyDogusUseCase { + return &ApplyDogusUseCase{ + repo: repo, + doguInstallUseCase: doguInstallUseCase, + } +} + +// ApplyDogus applies dogus if necessary. +// The conditions in the blueprint will be set accordingly. +// returns true if the dogus were applied, false if not. +// returns domainservice.ConflictError if there was a concurrent update to the blueprint or +// returns a domainservice.InternalError if there was an unspecified error while collecting or modifying the ecosystem state. +func (useCase *ApplyDogusUseCase) ApplyDogus(ctx context.Context, blueprint *domain.BlueprintSpec) (bool, error) { + err := useCase.doguInstallUseCase.ApplyDoguStates(ctx, blueprint) + isDogusApplied := blueprint.StateDiff.DoguDiffs.HasChanges() && err == nil + conditionChanged := blueprint.MarkDogusApplied(isDogusApplied, err) + + if isDogusApplied || conditionChanged { + updateErr := useCase.repo.Update(ctx, blueprint) + if updateErr != nil { + return isDogusApplied, fmt.Errorf("cannot update status while applying dogus: %w", errors.Join(updateErr, err)) + } + } + return isDogusApplied, err +} diff --git a/pkg/application/applyDogusUseCase_test.go b/pkg/application/applyDogusUseCase_test.go new file mode 100644 index 00000000..a66addb8 --- /dev/null +++ b/pkg/application/applyDogusUseCase_test.go @@ -0,0 +1,114 @@ +package application + +import ( + "testing" + + "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/api/meta" +) + +func TestApplyDogusUseCase_ApplyDogus(t *testing.T) { + t.Run("ok", func(t *testing.T) { + blueprint := &domain.BlueprintSpec{ + Conditions: []domain.Condition{}, + StateDiff: domain.StateDiff{ + DoguDiffs: domain.DoguDiffs{ + { + DoguName: "cas", + NeededActions: []domain.Action{ + domain.ActionUpgrade, + }, + }, + }, + }, + } + + repoMock := newMockBlueprintSpecRepository(t) + repoMock.EXPECT().Update(testCtx, blueprint).Return(nil) + doguInstallUseCaseMock := newMockDoguInstallationUseCase(t) + doguInstallUseCaseMock.EXPECT().ApplyDoguStates(testCtx, blueprint).Return(nil) + useCase := NewApplyDogusUseCase(repoMock, doguInstallUseCaseMock) + + changed, err := useCase.ApplyDogus(testCtx, blueprint) + + require.NoError(t, err) + assert.True(t, changed) + require.Equal(t, 1, len(blueprint.Events)) + assert.Equal(t, domain.DogusAppliedEvent{Diffs: blueprint.StateDiff.DoguDiffs}, blueprint.Events[0]) + require.Empty(t, blueprint.Conditions) + + }) + + t.Run("no update without condition change", func(t *testing.T) { + blueprint := &domain.BlueprintSpec{ + Conditions: []domain.Condition{}, + } + + repoMock := newMockBlueprintSpecRepository(t) + // Here is the important part: we expect the update to be called only once + repoMock.EXPECT().Update(testCtx, blueprint).Return(nil).Once() + doguInstallUseCaseMock := newMockDoguInstallationUseCase(t) + doguInstallUseCaseMock.EXPECT().ApplyDoguStates(testCtx, blueprint).Return(assert.AnError) + useCase := NewApplyDogusUseCase(repoMock, doguInstallUseCaseMock) + + dogusApplied, err := useCase.ApplyDogus(testCtx, blueprint) + require.Error(t, err) + assert.True(t, meta.IsStatusConditionFalse(blueprint.Conditions, domain.ConditionLastApplySucceeded)) + assert.False(t, dogusApplied) + require.Equal(t, 1, len(blueprint.Events)) + assert.Equal(t, domain.NewExecutionFailedEvent(err), blueprint.Events[0]) + // second apply + dogusApplied, err = useCase.ApplyDogus(testCtx, blueprint) + require.Error(t, err) + assert.True(t, meta.IsStatusConditionFalse(blueprint.Conditions, domain.ConditionLastApplySucceeded)) + assert.False(t, dogusApplied) + require.Equal(t, 1, len(blueprint.Events)) + }) + + t.Run("fail to apply dogus", func(t *testing.T) { + blueprint := &domain.BlueprintSpec{ + Conditions: []domain.Condition{}, + } + + repoMock := newMockBlueprintSpecRepository(t) + repoMock.EXPECT().Update(testCtx, blueprint).Return(nil) + doguInstallUseCaseMock := newMockDoguInstallationUseCase(t) + doguInstallUseCaseMock.EXPECT().ApplyDoguStates(testCtx, blueprint).Return(assert.AnError) + useCase := NewApplyDogusUseCase(repoMock, doguInstallUseCaseMock) + + changed, err := useCase.ApplyDogus(testCtx, blueprint) + + require.ErrorIs(t, err, assert.AnError) + assert.True(t, meta.IsStatusConditionFalse(blueprint.Conditions, domain.ConditionLastApplySucceeded)) + assert.False(t, changed) + require.Equal(t, 1, len(blueprint.Events)) + assert.Equal(t, domain.NewExecutionFailedEvent(err), blueprint.Events[0]) + }) + + t.Run("fail to update blueprint", func(t *testing.T) { + blueprint := &domain.BlueprintSpec{ + Conditions: []domain.Condition{}, + StateDiff: domain.StateDiff{ + DoguDiffs: domain.DoguDiffs{ + {NeededActions: []domain.Action{domain.ActionInstall}}, + }, + }, + } + + repoMock := newMockBlueprintSpecRepository(t) + repoMock.EXPECT().Update(testCtx, blueprint).Return(assert.AnError) + doguInstallUseCaseMock := newMockDoguInstallationUseCase(t) + doguInstallUseCaseMock.EXPECT().ApplyDoguStates(testCtx, blueprint).Return(nil) + useCase := NewApplyDogusUseCase(repoMock, doguInstallUseCaseMock) + + changed, err := useCase.ApplyDogus(testCtx, blueprint) + + require.ErrorIs(t, err, assert.AnError) + assert.Empty(t, blueprint.Conditions) + require.Equal(t, 1, len(blueprint.Events)) + assert.Equal(t, domain.DogusAppliedEvent{Diffs: blueprint.StateDiff.DoguDiffs}, blueprint.Events[0]) + assert.True(t, changed) + }) +} diff --git a/pkg/application/blueprintApplyUseCase.go b/pkg/application/blueprintApplyUseCase.go new file mode 100644 index 00000000..85f15109 --- /dev/null +++ b/pkg/application/blueprintApplyUseCase.go @@ -0,0 +1,66 @@ +package application + +import ( + "context" + + "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" +) + +type BlueprintApplyUseCase struct { + completeUseCase completeBlueprintUseCase + ecosystemConfigUseCase ecosystemConfigUseCase + applyDogusUseCase applyDogusUseCase + healthUseCase ecosystemHealthUseCase + dogusUpToDateUseCase dogusUpToDateUseCase +} + +func NewBlueprintApplyUseCase( + completeUseCase completeBlueprintUseCase, + ecosystemConfigUseCase ecosystemConfigUseCase, + applyDogusUseCase applyDogusUseCase, + healthUseCase ecosystemHealthUseCase, + dogusUpToDateUseCase dogusUpToDateUseCase, +) BlueprintApplyUseCase { + return BlueprintApplyUseCase{ + completeUseCase: completeUseCase, + ecosystemConfigUseCase: ecosystemConfigUseCase, + applyDogusUseCase: applyDogusUseCase, + healthUseCase: healthUseCase, + dogusUpToDateUseCase: dogusUpToDateUseCase, + } +} + +func (useCase *BlueprintApplyUseCase) applyBlueprint(ctx context.Context, blueprint *domain.BlueprintSpec) error { + err := useCase.ecosystemConfigUseCase.ApplyConfig(ctx, blueprint) + if err != nil { + return err + } + changedDogus, err := useCase.applyDogusUseCase.ApplyDogus(ctx, blueprint) + if err != nil { + return err + } + // check after installing or updating dogus + if changedDogus { + _, err = useCase.healthUseCase.CheckEcosystemHealth(ctx, blueprint) + if err != nil { + return err + } + } + + err = useCase.dogusUpToDateUseCase.CheckDogus(ctx, blueprint) + if err != nil { + // could be a domain.AwaitSelfUpgradeError to trigger another reconcile + return err + } + + // Only complete if there are no changes left + if blueprint.StateDiff.HasChanges() { + return &domain.StateDiffNotEmptyError{Message: "cannot complete blueprint because the StateDiff has still changes"} + } else { + err = useCase.completeUseCase.CompleteBlueprint(ctx, blueprint) + if err != nil { + return err + } + } + return nil +} diff --git a/pkg/application/blueprintPreparationUseCase.go b/pkg/application/blueprintPreparationUseCase.go new file mode 100644 index 00000000..365953d2 --- /dev/null +++ b/pkg/application/blueprintPreparationUseCase.go @@ -0,0 +1,63 @@ +package application + +import ( + "context" + + "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" +) + +type BlueprintPreparationUseCase struct { + initialStatus initialBlueprintStatusUseCase + validation blueprintSpecValidationUseCase + effectiveBlueprint effectiveBlueprintUseCase + stateDiff stateDiffUseCase + healthUseCase ecosystemHealthUseCase +} + +func NewBlueprintPreparationUseCase( + initialStatus initialBlueprintStatusUseCase, + validation blueprintSpecValidationUseCase, + effectiveBlueprint effectiveBlueprintUseCase, + stateDiff stateDiffUseCase, + ecosystemHealthUseCase ecosystemHealthUseCase, +) BlueprintPreparationUseCase { + return BlueprintPreparationUseCase{ + initialStatus: initialStatus, + validation: validation, + effectiveBlueprint: effectiveBlueprint, + stateDiff: stateDiff, + healthUseCase: ecosystemHealthUseCase, + } +} + +func (useCase *BlueprintPreparationUseCase) prepareBlueprint(ctx context.Context, blueprint *domain.BlueprintSpec) error { + err := useCase.initialStatus.InitateConditions(ctx, blueprint) + if err != nil { + return err + } + err = useCase.validation.ValidateBlueprintSpecStatically(ctx, blueprint) + if err != nil { + return err + } + err = useCase.effectiveBlueprint.CalculateEffectiveBlueprint(ctx, blueprint) + if err != nil { + return err + } + err = useCase.validation.ValidateBlueprintSpecDynamically(ctx, blueprint) + if err != nil { + return err + } + // always check health here, even if we already know here, that we don't need to apply anything + // because we need to update the health condition + _, err = useCase.healthUseCase.CheckEcosystemHealth(ctx, blueprint) + if err != nil { + return err + } + err = useCase.stateDiff.DetermineStateDiff(ctx, blueprint) + if err != nil { + // error could be either a technical error from a repository or an InvalidBlueprintError from the domain + // both cases can be handled the same way as the calling method (reconciler) can handle the error type itself. + return err + } + return nil +} diff --git a/pkg/application/blueprintSpecChangeUseCase.go b/pkg/application/blueprintSpecChangeUseCase.go index 68c69fd6..f0556b5d 100644 --- a/pkg/application/blueprintSpecChangeUseCase.go +++ b/pkg/application/blueprintSpecChangeUseCase.go @@ -4,220 +4,97 @@ import ( "context" "fmt" - "sigs.k8s.io/controller-runtime/pkg/log" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domainservice" + "github.com/go-logr/logr" + "sigs.k8s.io/controller-runtime/pkg/log" ) type BlueprintSpecChangeUseCase struct { - repo blueprintSpecRepository - validation blueprintSpecValidationUseCase - effectiveBlueprint effectiveBlueprintUseCase - stateDiff stateDiffUseCase - applyUseCase applyBlueprintSpecUseCase - ecosystemConfigUseCase ecosystemConfigUseCase - doguRestartUseCase doguRestartUseCase - selfUpgradeUseCase selfUpgradeUseCase + repo blueprintSpecRepository + preparationUseCase BlueprintPreparationUseCase + applyUseCase BlueprintApplyUseCase } func NewBlueprintSpecChangeUseCase( - repo domainservice.BlueprintSpecRepository, - validation blueprintSpecValidationUseCase, - effectiveBlueprint effectiveBlueprintUseCase, - stateDiff stateDiffUseCase, - applyUseCase applyBlueprintSpecUseCase, - ecosystemConfigUseCase ecosystemConfigUseCase, - doguRestartUseCase doguRestartUseCase, - selfUpgradeUseCase selfUpgradeUseCase, - + repo blueprintSpecRepository, + preparationUseCase BlueprintPreparationUseCase, + applyUseCase BlueprintApplyUseCase, ) *BlueprintSpecChangeUseCase { return &BlueprintSpecChangeUseCase{ - repo: repo, - validation: validation, - effectiveBlueprint: effectiveBlueprint, - stateDiff: stateDiff, - applyUseCase: applyUseCase, - ecosystemConfigUseCase: ecosystemConfigUseCase, - doguRestartUseCase: doguRestartUseCase, - selfUpgradeUseCase: selfUpgradeUseCase, + repo: repo, + preparationUseCase: preparationUseCase, + applyUseCase: applyUseCase, } } -// HandleChange further executes a blueprint spec given by the blueprintId until it is fully applied or an error occurred. +// HandleUntilApplied further executes a blueprint given by the blueprintId until it is as far applied as possible or an error occurred. +// If the process needs to wait for something, this function will return. +// Another call of this function is necessary to proceed. // Returns a domainservice.NotFoundError if the blueprintId does not correspond to a blueprintSpec or // a domainservice.InternalError if there is any error while loading or persisting the blueprintSpec or // a domainservice.ConflictError if there was a concurrent write or // a domain.InvalidBlueprintError if the blueprint is invalid. -func (useCase *BlueprintSpecChangeUseCase) HandleChange(ctx context.Context, blueprintId string) error { - logger := log.FromContext(ctx). - WithName("BlueprintSpecChangeUseCase.HandleChange"). +func (useCase *BlueprintSpecChangeUseCase) HandleUntilApplied(givenCtx context.Context, blueprintId string) error { + logger := log.FromContext(givenCtx). WithValues("blueprintId", blueprintId) + // set the logger in the context to make use of structured logging + // we will give this ctx in every use case, therefore all of them will include the values given here + ctx := log.IntoContext(givenCtx, logger) + logger = logger.WithName("BlueprintSpecChangeUseCase.HandleUntilApplied") - logger.Info("getting changed blueprint") // log with id - blueprintSpec, err := useCase.repo.GetById(ctx, blueprintId) + logger.V(2).Info("getting changed blueprint") // log with id + blueprint, err := useCase.repo.GetById(ctx, blueprintId) if err != nil { errMsg := "cannot load blueprint spec" logger.Error(err, errMsg) return fmt.Errorf("%s: %w", errMsg, err) } - logger = logger.WithValues("blueprintStatus", blueprintSpec.Status) - logger.Info("handle blueprint") // log with id and status values. - - // without any error, the blueprint spec is always ready to be further evaluated, therefore call this function again to do that. - switch blueprintSpec.Status { - case domain.StatusPhaseNew: - return useCase.validateStatically(ctx, blueprintId) - case domain.StatusPhaseInvalid: - return nil - case domain.StatusPhaseStaticallyValidated: - return useCase.calculateEffectiveBlueprint(ctx, blueprintId) - case domain.StatusPhaseEffectiveBlueprintGenerated: - return useCase.validateDynamically(ctx, blueprintId) - case domain.StatusPhaseValidated: - return useCase.determineStateDiff(ctx, blueprintId) - case domain.StatusPhaseStateDiffDetermined: - return useCase.checkEcosystemHealthUpfront(ctx, blueprintId) - case domain.StatusPhaseEcosystemHealthyUpfront: - return useCase.preProcessBlueprintApplication(ctx, blueprintSpec) - case domain.StatusPhaseEcosystemUnhealthyUpfront: - return nil - case domain.StatusPhaseBlueprintApplicationPreProcessed: - return useCase.handleSelfUpgrade(ctx, blueprintId) - case domain.StatusPhaseAwaitSelfUpgrade: - return useCase.handleSelfUpgrade(ctx, blueprintId) - case domain.StatusPhaseSelfUpgradeCompleted: - return useCase.applyEcosystemConfig(ctx, blueprintId) - case domain.StatusPhaseEcosystemConfigApplied: - return useCase.applyBlueprintSpec(ctx, blueprintId) - case domain.StatusPhaseApplyEcosystemConfigFailed: - return useCase.applyUseCase.PostProcessBlueprintApplication(ctx, blueprintId) - case domain.StatusPhaseInProgress: - // should only happen if the system was interrupted, normally this state will be updated to blueprintApplied or BlueprintApplicationFailed - return useCase.applyUseCase.PostProcessBlueprintApplication(ctx, blueprintId) - case domain.StatusPhaseBlueprintApplied: - return useCase.triggerDoguRestarts(ctx, blueprintId) - case domain.StatusPhaseRestartsTriggered: - return useCase.checkEcosystemHealthAfterwards(ctx, blueprintId) - case domain.StatusPhaseBlueprintApplicationFailed: - return useCase.applyUseCase.PostProcessBlueprintApplication(ctx, blueprintId) - case domain.StatusPhaseEcosystemHealthyAfterwards: - // censor and set status to completed - return useCase.applyUseCase.PostProcessBlueprintApplication(ctx, blueprintId) - case domain.StatusPhaseEcosystemUnhealthyAfterwards: - // censor and set status to failed - return useCase.applyUseCase.PostProcessBlueprintApplication(ctx, blueprintId) - case domain.StatusPhaseCompleted: - return nil - case domain.StatusPhaseFailed: - return nil - default: - return fmt.Errorf("could not handle unknown status of blueprint") - } -} - -func (useCase *BlueprintSpecChangeUseCase) validateStatically(ctx context.Context, blueprintId string) error { - err := useCase.validation.ValidateBlueprintSpecStatically(ctx, blueprintId) - if err != nil { - return err - } - - return useCase.HandleChange(ctx, blueprintId) -} - -func (useCase *BlueprintSpecChangeUseCase) calculateEffectiveBlueprint(ctx context.Context, blueprintId string) error { - err := useCase.effectiveBlueprint.CalculateEffectiveBlueprint(ctx, blueprintId) - if err != nil { - return err - } - - return useCase.HandleChange(ctx, blueprintId) -} - -func (useCase *BlueprintSpecChangeUseCase) validateDynamically(ctx context.Context, blueprintId string) error { - err := useCase.validation.ValidateBlueprintSpecDynamically(ctx, blueprintId) - if err != nil { - return err - } - - return useCase.HandleChange(ctx, blueprintId) -} - -func (useCase *BlueprintSpecChangeUseCase) determineStateDiff(ctx context.Context, blueprintId string) error { - err := useCase.stateDiff.DetermineStateDiff(ctx, blueprintId) - - // error could be either a technical error from a repository or an InvalidBlueprintError from the domain - // both cases can be handled the same way as the calling method (reconciler) can handle the error type itself. - if err != nil { - return err - } - - return useCase.HandleChange(ctx, blueprintId) -} + logger.V(1).Info("handle blueprint") -func (useCase *BlueprintSpecChangeUseCase) checkEcosystemHealthUpfront(ctx context.Context, blueprintId string) error { - err := useCase.applyUseCase.CheckEcosystemHealthUpfront(ctx, blueprintId) + err = useCase.preparationUseCase.prepareBlueprint(ctx, blueprint) if err != nil { return err } - return useCase.HandleChange(ctx, blueprintId) -} - -func (useCase *BlueprintSpecChangeUseCase) preProcessBlueprintApplication(ctx context.Context, blueprintSpec *domain.BlueprintSpec) error { - err := useCase.applyUseCase.PreProcessBlueprintApplication(ctx, blueprintSpec.Id) - if err != nil { - return err - } - if !blueprintSpec.ShouldBeApplied() { - // event recording and so on happen in PreProcessBlueprintApplication - // just stop the loop here on dry run or early exit - return nil - } - return useCase.HandleChange(ctx, blueprintSpec.Id) -} - -func (useCase *BlueprintSpecChangeUseCase) handleSelfUpgrade(ctx context.Context, blueprintId string) error { - err := useCase.selfUpgradeUseCase.HandleSelfUpgrade(ctx, blueprintId) - if err != nil { - return err + if !blueprint.ShouldBeApplied() { + // stop the loop here on stopped-flag or early exit + return useCase.handleShouldNotBeApplied(ctx, logger, blueprint) } - return useCase.HandleChange(ctx, blueprintId) -} -func (useCase *BlueprintSpecChangeUseCase) applyBlueprintSpec(ctx context.Context, blueprintId string) error { - err := useCase.applyUseCase.ApplyBlueprintSpec(ctx, blueprintId) + // === Apply from here on === + err = useCase.applyUseCase.applyBlueprint(ctx, blueprint) if err != nil { return err } - return useCase.HandleChange(ctx, blueprintId) + logger.Info("blueprint successfully applied") + return nil } -func (useCase *BlueprintSpecChangeUseCase) checkEcosystemHealthAfterwards(ctx context.Context, blueprintId string) error { - err := useCase.applyUseCase.CheckEcosystemHealthAfterwards(ctx, blueprintId) - if err != nil { - return err +func (useCase *BlueprintSpecChangeUseCase) handleShouldNotBeApplied(ctx context.Context, logger logr.Logger, blueprint *domain.BlueprintSpec) error { + // post event and log only if blueprint is stopped, all other cases are just NoOps + if blueprint.Config.Stopped { + logger.Info("blueprint is currently set as stopped and will not be applied") + blueprint.MarkBlueprintStopped() + err := useCase.repo.Update(ctx, blueprint) + if err != nil { + return fmt.Errorf("cannot update status to set stopped event: %w", err) + } + } else { + logger.V(1).Info("no diff detected, no changes required") } - - return useCase.HandleChange(ctx, blueprintId) + return nil } -func (useCase *BlueprintSpecChangeUseCase) applyEcosystemConfig(ctx context.Context, blueprintId string) error { - err := useCase.ecosystemConfigUseCase.ApplyConfig(ctx, blueprintId) - if err != nil { - return err - } +func (useCase *BlueprintSpecChangeUseCase) CheckForMultipleBlueprintResources(ctx context.Context) error { + logger := log.FromContext(ctx).WithName("BlueprintSpecChangeUseCase.CheckForMultipleBlueprintResources") - return useCase.HandleChange(ctx, blueprintId) -} - -func (useCase *BlueprintSpecChangeUseCase) triggerDoguRestarts(ctx context.Context, blueprintId string) error { - err := useCase.doguRestartUseCase.TriggerDoguRestarts(ctx, blueprintId) + logger.V(2).Info("check for multiple blueprints") + err := useCase.repo.CheckSingleton(ctx) if err != nil { - return err + return fmt.Errorf("%s: %w", "check for multiple blueprints not successful", err) } - return useCase.HandleChange(ctx, blueprintId) + return nil } diff --git a/pkg/application/blueprintSpecChangeUseCase_test.go b/pkg/application/blueprintSpecChangeUseCase_test.go index 425bdd41..b8130985 100644 --- a/pkg/application/blueprintSpecChangeUseCase_test.go +++ b/pkg/application/blueprintSpecChangeUseCase_test.go @@ -2,696 +2,457 @@ package application import ( "context" - "errors" - cescommons "github.com/cloudogu/ces-commons-lib/dogu" "testing" + "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" + "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" + "github.com/go-logr/logr" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domainservice" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -var testCtx = context.Background() -var testBlueprintId = "testBlueprint1" - -func TestBlueprintSpecChangeUseCase_HandleChange(t *testing.T) { - - t.Run("do all steps with blueprint", func(t *testing.T) { - // given - repoMock := newMockBlueprintSpecRepository(t) - validationMock := newMockBlueprintSpecValidationUseCase(t) - effectiveBlueprintMock := newMockEffectiveBlueprintUseCase(t) - stateDiffMock := newMockStateDiffUseCase(t) - applyMock := newMockApplyBlueprintSpecUseCase(t) - ecosystemConfigUseCaseMock := newMockEcosystemConfigUseCase(t) - doguRestartUseCaseMock := newMockDoguRestartUseCase(t) - selfUpgradeUseCase := newMockSelfUpgradeUseCase(t) - useCase := NewBlueprintSpecChangeUseCase(repoMock, validationMock, effectiveBlueprintMock, stateDiffMock, applyMock, ecosystemConfigUseCaseMock, doguRestartUseCaseMock, selfUpgradeUseCase) - - blueprintSpec := &domain.BlueprintSpec{ - Id: testBlueprintId, - Status: domain.StatusPhaseNew, - } - repoMock.EXPECT().GetById(testCtx, testBlueprintId).Return(blueprintSpec, nil) - validationMock.EXPECT().ValidateBlueprintSpecStatically(testCtx, testBlueprintId).Return(nil). - Run(func(ctx context.Context, blueprintId string) { - blueprintSpec.Status = domain.StatusPhaseStaticallyValidated - }) - effectiveBlueprintMock.EXPECT().CalculateEffectiveBlueprint(testCtx, testBlueprintId).Return(nil). - Run(func(ctx context.Context, blueprintId string) { - blueprintSpec.Status = domain.StatusPhaseEffectiveBlueprintGenerated - }) - validationMock.EXPECT().ValidateBlueprintSpecDynamically(testCtx, testBlueprintId).Return(nil). - Run(func(ctx context.Context, blueprintId string) { - blueprintSpec.Status = domain.StatusPhaseValidated - }) - stateDiffMock.EXPECT().DetermineStateDiff(testCtx, testBlueprintId).Return(nil). - Run(func(ctx context.Context, blueprintId string) { - blueprintSpec.Status = domain.StatusPhaseStateDiffDetermined - }) - applyMock.EXPECT().CheckEcosystemHealthUpfront(testCtx, testBlueprintId).Return(nil). - Run(func(ctx context.Context, blueprintId string) { - blueprintSpec.Status = domain.StatusPhaseEcosystemHealthyUpfront - }) - applyMock.EXPECT().PreProcessBlueprintApplication(testCtx, testBlueprintId).Return(nil). - Run(func(ctx context.Context, blueprintId string) { - blueprintSpec.Status = domain.StatusPhaseBlueprintApplicationPreProcessed - }) - ecosystemConfigUseCaseMock.EXPECT().ApplyConfig(testCtx, testBlueprintId).Return(nil).Run(func(ctx context.Context, blueprintId string) { - blueprintSpec.Status = domain.StatusPhaseEcosystemConfigApplied - }) - selfUpgradeUseCase.EXPECT().HandleSelfUpgrade(testCtx, "testBlueprint1").Return(nil).Run(func(ctx context.Context, blueprintId string) { - blueprintSpec.Status = domain.StatusPhaseSelfUpgradeCompleted - }) - applyMock.EXPECT().ApplyBlueprintSpec(testCtx, testBlueprintId).Return(nil). - Run(func(ctx context.Context, blueprintId string) { - blueprintSpec.Status = domain.StatusPhaseBlueprintApplied - }) - applyMock.EXPECT().CheckEcosystemHealthAfterwards(testCtx, testBlueprintId).Return(nil). - Run(func(ctx context.Context, blueprintId string) { - blueprintSpec.Status = domain.StatusPhaseEcosystemHealthyAfterwards - }) - applyMock.EXPECT().PostProcessBlueprintApplication(testCtx, testBlueprintId).Return(nil). - Run(func(ctx context.Context, blueprintId string) { - blueprintSpec.Status = domain.StatusPhaseCompleted - }) - doguRestartUseCaseMock.EXPECT().TriggerDoguRestarts(testCtx, testBlueprintId).Return(nil). - Run(func(ctx context.Context, blueprintId string) { - blueprintSpec.Status = domain.StatusPhaseRestartsTriggered - }) - - // when - err := useCase.HandleChange(testCtx, testBlueprintId) - // then - require.NoError(t, err) - assert.Equal(t, domain.StatusPhaseCompleted, blueprintSpec.Status) - }) - - t.Run("cannot load blueprint spec initially", func(t *testing.T) { - // given - repoMock := newMockBlueprintSpecRepository(t) - validationMock := newMockBlueprintSpecValidationUseCase(t) - effectiveBlueprintMock := newMockEffectiveBlueprintUseCase(t) - stateDiffMock := newMockStateDiffUseCase(t) - applyMock := newMockApplyBlueprintSpecUseCase(t) - ecosystemConfigUseCaseMock := newMockEcosystemConfigUseCase(t) - doguRestartUseCaseMock := newMockDoguRestartUseCase(t) - selfUpgradeUseCase := newMockSelfUpgradeUseCase(t) - useCase := NewBlueprintSpecChangeUseCase(repoMock, validationMock, effectiveBlueprintMock, stateDiffMock, applyMock, ecosystemConfigUseCaseMock, doguRestartUseCaseMock, selfUpgradeUseCase) - - expectedError := &domainservice.InternalError{ - WrappedError: nil, - Message: "test-error", - } - repoMock.EXPECT().GetById(testCtx, blueprintId).Return(nil, expectedError) - - // when - err := useCase.HandleChange(testCtx, blueprintId) - - // then - require.Error(t, err) - assert.ErrorIs(t, err, expectedError) - }) - - t.Run("new with static validation error", func(t *testing.T) { - // given - repoMock := newMockBlueprintSpecRepository(t) - validationMock := newMockBlueprintSpecValidationUseCase(t) - effectiveBlueprintMock := newMockEffectiveBlueprintUseCase(t) - stateDiffMock := newMockStateDiffUseCase(t) - applyMock := newMockApplyBlueprintSpecUseCase(t) - ecosystemConfigUseCaseMock := newMockEcosystemConfigUseCase(t) - doguRestartUseCaseMock := newMockDoguRestartUseCase(t) - selfUpgradeUseCase := newMockSelfUpgradeUseCase(t) - useCase := NewBlueprintSpecChangeUseCase(repoMock, validationMock, effectiveBlueprintMock, stateDiffMock, applyMock, ecosystemConfigUseCaseMock, doguRestartUseCaseMock, selfUpgradeUseCase) - - repoMock.EXPECT().GetById(testCtx, "testBlueprint1").Return(&domain.BlueprintSpec{ - Id: "testBlueprint1", - Status: domain.StatusPhaseNew, - Blueprint: domain.Blueprint{Dogus: []domain.Dogu{ - {Name: cescommons.QualifiedName{Namespace: "official", SimpleName: "DoguWithNoVersion"}, TargetState: domain.TargetStatePresent}, - }}, - }, nil) - validationMock.EXPECT().ValidateBlueprintSpecStatically(testCtx, "testBlueprint1").Return(assert.AnError) - - // when - err := useCase.HandleChange(testCtx, "testBlueprint1") +var ( + testCtx = context.Background() + testBlueprintId = "testBlueprint1" + testBlueprintSpec = &domain.BlueprintSpec{ + Id: testBlueprintId, + StateDiff: domain.StateDiff{DoguDiffs: domain.DoguDiffs{{NeededActions: []domain.Action{domain.ActionInstall}}}}, + } + testBlueprintSpecEmptyDiff = &domain.BlueprintSpec{ + Id: testBlueprintId, + } + testStoppedBlueprintSpec = &domain.BlueprintSpec{ + Id: testBlueprintId, + Config: domain.BlueprintConfiguration{Stopped: true}, + } + testCompletedBlueprintSpec = &domain.BlueprintSpec{ + Id: testBlueprintId, + Conditions: []domain.Condition{{ + Type: domain.ConditionCompleted, + Status: metav1.ConditionTrue, + }}, + } +) - // then - require.Error(t, err) - assert.ErrorIs(t, err, assert.AnError) - }) +func TestNewBlueprintSpecChangeUseCase(t *testing.T) { + // given + mocks := createAllMocks(t) + preparationUseCases := NewBlueprintPreparationUseCase( + mocks.initialStatus, + mocks.validation, + mocks.effectiveBlueprint, + mocks.stateDiff, + mocks.ecosystemHealth, + ) + applyUseCases := NewBlueprintApplyUseCase( + mocks.completeBlueprint, + mocks.ecosystemConfig, + mocks.applyDogus, + mocks.ecosystemHealth, + mocks.dogusUpToDate, + ) + + // when + result := NewBlueprintSpecChangeUseCase(mocks.repo, preparationUseCases, applyUseCases) + + // then + require.NotNil(t, result) + assert.Equal(t, mocks.repo, result.repo) + assertPreparationUseCases(t, result.preparationUseCase, mocks) + assertApplyUseCases(t, result.applyUseCase, mocks) +} - t.Run("new with error calculating effective blueprint", func(t *testing.T) { - // given - repoMock := newMockBlueprintSpecRepository(t) - validationMock := newMockBlueprintSpecValidationUseCase(t) - effectiveBlueprintMock := newMockEffectiveBlueprintUseCase(t) - stateDiffMock := newMockStateDiffUseCase(t) - applyMock := newMockApplyBlueprintSpecUseCase(t) - ecosystemConfigUseCaseMock := newMockEcosystemConfigUseCase(t) - doguRestartUseCaseMock := newMockDoguRestartUseCase(t) - selfUpgradeUseCase := newMockSelfUpgradeUseCase(t) - useCase := NewBlueprintSpecChangeUseCase(repoMock, validationMock, effectiveBlueprintMock, stateDiffMock, applyMock, ecosystemConfigUseCaseMock, doguRestartUseCaseMock, selfUpgradeUseCase) - - updatedSpec := &domain.BlueprintSpec{ - Id: "testBlueprint1", - Status: domain.StatusPhaseNew, - } +type allMocks struct { + repo *mockBlueprintSpecRepository + initialStatus *mockInitialBlueprintStatusUseCase + validation *mockBlueprintSpecValidationUseCase + effectiveBlueprint *mockEffectiveBlueprintUseCase + stateDiff *mockStateDiffUseCase + completeBlueprint *mockCompleteBlueprintUseCase + ecosystemConfig *mockEcosystemConfigUseCase + applyDogus *mockApplyDogusUseCase + ecosystemHealth *mockEcosystemHealthUseCase + dogusUpToDate *mockDogusUpToDateUseCase +} - repoMock.EXPECT().GetById(testCtx, "testBlueprint1").Return(updatedSpec, nil) - validationMock.EXPECT().ValidateBlueprintSpecStatically(testCtx, "testBlueprint1").Return(nil). - Run(func(ctx context.Context, blueprintId string) { - updatedSpec.Status = domain.StatusPhaseStaticallyValidated - }) - effectiveBlueprintMock.EXPECT().CalculateEffectiveBlueprint(testCtx, "testBlueprint1").Return(assert.AnError) +func createAllMocks(t *testing.T) *allMocks { + return &allMocks{ + repo: newMockBlueprintSpecRepository(t), + initialStatus: newMockInitialBlueprintStatusUseCase(t), + validation: newMockBlueprintSpecValidationUseCase(t), + effectiveBlueprint: newMockEffectiveBlueprintUseCase(t), + stateDiff: newMockStateDiffUseCase(t), + completeBlueprint: newMockCompleteBlueprintUseCase(t), + ecosystemConfig: newMockEcosystemConfigUseCase(t), + applyDogus: newMockApplyDogusUseCase(t), + ecosystemHealth: newMockEcosystemHealthUseCase(t), + dogusUpToDate: newMockDogusUpToDateUseCase(t), + } +} - // when - err := useCase.HandleChange(testCtx, "testBlueprint1") +func assertPreparationUseCases(t *testing.T, useCases BlueprintPreparationUseCase, mocks *allMocks) { + assert.Equal(t, mocks.validation, useCases.validation) + assert.Equal(t, mocks.effectiveBlueprint, useCases.effectiveBlueprint) + assert.Equal(t, mocks.stateDiff, useCases.stateDiff) + assert.Equal(t, mocks.ecosystemHealth, useCases.healthUseCase) +} - // then - require.Error(t, err) - assert.ErrorIs(t, err, assert.AnError) - }) +func assertApplyUseCases(t *testing.T, useCases BlueprintApplyUseCase, mocks *allMocks) { + assert.Equal(t, mocks.ecosystemConfig, useCases.ecosystemConfigUseCase) + assert.Equal(t, mocks.applyDogus, useCases.applyDogusUseCase) + assert.Equal(t, mocks.completeBlueprint, useCases.completeUseCase) + assert.Equal(t, mocks.ecosystemHealth, useCases.healthUseCase) + assert.Equal(t, mocks.dogusUpToDate, useCases.dogusUpToDateUseCase) +} - t.Run("new with dynamic validation error", func(t *testing.T) { +func TestBlueprintSpecChangeUseCase_HandleUntilApplied_RepositoryErrors(t *testing.T) { + t.Run("should return error on error getting blueprint by id", func(t *testing.T) { // given - repoMock := newMockBlueprintSpecRepository(t) - validationMock := newMockBlueprintSpecValidationUseCase(t) - effectiveBlueprintMock := newMockEffectiveBlueprintUseCase(t) - stateDiffMock := newMockStateDiffUseCase(t) - applyMock := newMockApplyBlueprintSpecUseCase(t) - ecosystemConfigUseCaseMock := newMockEcosystemConfigUseCase(t) - doguRestartUseCaseMock := newMockDoguRestartUseCase(t) - selfUpgradeUseCase := newMockSelfUpgradeUseCase(t) - useCase := NewBlueprintSpecChangeUseCase(repoMock, validationMock, effectiveBlueprintMock, stateDiffMock, applyMock, ecosystemConfigUseCaseMock, doguRestartUseCaseMock, selfUpgradeUseCase) - - updatedSpec := &domain.BlueprintSpec{ - Id: "testBlueprint1", - Status: domain.StatusPhaseNew, - } + mocks := createAllMocks(t) + mocks.repo.EXPECT().GetById(mock.Anything, testBlueprintId).Return(nil, assert.AnError).Run(func(ctx context.Context, blueprintId string) { + logger, err := logr.FromContext(ctx) + require.NoError(t, err) + assert.NotNil(t, logger) + }) - repoMock.EXPECT().GetById(testCtx, "testBlueprint1").Return(updatedSpec, nil) - validationMock.EXPECT().ValidateBlueprintSpecStatically(testCtx, "testBlueprint1").Return(nil). - Run(func(ctx context.Context, blueprintId string) { - updatedSpec.Status = domain.StatusPhaseStaticallyValidated - }) - effectiveBlueprintMock.EXPECT().CalculateEffectiveBlueprint(testCtx, "testBlueprint1").Return(nil). - Run(func(ctx context.Context, blueprintId string) { - updatedSpec.Status = domain.StatusPhaseEffectiveBlueprintGenerated - }) - validationMock.EXPECT().ValidateBlueprintSpecDynamically(testCtx, "testBlueprint1").Return(assert.AnError) + useCase := createUseCase(mocks) // when - err := useCase.HandleChange(testCtx, "testBlueprint1") + err := useCase.HandleUntilApplied(testCtx, testBlueprintId) // then - require.Error(t, err) - assert.ErrorIs(t, err, assert.AnError) + assert.ErrorContains(t, err, "cannot load blueprint spec") }) +} - t.Run("new with error determining state diff", func(t *testing.T) { - // given - repoMock := newMockBlueprintSpecRepository(t) - validationMock := newMockBlueprintSpecValidationUseCase(t) - effectiveBlueprintMock := newMockEffectiveBlueprintUseCase(t) - stateDiffMock := newMockStateDiffUseCase(t) - applyMock := newMockApplyBlueprintSpecUseCase(t) - ecosystemConfigUseCaseMock := newMockEcosystemConfigUseCase(t) - doguRestartUseCaseMock := newMockDoguRestartUseCase(t) - selfUpgradeUseCase := newMockSelfUpgradeUseCase(t) - useCase := NewBlueprintSpecChangeUseCase(repoMock, validationMock, effectiveBlueprintMock, stateDiffMock, applyMock, ecosystemConfigUseCaseMock, doguRestartUseCaseMock, selfUpgradeUseCase) - - updatedSpec := &domain.BlueprintSpec{ - Id: "testBlueprint1", - Status: domain.StatusPhaseNew, - } - - repoMock.EXPECT().GetById(testCtx, "testBlueprint1").Return(updatedSpec, nil) - validationMock.EXPECT().ValidateBlueprintSpecStatically(testCtx, "testBlueprint1").Return(nil). - Run(func(ctx context.Context, blueprintId string) { - updatedSpec.Status = domain.StatusPhaseStaticallyValidated - }) - effectiveBlueprintMock.EXPECT().CalculateEffectiveBlueprint(testCtx, "testBlueprint1").Return(nil). - Run(func(ctx context.Context, blueprintId string) { - updatedSpec.Status = domain.StatusPhaseEffectiveBlueprintGenerated - }) - validationMock.EXPECT().ValidateBlueprintSpecDynamically(testCtx, "testBlueprint1").Return(nil). - Run(func(ctx context.Context, blueprintId string) { - updatedSpec.Status = domain.StatusPhaseValidated - }) - stateDiffMock.EXPECT().DetermineStateDiff(testCtx, "testBlueprint1").Return(assert.AnError) - // when - err := useCase.HandleChange(testCtx, "testBlueprint1") - - // then - require.Error(t, err) - assert.ErrorIs(t, err, assert.AnError) - }) +func TestBlueprintSpecChangeUseCase_HandleUntilApplied_PreparationPhaseErrors(t *testing.T) { + tests := []struct { + name string + setupMocks func(*allMocks) + wantErrTest func(*testing.T, error) + }{ + { + name: "should return error on error initially setting the blueprint status", + setupMocks: func(mocks *allMocks) { + mocks.repo.EXPECT().GetById(mock.Anything, testBlueprintId).Return(testBlueprintSpec, nil) + mocks.initialStatus.EXPECT().InitateConditions(mock.Anything, testBlueprintSpec).Return(assert.AnError) + }, + wantErrTest: func(t *testing.T, err error) { + assert.Error(t, err) + }, + }, + { + name: "should return error on error validating blueprint statically", + setupMocks: func(mocks *allMocks) { + mocks.repo.EXPECT().GetById(mock.Anything, testBlueprintId).Return(testBlueprintSpec, nil) + mocks.initialStatus.EXPECT().InitateConditions(mock.Anything, mock.Anything).Return(nil) + mocks.validation.EXPECT().ValidateBlueprintSpecStatically(mock.Anything, testBlueprintSpec).Return(assert.AnError) + }, + wantErrTest: func(t *testing.T, err error) { + assert.Error(t, err) + }, + }, + { + name: "should return error on error calculate effective blueprint", + setupMocks: func(mocks *allMocks) { + mocks.repo.EXPECT().GetById(mock.Anything, testBlueprintId).Return(testBlueprintSpec, nil) + mocks.initialStatus.EXPECT().InitateConditions(mock.Anything, mock.Anything).Return(nil) + mocks.validation.EXPECT().ValidateBlueprintSpecStatically(mock.Anything, mock.Anything).Return(nil) + mocks.effectiveBlueprint.EXPECT().CalculateEffectiveBlueprint(mock.Anything, testBlueprintSpec).Return(assert.AnError) + }, + wantErrTest: func(t *testing.T, err error) { + assert.Error(t, err) + }, + }, + { + name: "should return error on error validating blueprint dynamically", + setupMocks: func(mocks *allMocks) { + mocks.repo.EXPECT().GetById(mock.Anything, testBlueprintId).Return(testBlueprintSpec, nil) + mocks.initialStatus.EXPECT().InitateConditions(mock.Anything, mock.Anything).Return(nil) + mocks.validation.EXPECT().ValidateBlueprintSpecStatically(mock.Anything, mock.Anything).Return(nil) + mocks.effectiveBlueprint.EXPECT().CalculateEffectiveBlueprint(mock.Anything, testBlueprintSpec).Return(nil) + mocks.validation.EXPECT().ValidateBlueprintSpecDynamically(mock.Anything, testBlueprintSpec).Return(assert.AnError) + }, + wantErrTest: func(t *testing.T, err error) { + assert.Error(t, err) + }, + }, + { + name: "should return error on error checking ecosystem health", + setupMocks: func(mocks *allMocks) { + mocks.repo.EXPECT().GetById(mock.Anything, testBlueprintId).Return(testBlueprintSpec, nil) + mocks.initialStatus.EXPECT().InitateConditions(mock.Anything, mock.Anything).Return(nil) + mocks.validation.EXPECT().ValidateBlueprintSpecStatically(mock.Anything, mock.Anything).Return(nil) + mocks.effectiveBlueprint.EXPECT().CalculateEffectiveBlueprint(mock.Anything, testBlueprintSpec).Return(nil) + mocks.validation.EXPECT().ValidateBlueprintSpecDynamically(mock.Anything, testBlueprintSpec).Return(nil) + mocks.ecosystemHealth.EXPECT().CheckEcosystemHealth(mock.Anything, testBlueprintSpec).Return(ecosystem.HealthResult{}, assert.AnError) + }, + wantErrTest: func(t *testing.T, err error) { + assert.Error(t, err) + }, + }, + { + name: "should return error on error determining state diff", + setupMocks: func(mocks *allMocks) { + mocks.repo.EXPECT().GetById(mock.Anything, testBlueprintId).Return(testBlueprintSpec, nil) + mocks.initialStatus.EXPECT().InitateConditions(mock.Anything, mock.Anything).Return(nil) + mocks.validation.EXPECT().ValidateBlueprintSpecStatically(mock.Anything, mock.Anything).Return(nil) + mocks.effectiveBlueprint.EXPECT().CalculateEffectiveBlueprint(mock.Anything, testBlueprintSpec).Return(nil) + mocks.validation.EXPECT().ValidateBlueprintSpecDynamically(mock.Anything, testBlueprintSpec).Return(nil) + mocks.ecosystemHealth.EXPECT().CheckEcosystemHealth(mock.Anything, testBlueprintSpec).Return(ecosystem.HealthResult{}, nil) + mocks.stateDiff.EXPECT().DetermineStateDiff(mock.Anything, testBlueprintSpec).Return(assert.AnError) + }, + wantErrTest: func(t *testing.T, err error) { + assert.Error(t, err) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mocks := createAllMocks(t) + tt.setupMocks(mocks) + useCase := createUseCase(mocks) + + err := useCase.HandleUntilApplied(testCtx, testBlueprintId) + tt.wantErrTest(t, err) + }) + } +} - t.Run("new with error checking dogu health", func(t *testing.T) { +func TestBlueprintSpecChangeUseCase_HandleUntilApplied_SpecialBlueprintStates(t *testing.T) { + t.Run("should handle stopped blueprint", func(t *testing.T) { // given - repoMock := newMockBlueprintSpecRepository(t) - validationMock := newMockBlueprintSpecValidationUseCase(t) - effectiveBlueprintMock := newMockEffectiveBlueprintUseCase(t) - stateDiffMock := newMockStateDiffUseCase(t) - applyMock := newMockApplyBlueprintSpecUseCase(t) - ecosystemConfigUseCaseMock := newMockEcosystemConfigUseCase(t) - doguRestartUseCaseMock := newMockDoguRestartUseCase(t) - selfUpgradeUseCase := newMockSelfUpgradeUseCase(t) - useCase := NewBlueprintSpecChangeUseCase(repoMock, validationMock, effectiveBlueprintMock, stateDiffMock, applyMock, ecosystemConfigUseCaseMock, doguRestartUseCaseMock, selfUpgradeUseCase) - - updatedSpec := &domain.BlueprintSpec{ - Id: "testBlueprint1", - Status: domain.StatusPhaseNew, - } + mocks := createAllMocks(t) + mocks.repo.EXPECT().GetById(mock.Anything, testBlueprintId).Return(testStoppedBlueprintSpec, nil) + setupSuccessfulPreparationPhase(mocks, testStoppedBlueprintSpec) + mocks.repo.EXPECT().Update(mock.Anything, mock.Anything).Run(func(ctx context.Context, blueprint *domain.BlueprintSpec) { + assert.Len(t, blueprint.Events, 1) + assert.Equal(t, domain.BlueprintStoppedEvent{}.Name(), blueprint.Events[0].Name()) + }).Return(nil) - repoMock.EXPECT().GetById(testCtx, "testBlueprint1").Return(updatedSpec, nil) - validationMock.EXPECT().ValidateBlueprintSpecStatically(testCtx, "testBlueprint1").Return(nil). - Run(func(ctx context.Context, blueprintId string) { - updatedSpec.Status = domain.StatusPhaseStaticallyValidated - }) - effectiveBlueprintMock.EXPECT().CalculateEffectiveBlueprint(testCtx, "testBlueprint1").Return(nil). - Run(func(ctx context.Context, blueprintId string) { - updatedSpec.Status = domain.StatusPhaseEffectiveBlueprintGenerated - }) - validationMock.EXPECT().ValidateBlueprintSpecDynamically(testCtx, "testBlueprint1").Return(nil). - Run(func(ctx context.Context, blueprintId string) { - updatedSpec.Status = domain.StatusPhaseValidated - }) - stateDiffMock.EXPECT().DetermineStateDiff(testCtx, "testBlueprint1").Return(nil). - Run(func(ctx context.Context, blueprintId string) { - updatedSpec.Status = domain.StatusPhaseStateDiffDetermined - }) - applyMock.EXPECT().CheckEcosystemHealthUpfront(testCtx, "testBlueprint1").Return(assert.AnError) + useCase := createUseCase(mocks) // when - err := useCase.HandleChange(testCtx, "testBlueprint1") - - // then - require.Error(t, err) - assert.ErrorIs(t, err, assert.AnError) - }) + err := useCase.HandleUntilApplied(testCtx, testBlueprintId) - t.Run("handle invalid blueprint", func(t *testing.T) { - // given - repoMock := newMockBlueprintSpecRepository(t) - validationMock := newMockBlueprintSpecValidationUseCase(t) - effectiveBlueprintMock := newMockEffectiveBlueprintUseCase(t) - stateDiffMock := newMockStateDiffUseCase(t) - applyMock := newMockApplyBlueprintSpecUseCase(t) - ecosystemConfigUseCaseMock := newMockEcosystemConfigUseCase(t) - doguRestartUseCaseMock := newMockDoguRestartUseCase(t) - selfUpgradeUseCase := newMockSelfUpgradeUseCase(t) - useCase := NewBlueprintSpecChangeUseCase(repoMock, validationMock, effectiveBlueprintMock, stateDiffMock, applyMock, ecosystemConfigUseCaseMock, doguRestartUseCaseMock, selfUpgradeUseCase) - - repoMock.EXPECT().GetById(testCtx, "testBlueprint1").Return(&domain.BlueprintSpec{ - Id: "testBlueprint1", - Status: domain.StatusPhaseInvalid, - }, nil) - // when - err := useCase.HandleChange(testCtx, "testBlueprint1") - // then - require.NoError(t, err) - }) - - t.Run("handle unhealthy ecosystem upfront", func(t *testing.T) { - // given - repoMock := newMockBlueprintSpecRepository(t) - validationMock := newMockBlueprintSpecValidationUseCase(t) - effectiveBlueprintMock := newMockEffectiveBlueprintUseCase(t) - stateDiffMock := newMockStateDiffUseCase(t) - applyMock := newMockApplyBlueprintSpecUseCase(t) - ecosystemConfigUseCaseMock := newMockEcosystemConfigUseCase(t) - doguRestartUseCaseMock := newMockDoguRestartUseCase(t) - selfUpgradeUseCase := newMockSelfUpgradeUseCase(t) - useCase := NewBlueprintSpecChangeUseCase(repoMock, validationMock, effectiveBlueprintMock, stateDiffMock, applyMock, ecosystemConfigUseCaseMock, doguRestartUseCaseMock, selfUpgradeUseCase) - - repoMock.EXPECT().GetById(testCtx, "testBlueprint1").Return(&domain.BlueprintSpec{ - Id: "testBlueprint1", - Status: domain.StatusPhaseEcosystemUnhealthyUpfront, - }, nil) - // when - err := useCase.HandleChange(testCtx, "testBlueprint1") - // then - require.NoError(t, err) - }) - - t.Run("handle unhealthy ecosystem upfront", func(t *testing.T) { - // given - repoMock := newMockBlueprintSpecRepository(t) - validationMock := newMockBlueprintSpecValidationUseCase(t) - effectiveBlueprintMock := newMockEffectiveBlueprintUseCase(t) - stateDiffMock := newMockStateDiffUseCase(t) - applyMock := newMockApplyBlueprintSpecUseCase(t) - ecosystemConfigUseCaseMock := newMockEcosystemConfigUseCase(t) - doguRestartUseCaseMock := newMockDoguRestartUseCase(t) - selfUpgradeUseCase := newMockSelfUpgradeUseCase(t) - useCase := NewBlueprintSpecChangeUseCase(repoMock, validationMock, effectiveBlueprintMock, stateDiffMock, applyMock, ecosystemConfigUseCaseMock, doguRestartUseCaseMock, selfUpgradeUseCase) - - repoMock.EXPECT().GetById(testCtx, "testBlueprint1").Return(&domain.BlueprintSpec{ - Id: "testBlueprint1", - Status: domain.StatusPhaseEcosystemUnhealthyUpfront, - }, nil) - // when - err := useCase.HandleChange(testCtx, "testBlueprint1") // then - require.NoError(t, err) + assert.NoError(t, err) }) - t.Run("handle error apply registry config", func(t *testing.T) { + t.Run("should not apply completed blueprint with no diff", func(t *testing.T) { // given - repoMock := newMockBlueprintSpecRepository(t) - validationMock := newMockBlueprintSpecValidationUseCase(t) - effectiveBlueprintMock := newMockEffectiveBlueprintUseCase(t) - stateDiffMock := newMockStateDiffUseCase(t) - applyMock := newMockApplyBlueprintSpecUseCase(t) - ecosystemConfigUseCaseMock := newMockEcosystemConfigUseCase(t) - doguRestartUseCaseMock := newMockDoguRestartUseCase(t) - selfUpgradeUseCase := newMockSelfUpgradeUseCase(t) - useCase := NewBlueprintSpecChangeUseCase(repoMock, validationMock, effectiveBlueprintMock, stateDiffMock, applyMock, ecosystemConfigUseCaseMock, doguRestartUseCaseMock, selfUpgradeUseCase) - - blueprintSpec := &domain.BlueprintSpec{ - Id: "testBlueprint1", - Status: domain.StatusPhaseSelfUpgradeCompleted, - } - repoMock.EXPECT().GetById(testCtx, "testBlueprint1").Return(blueprintSpec, nil) - ecosystemConfigUseCaseMock.EXPECT().ApplyConfig(testCtx, blueprintSpec.Id).Return(assert.AnError) - // when - actualErr := useCase.HandleChange(testCtx, "testBlueprint1") - // then - require.ErrorIs(t, actualErr, assert.AnError) - }) + mocks := createAllMocks(t) + mocks.repo.EXPECT().GetById(mock.Anything, testBlueprintId).Return(testCompletedBlueprintSpec, nil) + setupSuccessfulPreparationPhase(mocks, testCompletedBlueprintSpec) - t.Run("handle in progress blueprint", func(t *testing.T) { - // given - repoMock := newMockBlueprintSpecRepository(t) - validationMock := newMockBlueprintSpecValidationUseCase(t) - effectiveBlueprintMock := newMockEffectiveBlueprintUseCase(t) - stateDiffMock := newMockStateDiffUseCase(t) - applyMock := newMockApplyBlueprintSpecUseCase(t) - ecosystemConfigUseCaseMock := newMockEcosystemConfigUseCase(t) - doguRestartUseCaseMock := newMockDoguRestartUseCase(t) - selfUpgradeUseCase := newMockSelfUpgradeUseCase(t) - useCase := NewBlueprintSpecChangeUseCase(repoMock, validationMock, effectiveBlueprintMock, stateDiffMock, applyMock, ecosystemConfigUseCaseMock, doguRestartUseCaseMock, selfUpgradeUseCase) - - blueprintSpec := &domain.BlueprintSpec{ - Id: "testBlueprint1", - Status: domain.StatusPhaseInProgress, - } - repoMock.EXPECT().GetById(testCtx, "testBlueprint1").Return(blueprintSpec, nil) - applyMock.EXPECT().PostProcessBlueprintApplication(testCtx, blueprintSpec.Id).Return(nil) - // when - actualErr := useCase.HandleChange(testCtx, "testBlueprint1") - // then - require.NoError(t, actualErr) - }) + useCase := createUseCase(mocks) - t.Run("handle error when blueprint is in progress", func(t *testing.T) { - // given - repoMock := newMockBlueprintSpecRepository(t) - validationMock := newMockBlueprintSpecValidationUseCase(t) - effectiveBlueprintMock := newMockEffectiveBlueprintUseCase(t) - stateDiffMock := newMockStateDiffUseCase(t) - applyMock := newMockApplyBlueprintSpecUseCase(t) - ecosystemConfigUseCaseMock := newMockEcosystemConfigUseCase(t) - doguRestartUseCaseMock := newMockDoguRestartUseCase(t) - selfUpgradeUseCase := newMockSelfUpgradeUseCase(t) - useCase := NewBlueprintSpecChangeUseCase(repoMock, validationMock, effectiveBlueprintMock, stateDiffMock, applyMock, ecosystemConfigUseCaseMock, doguRestartUseCaseMock, selfUpgradeUseCase) - - blueprintSpec := &domain.BlueprintSpec{ - Id: "testBlueprint1", - Status: domain.StatusPhaseInProgress, - } - repoMock.EXPECT().GetById(testCtx, "testBlueprint1").Return(blueprintSpec, nil) - applyMock.EXPECT().PostProcessBlueprintApplication(testCtx, blueprintSpec.Id).Return(assert.AnError) // when - actualErr := useCase.HandleChange(testCtx, "testBlueprint1") - // then - require.ErrorIs(t, actualErr, assert.AnError) - }) + err := useCase.HandleUntilApplied(testCtx, testBlueprintId) - t.Run("handle error after blueprint was applied", func(t *testing.T) { - // given - repoMock := newMockBlueprintSpecRepository(t) - validationMock := newMockBlueprintSpecValidationUseCase(t) - effectiveBlueprintMock := newMockEffectiveBlueprintUseCase(t) - stateDiffMock := newMockStateDiffUseCase(t) - applyMock := newMockApplyBlueprintSpecUseCase(t) - ecosystemConfigUseCaseMock := newMockEcosystemConfigUseCase(t) - doguRestartUseCaseMock := newMockDoguRestartUseCase(t) - selfUpgradeUseCase := newMockSelfUpgradeUseCase(t) - useCase := NewBlueprintSpecChangeUseCase(repoMock, validationMock, effectiveBlueprintMock, stateDiffMock, applyMock, ecosystemConfigUseCaseMock, doguRestartUseCaseMock, selfUpgradeUseCase) - - blueprintSpec := &domain.BlueprintSpec{ - Id: testBlueprintId, - Status: domain.StatusPhaseBlueprintApplied, - } - repoMock.EXPECT().GetById(testCtx, "testBlueprint1").Return(blueprintSpec, nil) - applyMock.EXPECT().CheckEcosystemHealthAfterwards(testCtx, testBlueprintId).Return(assert.AnError) - doguRestartUseCaseMock.EXPECT().TriggerDoguRestarts(testCtx, testBlueprintId).Return(nil). - Run(func(ctx context.Context, blueprintId string) { - blueprintSpec.Status = domain.StatusPhaseRestartsTriggered - }) - - // when - err := useCase.HandleChange(testCtx, testBlueprintId) // then - require.ErrorIs(t, err, assert.AnError) + assert.NoError(t, err) }) +} - t.Run("handle ecosystem healthy afterwards", func(t *testing.T) { - // given - repoMock := newMockBlueprintSpecRepository(t) - validationMock := newMockBlueprintSpecValidationUseCase(t) - effectiveBlueprintMock := newMockEffectiveBlueprintUseCase(t) - stateDiffMock := newMockStateDiffUseCase(t) - applyMock := newMockApplyBlueprintSpecUseCase(t) - ecosystemConfigUseCaseMock := newMockEcosystemConfigUseCase(t) - doguRestartUseCaseMock := newMockDoguRestartUseCase(t) - selfUpgradeUseCase := newMockSelfUpgradeUseCase(t) - useCase := NewBlueprintSpecChangeUseCase(repoMock, validationMock, effectiveBlueprintMock, stateDiffMock, applyMock, ecosystemConfigUseCaseMock, doguRestartUseCaseMock, selfUpgradeUseCase) - - blueprintSpec := &domain.BlueprintSpec{ - Id: "testBlueprint1", - Status: domain.StatusPhaseEcosystemHealthyAfterwards, - } - repoMock.EXPECT().GetById(testCtx, "testBlueprint1").Return(blueprintSpec, nil) - applyMock.EXPECT().PostProcessBlueprintApplication(testCtx, "testBlueprint1").Return(nil).Run(func(ctx context.Context, blueprintId string) { - blueprintSpec.Status = domain.StatusPhaseCompleted +func TestBlueprintSpecChangeUseCase_HandleUntilApplied_ApplyPhaseErrors(t *testing.T) { + tests := []struct { + name string + setupMocks func(*allMocks) + wantErrTest func(*testing.T, error) + }{ + { + name: "should return error on error apply config", + setupMocks: func(mocks *allMocks) { + mocks.repo.EXPECT().GetById(mock.Anything, testBlueprintId).Return(testBlueprintSpec, nil) + setupSuccessfulPreparationPhase(mocks, testBlueprintSpec) + mocks.ecosystemConfig.EXPECT().ApplyConfig(mock.Anything, testBlueprintSpec).Return(assert.AnError) + }, + wantErrTest: func(t *testing.T, err error) { + assert.Error(t, err) + }, + }, + { + name: "should return error on error apply dogus", + setupMocks: func(mocks *allMocks) { + mocks.repo.EXPECT().GetById(mock.Anything, testBlueprintId).Return(testBlueprintSpec, nil) + setupSuccessfulPreparationPhase(mocks, testBlueprintSpec) + mocks.ecosystemConfig.EXPECT().ApplyConfig(mock.Anything, testBlueprintSpec).Return(nil) + mocks.applyDogus.EXPECT().ApplyDogus(mock.Anything, testBlueprintSpec).Return(false, assert.AnError) + }, + wantErrTest: func(t *testing.T, err error) { + assert.Error(t, err) + }, + }, + { + name: "should return error on error check health after dogu apply", + setupMocks: func(mocks *allMocks) { + mocks.repo.EXPECT().GetById(mock.Anything, testBlueprintId).Return(testBlueprintSpec, nil) + setupSuccessfulPreparationPhase(mocks, testBlueprintSpec) + mocks.ecosystemConfig.EXPECT().ApplyConfig(mock.Anything, testBlueprintSpec).Return(nil) + mocks.applyDogus.EXPECT().ApplyDogus(mock.Anything, testBlueprintSpec).Return(true, nil) + mocks.ecosystemHealth.EXPECT().CheckEcosystemHealth(mock.Anything, testBlueprintSpec).Return(ecosystem.HealthResult{}, assert.AnError) + }, + wantErrTest: func(t *testing.T, err error) { + assert.Error(t, err) + }, + }, + { + name: "should return error on error checking if dogus are up to date", + setupMocks: func(mocks *allMocks) { + mocks.repo.EXPECT().GetById(mock.Anything, testBlueprintId).Return(testBlueprintSpec, nil) + setupSuccessfulPreparationPhase(mocks, testBlueprintSpec) + mocks.ecosystemConfig.EXPECT().ApplyConfig(mock.Anything, testBlueprintSpec).Return(nil) + mocks.applyDogus.EXPECT().ApplyDogus(mock.Anything, testBlueprintSpec).Return(false, nil) + mocks.dogusUpToDate.EXPECT().CheckDogus(mock.Anything, testBlueprintSpec).Return(assert.AnError) + }, + wantErrTest: func(t *testing.T, err error) { + assert.Error(t, err) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mocks := createAllMocks(t) + tt.setupMocks(mocks) + useCase := createUseCase(mocks) + + err := useCase.HandleUntilApplied(testCtx, testBlueprintId) + tt.wantErrTest(t, err) }) - // when - err := useCase.HandleChange(testCtx, "testBlueprint1") - // then - require.NoError(t, err) - }) + } +} - t.Run("handle ecosystem unhealthy afterwards", func(t *testing.T) { - // given - repoMock := newMockBlueprintSpecRepository(t) - validationMock := newMockBlueprintSpecValidationUseCase(t) - effectiveBlueprintMock := newMockEffectiveBlueprintUseCase(t) - stateDiffMock := newMockStateDiffUseCase(t) - applyMock := newMockApplyBlueprintSpecUseCase(t) - ecosystemConfigUseCaseMock := newMockEcosystemConfigUseCase(t) - doguRestartUseCaseMock := newMockDoguRestartUseCase(t) - selfUpgradeUseCase := newMockSelfUpgradeUseCase(t) - useCase := NewBlueprintSpecChangeUseCase(repoMock, validationMock, effectiveBlueprintMock, stateDiffMock, applyMock, ecosystemConfigUseCaseMock, doguRestartUseCaseMock, selfUpgradeUseCase) - - blueprintSpec := &domain.BlueprintSpec{ - Id: "testBlueprint1", - Status: domain.StatusPhaseEcosystemUnhealthyAfterwards, - } - repoMock.EXPECT().GetById(testCtx, "testBlueprint1").Return(blueprintSpec, nil) - applyMock.EXPECT().PostProcessBlueprintApplication(testCtx, "testBlueprint1").Return(nil).Run(func(ctx context.Context, blueprintId string) { - blueprintSpec.Status = domain.StatusPhaseCompleted +func TestBlueprintSpecChangeUseCase_HandleUntilApplied_CompletionScenarios(t *testing.T) { + tests := []struct { + name string + setupMocks func(*allMocks) + wantErrTest func(*testing.T, error) + }{ + { + name: "should return nil on complete success", + setupMocks: func(mocks *allMocks) { + mocks.repo.EXPECT().GetById(mock.Anything, testBlueprintId).Return(testBlueprintSpecEmptyDiff, nil) + setupSuccessfulPreparationPhase(mocks, testBlueprintSpecEmptyDiff) + setupSuccessfulApplyPhaseExceptComplete(mocks, testBlueprintSpecEmptyDiff) + mocks.completeBlueprint.EXPECT().CompleteBlueprint(mock.Anything, testBlueprintSpecEmptyDiff).Return(nil) + }, + wantErrTest: func(t *testing.T, err error) { + assert.NoError(t, err) + }, + }, + { + name: "should return error on error complete blueprint", + setupMocks: func(mocks *allMocks) { + mocks.repo.EXPECT().GetById(mock.Anything, testBlueprintId).Return(testBlueprintSpecEmptyDiff, nil) + setupSuccessfulPreparationPhase(mocks, testBlueprintSpecEmptyDiff) + setupSuccessfulApplyPhaseExceptComplete(mocks, testBlueprintSpecEmptyDiff) + mocks.completeBlueprint.EXPECT().CompleteBlueprint(mock.Anything, testBlueprintSpecEmptyDiff).Return(assert.AnError) + }, + wantErrTest: func(t *testing.T, err error) { + assert.Error(t, err) + }, + }, + { + name: "should return StateDiffNotEmptyError when diff not empty", + setupMocks: func(mocks *allMocks) { + mocks.repo.EXPECT().GetById(mock.Anything, testBlueprintId).Return(testBlueprintSpec, nil) + setupSuccessfulPreparationPhase(mocks, testBlueprintSpec) + setupSuccessfulApplyPhaseExceptComplete(mocks, testBlueprintSpec) + }, + wantErrTest: func(t *testing.T, err error) { + var targetError *domain.StateDiffNotEmptyError + assert.Error(t, err) + assert.ErrorAs(t, err, &targetError) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mocks := createAllMocks(t) + tt.setupMocks(mocks) + useCase := createUseCase(mocks) + + err := useCase.HandleUntilApplied(testCtx, testBlueprintId) + tt.wantErrTest(t, err) }) - // when - err := useCase.HandleChange(testCtx, "testBlueprint1") - // then - require.NoError(t, err) - }) - - t.Run("handle completed blueprint", func(t *testing.T) { - // given - repoMock := newMockBlueprintSpecRepository(t) - validationMock := newMockBlueprintSpecValidationUseCase(t) - effectiveBlueprintMock := newMockEffectiveBlueprintUseCase(t) - stateDiffMock := newMockStateDiffUseCase(t) - applyMock := newMockApplyBlueprintSpecUseCase(t) - ecosystemConfigUseCaseMock := newMockEcosystemConfigUseCase(t) - doguRestartUseCaseMock := newMockDoguRestartUseCase(t) - selfUpgradeUseCase := newMockSelfUpgradeUseCase(t) - useCase := NewBlueprintSpecChangeUseCase(repoMock, validationMock, effectiveBlueprintMock, stateDiffMock, applyMock, ecosystemConfigUseCaseMock, doguRestartUseCaseMock, selfUpgradeUseCase) - - repoMock.EXPECT().GetById(testCtx, "testBlueprint1").Return(&domain.BlueprintSpec{ - Id: "testBlueprint1", - Status: domain.StatusPhaseCompleted, - }, nil) - // when - err := useCase.HandleChange(testCtx, "testBlueprint1") - // then - require.NoError(t, err) - }) + } +} - t.Run("handle blueprint application failed", func(t *testing.T) { - // given - repoMock := newMockBlueprintSpecRepository(t) - validationMock := newMockBlueprintSpecValidationUseCase(t) - effectiveBlueprintMock := newMockEffectiveBlueprintUseCase(t) - stateDiffMock := newMockStateDiffUseCase(t) - applyMock := newMockApplyBlueprintSpecUseCase(t) - ecosystemConfigUseCaseMock := newMockEcosystemConfigUseCase(t) - doguRestartUseCaseMock := newMockDoguRestartUseCase(t) - selfUpgradeUseCase := newMockSelfUpgradeUseCase(t) - useCase := NewBlueprintSpecChangeUseCase(repoMock, validationMock, effectiveBlueprintMock, stateDiffMock, applyMock, ecosystemConfigUseCaseMock, doguRestartUseCaseMock, selfUpgradeUseCase) - - repoMock.EXPECT().GetById(testCtx, blueprintId).Return(&domain.BlueprintSpec{ - Id: blueprintId, - Status: domain.StatusPhaseBlueprintApplicationFailed, - }, nil) - applyMock.EXPECT().PostProcessBlueprintApplication(testCtx, blueprintId).Return(nil) - // when - err := useCase.HandleChange(testCtx, blueprintId) - // then - require.NoError(t, err) - }) +func createUseCase(mocks *allMocks) *BlueprintSpecChangeUseCase { + preparationUseCases := BlueprintPreparationUseCase{ + initialStatus: mocks.initialStatus, + validation: mocks.validation, + effectiveBlueprint: mocks.effectiveBlueprint, + stateDiff: mocks.stateDiff, + healthUseCase: mocks.ecosystemHealth, + } + applyUseCases := BlueprintApplyUseCase{ + completeUseCase: mocks.completeBlueprint, + ecosystemConfigUseCase: mocks.ecosystemConfig, + applyDogusUseCase: mocks.applyDogus, + healthUseCase: mocks.ecosystemHealth, + dogusUpToDateUseCase: mocks.dogusUpToDate, + } + + return &BlueprintSpecChangeUseCase{ + repo: mocks.repo, + preparationUseCase: preparationUseCases, + applyUseCase: applyUseCases, + } +} - t.Run("handle failed blueprint", func(t *testing.T) { - // given - repoMock := newMockBlueprintSpecRepository(t) - validationMock := newMockBlueprintSpecValidationUseCase(t) - effectiveBlueprintMock := newMockEffectiveBlueprintUseCase(t) - stateDiffMock := newMockStateDiffUseCase(t) - applyMock := newMockApplyBlueprintSpecUseCase(t) - ecosystemConfigUseCaseMock := newMockEcosystemConfigUseCase(t) - doguRestartUseCaseMock := newMockDoguRestartUseCase(t) - selfUpgradeUseCase := newMockSelfUpgradeUseCase(t) - useCase := NewBlueprintSpecChangeUseCase(repoMock, validationMock, effectiveBlueprintMock, stateDiffMock, applyMock, ecosystemConfigUseCaseMock, doguRestartUseCaseMock, selfUpgradeUseCase) - - repoMock.EXPECT().GetById(testCtx, "testBlueprint1").Return(&domain.BlueprintSpec{ - Id: "testBlueprint1", - Status: domain.StatusPhaseFailed, - }, nil) - // when - err := useCase.HandleChange(testCtx, "testBlueprint1") - // then - require.NoError(t, err) - }) +func setupSuccessfulPreparationPhase(mocks *allMocks, spec *domain.BlueprintSpec) { + mocks.initialStatus.EXPECT().InitateConditions(mock.Anything, mock.Anything).Return(nil) + mocks.validation.EXPECT().ValidateBlueprintSpecStatically(mock.Anything, mock.Anything).Return(nil) + mocks.effectiveBlueprint.EXPECT().CalculateEffectiveBlueprint(mock.Anything, spec).Return(nil) + mocks.validation.EXPECT().ValidateBlueprintSpecDynamically(mock.Anything, spec).Return(nil) + mocks.ecosystemHealth.EXPECT().CheckEcosystemHealth(mock.Anything, spec).Return(ecosystem.HealthResult{}, nil).Times(1) + mocks.stateDiff.EXPECT().DetermineStateDiff(mock.Anything, spec).Return(nil) +} - t.Run("handle unknown status in blueprint", func(t *testing.T) { - // given - repoMock := newMockBlueprintSpecRepository(t) - validationMock := newMockBlueprintSpecValidationUseCase(t) - effectiveBlueprintMock := newMockEffectiveBlueprintUseCase(t) - stateDiffMock := newMockStateDiffUseCase(t) - applyMock := newMockApplyBlueprintSpecUseCase(t) - ecosystemConfigUseCaseMock := newMockEcosystemConfigUseCase(t) - doguRestartUseCaseMock := newMockDoguRestartUseCase(t) - selfUpgradeUseCase := newMockSelfUpgradeUseCase(t) - useCase := NewBlueprintSpecChangeUseCase(repoMock, validationMock, effectiveBlueprintMock, stateDiffMock, applyMock, ecosystemConfigUseCaseMock, doguRestartUseCaseMock, selfUpgradeUseCase) - - repoMock.EXPECT().GetById(testCtx, "testBlueprint1").Return(&domain.BlueprintSpec{ - Id: "testBlueprint1", - Status: "unknown", - }, nil) - // when - err := useCase.HandleChange(testCtx, "testBlueprint1") - // then - require.Error(t, err) - require.ErrorContains(t, err, "could not handle unknown status of blueprint") - }) +func setupSuccessfulApplyPhaseExceptComplete(mocks *allMocks, spec *domain.BlueprintSpec) { + mocks.ecosystemConfig.EXPECT().ApplyConfig(mock.Anything, spec).Return(nil) + mocks.applyDogus.EXPECT().ApplyDogus(mock.Anything, spec).Return(false, nil) + mocks.dogusUpToDate.EXPECT().CheckDogus(mock.Anything, spec).Return(nil) + // Note: no completeBlueprint expectation - this allows the completion steps to be tested } -func TestBlueprintSpecChangeUseCase_preProcessBlueprintApplication(t *testing.T) { - t.Run("stop on dry run", func(t *testing.T) { +func TestBlueprintSpecChangeUseCase_CheckForMultipleBlueprintResources(t *testing.T) { + t.Run("should succeed without error", func(t *testing.T) { // given - spec := &domain.BlueprintSpec{ - Id: blueprintId, - Config: domain.BlueprintConfiguration{DryRun: true}, + mockRepo := newMockBlueprintSpecRepository(t) + mockRepo.EXPECT().CheckSingleton(t.Context()).Return(nil) + useCase := &BlueprintSpecChangeUseCase{ + repo: mockRepo, } - applyMock := newMockApplyBlueprintSpecUseCase(t) - applyMock.EXPECT().PreProcessBlueprintApplication(testCtx, blueprintId).Return(nil) - useCase := NewBlueprintSpecChangeUseCase(nil, nil, nil, nil, applyMock, nil, nil, nil) - // when - err := useCase.preProcessBlueprintApplication(testCtx, spec) - // then - require.NoError(t, err) - }) - t.Run("error", func(t *testing.T) { - // given - spec := &domain.BlueprintSpec{ - Id: blueprintId, - } - repoMock := newMockBlueprintSpecRepository(t) - validationMock := newMockBlueprintSpecValidationUseCase(t) - effectiveBlueprintMock := newMockEffectiveBlueprintUseCase(t) - stateDiffMock := newMockStateDiffUseCase(t) - applyMock := newMockApplyBlueprintSpecUseCase(t) - applyMock.EXPECT().PreProcessBlueprintApplication(testCtx, blueprintId).Return(assert.AnError) - ecosystemConfigUseCaseMock := newMockEcosystemConfigUseCase(t) - doguRestartUseCaseMock := newMockDoguRestartUseCase(t) - selfUpgradeUseCase := newMockSelfUpgradeUseCase(t) - useCase := NewBlueprintSpecChangeUseCase(repoMock, validationMock, effectiveBlueprintMock, stateDiffMock, applyMock, ecosystemConfigUseCaseMock, doguRestartUseCaseMock, selfUpgradeUseCase) - // when - err := useCase.preProcessBlueprintApplication(testCtx, spec) - // then - require.ErrorIs(t, err, assert.AnError) - }) -} -func TestBlueprintSpecChangeUseCase_applyBlueprintSpec(t *testing.T) { - t.Run("error", func(t *testing.T) { - // given - applyMock := newMockApplyBlueprintSpecUseCase(t) - applyMock.EXPECT().ApplyBlueprintSpec(testCtx, blueprintId).Return(assert.AnError) - useCase := NewBlueprintSpecChangeUseCase(nil, nil, nil, nil, applyMock, nil, nil, nil) - // when - err := useCase.applyBlueprintSpec(testCtx, blueprintId) - // then - require.ErrorIs(t, err, assert.AnError) - }) -} + //when + err := useCase.CheckForMultipleBlueprintResources(t.Context()) -func TestBlueprintSpecChangeUseCase_checkEcosystemHealthAfterwards(t *testing.T) { - t.Run("error", func(t *testing.T) { - // given - applyMock := newMockApplyBlueprintSpecUseCase(t) - applyMock.EXPECT().CheckEcosystemHealthAfterwards(testCtx, blueprintId).Return(assert.AnError) - useCase := NewBlueprintSpecChangeUseCase(nil, nil, nil, nil, applyMock, nil, nil, nil) - // when - err := useCase.checkEcosystemHealthAfterwards(testCtx, blueprintId) // then - require.ErrorIs(t, err, assert.AnError) + require.NoError(t, err) }) -} -func TestBlueprintSpecChangeUseCase_triggerDoguRestarts(t *testing.T) { - t.Run("handle error in TriggerDoguRestarts", func(t *testing.T) { + t.Run("should return error on check error", func(t *testing.T) { // given - repoMock := newMockBlueprintSpecRepository(t) - validationMock := newMockBlueprintSpecValidationUseCase(t) - effectiveBlueprintMock := newMockEffectiveBlueprintUseCase(t) - stateDiffMock := newMockStateDiffUseCase(t) - applyMock := newMockApplyBlueprintSpecUseCase(t) - ecosystemConfigUseCaseMock := newMockEcosystemConfigUseCase(t) - doguRestartUseCaseMock := newMockDoguRestartUseCase(t) - selfUpgradeUseCase := newMockSelfUpgradeUseCase(t) - useCase := NewBlueprintSpecChangeUseCase(repoMock, validationMock, effectiveBlueprintMock, stateDiffMock, applyMock, ecosystemConfigUseCaseMock, doguRestartUseCaseMock, selfUpgradeUseCase) - - blueprintSpec := &domain.BlueprintSpec{ - Id: testBlueprintId, - Status: domain.StatusPhaseBlueprintApplied, + mockRepo := newMockBlueprintSpecRepository(t) + mockRepo.EXPECT().CheckSingleton(t.Context()).Return(assert.AnError) + useCase := &BlueprintSpecChangeUseCase{ + repo: mockRepo, } - repoMock.EXPECT().GetById(testCtx, "testBlueprint1").Return(blueprintSpec, nil) - doguRestartUseCaseMock.EXPECT().TriggerDoguRestarts(testCtx, testBlueprintId).Return(errors.New("testerror")) - // when - err := useCase.HandleChange(testCtx, testBlueprintId) + //when + err := useCase.CheckForMultipleBlueprintResources(t.Context()) + // then require.Error(t, err) - assert.Equal(t, "testerror", err.Error()) + assert.ErrorIs(t, err, assert.AnError) + assert.ErrorContains(t, err, "check for multiple blueprints not successful") }) } diff --git a/pkg/application/blueprintSpecValidationUseCase.go b/pkg/application/blueprintSpecValidationUseCase.go index 44a6ee4d..c86f7bc0 100644 --- a/pkg/application/blueprintSpecValidationUseCase.go +++ b/pkg/application/blueprintSpecValidationUseCase.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domainservice" "sigs.k8s.io/controller-runtime/pkg/log" @@ -29,34 +30,16 @@ func NewBlueprintSpecValidationUseCase( // ValidateBlueprintSpecStatically checks the blueprintSpec for semantic errors and persists it. // returns a domain.InvalidBlueprintError if blueprint is invalid or -// a domainservice.NotFoundError if the blueprintId does not correspond to a blueprintSpec or // a domainservice.InternalError if there is any error while loading or persisting the blueprintSpec or // a domainservice.ConflictError if there was a concurrent write. -func (useCase *BlueprintSpecValidationUseCase) ValidateBlueprintSpecStatically(ctx context.Context, blueprintId string) error { +func (useCase *BlueprintSpecValidationUseCase) ValidateBlueprintSpecStatically(ctx context.Context, blueprint *domain.BlueprintSpec) error { logger := log.FromContext(ctx). - WithName("BlueprintSpecValidationUseCase.ValidateBlueprintSpecStatically"). - WithValues("blueprintId", blueprintId) - - logger.Info("getting blueprint spec for static validation") - blueprintSpec, err := useCase.repo.GetById(ctx, blueprintId) - if err != nil { - var invalidError *domain.InvalidBlueprintError - if errors.As(err, &invalidError) { - blueprintSpec.MarkInvalid(err) - updateErr := useCase.repo.Update(ctx, blueprintSpec) - if updateErr != nil { - return updateErr - } - return fmt.Errorf("blueprint spec syntax is invalid: %w", err) - } else { - return fmt.Errorf("cannot load blueprint spec to validate it: %w", err) - } - } + WithName("BlueprintSpecValidationUseCase.ValidateBlueprintSpecStatically") - logger.Info("statically validate blueprint spec", "blueprintStatus", blueprintSpec.Status) + logger.V(1).Info("statically validate blueprint spec") - invalidBlueprintError := blueprintSpec.ValidateStatically() - err = useCase.repo.Update(ctx, blueprintSpec) + invalidBlueprintError := blueprint.ValidateStatically() + err := useCase.repo.Update(ctx, blueprint) if err != nil { // InternalError or ConflictError, both should be handled by the caller return fmt.Errorf("cannot update blueprint spec after static validation: %w", err) @@ -70,22 +53,14 @@ func (useCase *BlueprintSpecValidationUseCase) ValidateBlueprintSpecStatically(c // a domainservice.NotFoundError if the blueprintId does not correspond to a blueprintSpec or // a domainservice.InternalError if there is any error while loading or persisting the blueprintSpec or // a domainservice.ConflictError if there was a concurrent write. -func (useCase *BlueprintSpecValidationUseCase) ValidateBlueprintSpecDynamically(ctx context.Context, blueprintId string) error { +func (useCase *BlueprintSpecValidationUseCase) ValidateBlueprintSpecDynamically(ctx context.Context, blueprint *domain.BlueprintSpec) error { logger := log.FromContext(ctx). - WithName("BlueprintSpecValidationUseCase.ValidateBlueprintSpecDynamically"). - WithValues("blueprintId", blueprintId) - - logger.Info("getting blueprint spec for dynamic validation") - blueprintSpec, err := useCase.repo.GetById(ctx, blueprintId) - if err != nil { - return fmt.Errorf("cannot load blueprint spec to validate it: %w", err) - } - - logger.Info("dynamically validate blueprint spec", "blueprintStatus", blueprintSpec.Status) + WithName("BlueprintSpecValidationUseCase.ValidateBlueprintSpecDynamically") + logger.V(1).Info("dynamically validate blueprint spec") validationError := errors.Join( - useCase.validateDependenciesUseCase.ValidateDependenciesForAllDogus(ctx, blueprintSpec.EffectiveBlueprint), - useCase.ValidateMountsUseCase.ValidateAdditionalMounts(ctx, blueprintSpec.EffectiveBlueprint), + useCase.validateDependenciesUseCase.ValidateDependenciesForAllDogus(ctx, blueprint.EffectiveBlueprint), + useCase.ValidateMountsUseCase.ValidateAdditionalMounts(ctx, blueprint.EffectiveBlueprint), ) if validationError != nil { @@ -95,8 +70,8 @@ func (useCase *BlueprintSpecValidationUseCase) ValidateBlueprintSpecDynamically( } } - blueprintSpec.ValidateDynamically(validationError) - err = useCase.repo.Update(ctx, blueprintSpec) + blueprint.ValidateDynamically(validationError) + err := useCase.repo.Update(ctx, blueprint) if err != nil { return fmt.Errorf("cannot update blueprint spec after dynamic validation: %w", err) } diff --git a/pkg/application/blueprintSpecValidationUseCase_test.go b/pkg/application/blueprintSpecValidationUseCase_test.go index ed79ade1..759f37fb 100644 --- a/pkg/application/blueprintSpecValidationUseCase_test.go +++ b/pkg/application/blueprintSpecValidationUseCase_test.go @@ -3,6 +3,8 @@ package application import ( "context" "errors" + "testing" + cescommons "github.com/cloudogu/ces-commons-lib/dogu" "github.com/cloudogu/cesapp-lib/core" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" @@ -10,7 +12,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "testing" + "k8s.io/apimachinery/pkg/api/meta" ) var redmineQualifiedDoguName = cescommons.QualifiedName{ @@ -20,48 +22,46 @@ var redmineQualifiedDoguName = cescommons.QualifiedName{ func TestBlueprintSpecUseCase_ValidateBlueprintSpecStatically_ok(t *testing.T) { //given + blueprint := &domain.BlueprintSpec{ + Id: "testBlueprint1", + } + repoMock := newMockBlueprintSpecRepository(t) ctx := context.Background() DependencyUseCase := newMockValidateDependenciesDomainUseCase(t) MountsUseCase := newMockValidateAdditionalMountsDomainUseCase(t) useCase := NewBlueprintSpecValidationUseCase(repoMock, DependencyUseCase, MountsUseCase) - repoMock.EXPECT().GetById(ctx, "testBlueprint1").Return(&domain.BlueprintSpec{ - Id: "testBlueprint1", - Status: domain.StatusPhaseNew, - }, nil) repoMock.EXPECT().Update(ctx, &domain.BlueprintSpec{ - Id: "testBlueprint1", - Status: domain.StatusPhaseStaticallyValidated, - Events: []domain.Event{domain.BlueprintSpecStaticallyValidatedEvent{}}, + Id: "testBlueprint1", }).Return(nil) //when - err := useCase.ValidateBlueprintSpecStatically(ctx, "testBlueprint1") + err := useCase.ValidateBlueprintSpecStatically(ctx, blueprint) //then require.NoError(t, err) + assert.Nil(t, blueprint.Conditions, "should not set conditions") } func TestBlueprintSpecUseCase_ValidateBlueprintSpecStatically_invalid(t *testing.T) { //given + blueprint := &domain.BlueprintSpec{ + //missing ID + } + repoMock := newMockBlueprintSpecRepository(t) ctx := context.Background() DependencyUseCase := newMockValidateDependenciesDomainUseCase(t) MountsUseCase := newMockValidateAdditionalMountsDomainUseCase(t) useCase := NewBlueprintSpecValidationUseCase(repoMock, DependencyUseCase, MountsUseCase) - repoMock.EXPECT().GetById(ctx, "testBlueprint1").Return(&domain.BlueprintSpec{ - Id: "", - Status: domain.StatusPhaseNew, - }, nil) - repoMock.EXPECT().Update(ctx, mock.MatchedBy(func(i interface{}) bool { - spec := i.(*domain.BlueprintSpec) - return spec.Status == domain.StatusPhaseInvalid - })).Return(nil) + repoMock.EXPECT(). + Update(ctx, blueprint). + Return(nil) //when - err := useCase.ValidateBlueprintSpecStatically(ctx, "testBlueprint1") + err := useCase.ValidateBlueprintSpecStatically(ctx, blueprint) //then require.Error(t, err) @@ -71,83 +71,21 @@ func TestBlueprintSpecUseCase_ValidateBlueprintSpecStatically_invalid(t *testing } func TestBlueprintSpecUseCase_ValidateBlueprintSpecStatically_repoError(t *testing.T) { - - t.Run("blueprint spec not found while loading", func(t *testing.T) { - //given - repoMock := newMockBlueprintSpecRepository(t) - ctx := context.Background() - DependencyUseCase := newMockValidateDependenciesDomainUseCase(t) - MountsUseCase := newMockValidateAdditionalMountsDomainUseCase(t) - useCase := NewBlueprintSpecValidationUseCase(repoMock, DependencyUseCase, MountsUseCase) - - repoMock.EXPECT().GetById(ctx, "testBlueprint1").Return(nil, &domainservice.NotFoundError{Message: "test-error"}) - //when - err := useCase.ValidateBlueprintSpecStatically(ctx, "testBlueprint1") - - //then - require.Error(t, err) - var invalidError *domainservice.NotFoundError - assert.ErrorAs(t, err, &invalidError) - assert.ErrorContains(t, err, "cannot load blueprint spec to validate it: test-error") - }) - - t.Run("cannot parse blueprint in repository", func(t *testing.T) { - //given - repoMock := newMockBlueprintSpecRepository(t) - ctx := context.Background() - DependencyUseCase := newMockValidateDependenciesDomainUseCase(t) - MountsUseCase := newMockValidateAdditionalMountsDomainUseCase(t) - useCase := NewBlueprintSpecValidationUseCase(repoMock, DependencyUseCase, MountsUseCase) - invalidError := domain.InvalidBlueprintError{Message: "test-error"} - var events []domain.Event - events = append(events, domain.BlueprintSpecInvalidEvent{ValidationError: &invalidError}) - repoMock.EXPECT().GetById(ctx, "testBlueprint1").Return(&domain.BlueprintSpec{Id: "testBlueprint1"}, &invalidError) - repoMock.EXPECT().Update(ctx, &domain.BlueprintSpec{Id: "testBlueprint1", Status: domain.StatusPhaseInvalid, Events: events}).Return(nil) - //when - err := useCase.ValidateBlueprintSpecStatically(ctx, "testBlueprint1") - - //then - require.Error(t, err) - var expectedErrorType *domain.InvalidBlueprintError - assert.ErrorAs(t, err, &expectedErrorType) - assert.ErrorContains(t, err, "blueprint spec syntax is invalid: test-error") - }) - - t.Run("internal error while loading blueprint spec", func(t *testing.T) { - //given - repoMock := newMockBlueprintSpecRepository(t) - ctx := context.Background() - DependencyUseCase := newMockValidateDependenciesDomainUseCase(t) - MountsUseCase := newMockValidateAdditionalMountsDomainUseCase(t) - useCase := NewBlueprintSpecValidationUseCase(repoMock, DependencyUseCase, MountsUseCase) - - repoMock.EXPECT().GetById(ctx, "testBlueprint1").Return(nil, &domainservice.InternalError{Message: "test-error"}) - //when - err := useCase.ValidateBlueprintSpecStatically(ctx, "testBlueprint1") - - //then - require.Error(t, err) - var invalidError *domainservice.InternalError - assert.ErrorAs(t, err, &invalidError) - assert.ErrorContains(t, err, "cannot load blueprint spec to validate it: test-error") - }) - t.Run("error while saving blueprint spec", func(t *testing.T) { //given + blueprint := &domain.BlueprintSpec{ + Id: "testBlueprint1", + } repoMock := newMockBlueprintSpecRepository(t) ctx := context.Background() DependencyUseCase := newMockValidateDependenciesDomainUseCase(t) MountsUseCase := newMockValidateAdditionalMountsDomainUseCase(t) useCase := NewBlueprintSpecValidationUseCase(repoMock, DependencyUseCase, MountsUseCase) - repoMock.EXPECT().GetById(ctx, "testBlueprint1").Return(&domain.BlueprintSpec{ - Id: "testBlueprint1", - Status: domain.StatusPhaseNew, - }, nil) repoMock.EXPECT().Update(ctx, mock.Anything).Return(&domainservice.InternalError{Message: "test-error"}) //when - err := useCase.ValidateBlueprintSpecStatically(ctx, "testBlueprint1") + err := useCase.ValidateBlueprintSpecStatically(ctx, blueprint) //then require.Error(t, err) @@ -160,6 +98,10 @@ func TestBlueprintSpecUseCase_ValidateBlueprintSpecStatically_repoError(t *testi func TestBlueprintSpecUseCase_ValidateBlueprintSpecDynamically_ok(t *testing.T) { // given + blueprint := &domain.BlueprintSpec{ + Id: "testBlueprint1", + Conditions: []domain.Condition{}, + } repoMock := newMockBlueprintSpecRepository(t) ctx := context.Background() DependencyUseCase := newMockValidateDependenciesDomainUseCase(t) @@ -169,25 +111,14 @@ func TestBlueprintSpecUseCase_ValidateBlueprintSpecDynamically_ok(t *testing.T) DependencyUseCase.EXPECT().ValidateDependenciesForAllDogus(ctx, mock.Anything).Return(nil) MountsUseCase.EXPECT().ValidateAdditionalMounts(ctx, mock.Anything).Return(nil) - repoMock.EXPECT().GetById(ctx, "testBlueprint1").Return(&domain.BlueprintSpec{ - Id: "testBlueprint1", - Status: domain.StatusPhaseValidated, - }, nil) - repoMock.EXPECT().Update(ctx, &domain.BlueprintSpec{ - Id: "testBlueprint1", - Blueprint: domain.Blueprint{}, - BlueprintMask: domain.BlueprintMask{}, - EffectiveBlueprint: domain.EffectiveBlueprint{}, - StateDiff: domain.StateDiff{}, - Status: domain.StatusPhaseValidated, - Events: []domain.Event{domain.BlueprintSpecValidatedEvent{}}, - }).Return(nil) + repoMock.EXPECT().Update(ctx, blueprint).Return(nil) // when - err := useCase.ValidateBlueprintSpecDynamically(ctx, "testBlueprint1") + err := useCase.ValidateBlueprintSpecDynamically(ctx, blueprint) // then require.NoError(t, err) + assert.True(t, meta.IsStatusConditionTrue(blueprint.Conditions, domain.ConditionValid)) } func TestBlueprintSpecUseCase_ValidateBlueprintSpecDynamically_invalid(t *testing.T) { @@ -199,26 +130,27 @@ func TestBlueprintSpecUseCase_ValidateBlueprintSpecDynamically_invalid(t *testin useCase := NewBlueprintSpecValidationUseCase(repoMock, DependencyUseCase, MountsUseCase) version, _ := core.ParseVersion("1.0.0-1") - blueprintSpec := &domain.BlueprintSpec{ + blueprint := &domain.BlueprintSpec{ Id: "testBlueprint1", EffectiveBlueprint: domain.EffectiveBlueprint{Dogus: []domain.Dogu{{ - Name: redmineQualifiedDoguName, - Version: version, - TargetState: domain.TargetStatePresent, + Name: redmineQualifiedDoguName, + Version: &version, + Absent: false, }}}, - Status: domain.StatusPhaseValidated, + Conditions: []domain.Condition{}, } - repoMock.EXPECT().GetById(ctx, "testBlueprint1").Return(blueprintSpec, nil) invalidDependencyError := errors.New("invalid dependencies") invalidMountsError := errors.New("invalid mounts") DependencyUseCase.EXPECT().ValidateDependenciesForAllDogus(ctx, mock.Anything).Return(invalidDependencyError) MountsUseCase.EXPECT().ValidateAdditionalMounts(ctx, mock.Anything).Return(invalidMountsError) - repoMock.EXPECT().Update(ctx, mock.Anything).Return(nil) + repoMock.EXPECT().Update(ctx, blueprint).Return(nil) // when - err := useCase.ValidateBlueprintSpecDynamically(ctx, "testBlueprint1") + err := useCase.ValidateBlueprintSpecDynamically(ctx, blueprint) // then + assert.True(t, meta.IsStatusConditionFalse(blueprint.Conditions, domain.ConditionValid)) + require.Error(t, err) var invalidError *domain.InvalidBlueprintError assert.ErrorAs(t, err, &invalidError) @@ -226,50 +158,8 @@ func TestBlueprintSpecUseCase_ValidateBlueprintSpecDynamically_invalid(t *testin assert.ErrorIs(t, err, invalidMountsError) assert.ErrorContains(t, err, "blueprint spec is invalid") - assert.Equal(t, "testBlueprint1", blueprintSpec.Id) - assert.Equal(t, domain.StatusPhaseInvalid, blueprintSpec.Status) - require.Equal(t, 1, len(blueprintSpec.Events)) - assert.IsType(t, domain.BlueprintSpecInvalidEvent{}, blueprintSpec.Events[0]) - assert.ErrorContains(t, blueprintSpec.Events[0].(domain.BlueprintSpecInvalidEvent).ValidationError, "blueprint spec is invalid: ") -} - -func TestBlueprintSpecUseCase_ValidateBlueprintSpecDynamically_repoError(t *testing.T) { - t.Run("internal error", func(t *testing.T) { - // given - repoMock := newMockBlueprintSpecRepository(t) - ctx := context.Background() - DependencyUseCase := newMockValidateDependenciesDomainUseCase(t) - MountsUseCase := newMockValidateAdditionalMountsDomainUseCase(t) - useCase := NewBlueprintSpecValidationUseCase(repoMock, DependencyUseCase, MountsUseCase) - - repoMock.EXPECT().GetById(ctx, "testBlueprint1").Return(nil, &domainservice.InternalError{Message: "test-error"}) - - // when - err := useCase.ValidateBlueprintSpecDynamically(ctx, "testBlueprint1") - - // then - require.Error(t, err) - var invalidError *domainservice.InternalError - assert.ErrorAs(t, err, &invalidError) - assert.ErrorContains(t, err, "cannot load blueprint spec to validate it: test-error") - }) - t.Run("not found error", func(t *testing.T) { - // given - repoMock := newMockBlueprintSpecRepository(t) - ctx := context.Background() - DependencyUseCase := newMockValidateDependenciesDomainUseCase(t) - MountsUseCase := newMockValidateAdditionalMountsDomainUseCase(t) - useCase := NewBlueprintSpecValidationUseCase(repoMock, DependencyUseCase, MountsUseCase) - - repoMock.EXPECT().GetById(ctx, "testBlueprint1").Return(nil, &domainservice.NotFoundError{Message: "test-error"}) - - // when - err := useCase.ValidateBlueprintSpecDynamically(ctx, "testBlueprint1") - - // then - require.Error(t, err) - var invalidError *domainservice.NotFoundError - assert.ErrorAs(t, err, &invalidError) - assert.ErrorContains(t, err, "cannot load blueprint spec to validate it: test-error") - }) + assert.Equal(t, "testBlueprint1", blueprint.Id) + require.Equal(t, 1, len(blueprint.Events)) + assert.IsType(t, domain.BlueprintSpecInvalidEvent{}, blueprint.Events[0]) + assert.ErrorContains(t, blueprint.Events[0].(domain.BlueprintSpecInvalidEvent).ValidationError, "blueprint spec is invalid: ") } diff --git a/pkg/application/completeBlueprintUseCase.go b/pkg/application/completeBlueprintUseCase.go new file mode 100644 index 00000000..c8a0c746 --- /dev/null +++ b/pkg/application/completeBlueprintUseCase.go @@ -0,0 +1,35 @@ +package application + +import ( + "context" + "fmt" + + "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" +) + +// CompleteBlueprintUseCase contains all use cases which are needed for or around applying +// the new ecosystem state after the determining the state diff. +type CompleteBlueprintUseCase struct { + repo blueprintSpecRepository +} + +func NewCompleteBlueprintUseCase( + repo blueprintSpecRepository, +) *CompleteBlueprintUseCase { + return &CompleteBlueprintUseCase{ + repo: repo, + } +} + +// CompleteBlueprint handles the completion of the blueprint after all other steps were successful. +// returns a domainservice.InternalError on any error. +func (useCase *CompleteBlueprintUseCase) CompleteBlueprint(ctx context.Context, blueprint *domain.BlueprintSpec) error { + changed := blueprint.Complete() + if changed { + err := useCase.repo.Update(ctx, blueprint) + if err != nil { + return fmt.Errorf("cannot update blueprint to complete it: %w", err) + } + } + return nil +} diff --git a/pkg/application/completeBlueprintUseCase_test.go b/pkg/application/completeBlueprintUseCase_test.go new file mode 100644 index 00000000..162c57a2 --- /dev/null +++ b/pkg/application/completeBlueprintUseCase_test.go @@ -0,0 +1,64 @@ +package application + +import ( + "testing" + + "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/api/meta" +) + +func TestNewApplyBlueprintSpecUseCase(t *testing.T) { + repoMock := newMockBlueprintSpecRepository(t) + + sut := NewCompleteBlueprintUseCase(repoMock) + + assert.Equal(t, repoMock, sut.repo) +} + +func TestApplyBlueprintSpecUseCase_CompleteBlueprint(t *testing.T) { + t.Run("ok", func(t *testing.T) { + blueprint := &domain.BlueprintSpec{ + Conditions: []domain.Condition{}, + } + + repoMock := newMockBlueprintSpecRepository(t) + repoMock.EXPECT().Update(testCtx, blueprint).Return(nil) + useCase := NewCompleteBlueprintUseCase(repoMock) + + err := useCase.CompleteBlueprint(testCtx, blueprint) + + require.NoError(t, err) + + assert.True(t, meta.IsStatusConditionTrue(blueprint.Conditions, domain.ConditionCompleted)) + }) + t.Run("no change if already completed", func(t *testing.T) { + blueprint := &domain.BlueprintSpec{ + Conditions: []domain.Condition{}, + } + + repoMock := newMockBlueprintSpecRepository(t) + repoMock.EXPECT().Update(testCtx, blueprint).Return(nil).Once() + useCase := NewCompleteBlueprintUseCase(repoMock) + + err := useCase.CompleteBlueprint(testCtx, blueprint) + require.NoError(t, err) + err = useCase.CompleteBlueprint(testCtx, blueprint) + require.NoError(t, err) + assert.True(t, meta.IsStatusConditionTrue(blueprint.Conditions, domain.ConditionCompleted)) + }) + t.Run("repo error while saving", func(t *testing.T) { + blueprint := &domain.BlueprintSpec{ + Conditions: []domain.Condition{}, + } + + repoMock := newMockBlueprintSpecRepository(t) + repoMock.EXPECT().Update(testCtx, blueprint).Return(assert.AnError) + useCase := NewCompleteBlueprintUseCase(repoMock) + + err := useCase.CompleteBlueprint(testCtx, blueprint) + + require.ErrorIs(t, err, assert.AnError) + }) +} diff --git a/pkg/application/componentInstallationUseCase.go b/pkg/application/componentInstallationUseCase.go deleted file mode 100644 index 87d35095..00000000 --- a/pkg/application/componentInstallationUseCase.go +++ /dev/null @@ -1,176 +0,0 @@ -package application - -import ( - "context" - "errors" - "fmt" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/common" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domainservice" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/util" - "sigs.k8s.io/controller-runtime/pkg/log" -) - -const ( - noDowngradesExplanationTextFmt = "downgrades are not allowed as the data model of the %s could have changed and " + - "doing rollbacks to older models is not supported. " + - "You can downgrade %s by restoring a backup. " + - "If you want an 'allow-downgrades' flag, issue a feature request" - noDistributionNamespaceSwitchExplanationText = "switching distribution namespace of components is not allowed. If you want an " + - "`allow-switch-distribution-namespace` flag, issue a feature request" -) - -type ComponentInstallationUseCase struct { - blueprintSpecRepo domainservice.BlueprintSpecRepository - componentRepo domainservice.ComponentInstallationRepository - healthConfigProvider healthConfigProvider -} - -func NewComponentInstallationUseCase( - blueprintSpecRepo domainservice.BlueprintSpecRepository, - componentRepo domainservice.ComponentInstallationRepository, - healthConfigProvider healthConfigProvider, -) *ComponentInstallationUseCase { - return &ComponentInstallationUseCase{ - blueprintSpecRepo: blueprintSpecRepo, - componentRepo: componentRepo, - healthConfigProvider: healthConfigProvider, - } -} - -func (useCase *ComponentInstallationUseCase) CheckComponentHealth(ctx context.Context) (ecosystem.ComponentHealthResult, error) { - logger := log.FromContext(ctx).WithName("ComponentInstallationUseCase.CheckComponentHealth") - logger.Info("check component health...") - installedComponents, err := useCase.componentRepo.GetAll(ctx) - if err != nil { - return ecosystem.ComponentHealthResult{}, fmt.Errorf("cannot retrieve installed components: %w", err) - } - - requiredComponents, err := useCase.healthConfigProvider.GetRequiredComponents(ctx) - if err != nil { - return ecosystem.ComponentHealthResult{}, fmt.Errorf("cannot retrieve required components: %w", err) - } - - return ecosystem.CalculateComponentHealthResult(installedComponents, requiredComponents), nil -} - -func (useCase *ComponentInstallationUseCase) WaitForHealthyComponents(ctx context.Context) (ecosystem.ComponentHealthResult, error) { - logger := log.FromContext(ctx).WithName("ComponentInstallationUseCase.WaitForHealthyComponents") - - waitConfig, err := useCase.healthConfigProvider.GetWaitConfig(ctx) - if err != nil { - return ecosystem.ComponentHealthResult{}, fmt.Errorf("failed to get health check interval: %w", err) - } - - logger.Info("start waiting for component health") - healthResult, err := util.RetryUntilSuccessOrCancellation( - ctx, - waitConfig.Interval, - useCase.checkComponentHealthStatesRetryable, - ) - var result ecosystem.ComponentHealthResult - if healthResult == nil { - result = ecosystem.ComponentHealthResult{} - } else { - result = *healthResult - } - - if err != nil { - err = fmt.Errorf("stop waiting for component health: %w", err) - logger.Error(err, "stop waiting for component health because of an error or time out") - } else { - logger.Info("finished waiting for component health") - } - - return result, err -} - -func (useCase *ComponentInstallationUseCase) checkComponentHealthStatesRetryable(ctx context.Context) (result *ecosystem.ComponentHealthResult, err error, shouldRetry bool) { - // use named return values to make their meaning clear - health, err := useCase.CheckComponentHealth(ctx) - if err != nil { - // no retry on error while loading components - return &ecosystem.ComponentHealthResult{}, err, false - } - result = &health - shouldRetry = !health.AllHealthy() - return -} - -// ApplyComponentStates applies the expected component state from the Blueprint to the ecosystem. -// Fail-fast here, so that the possible damage is as small as possible. -func (useCase *ComponentInstallationUseCase) ApplyComponentStates(ctx context.Context, blueprintId string) error { - logger := log.FromContext(ctx).WithName("ComponentInstallationUseCase.ApplyComponentStates"). - WithValues("blueprintId", blueprintId) - log.IntoContext(ctx, logger) - - blueprintSpec, err := useCase.blueprintSpecRepo.GetById(ctx, blueprintId) - if err != nil { - return fmt.Errorf("cannot load blueprint spec %q to apply components: %w", blueprintId, err) - } - - if len(blueprintSpec.StateDiff.ComponentDiffs) == 0 { - logger.Info("apply no components because blueprint has no component state differences") - return nil - } - - // ComponentDiff contains all installed components anyway (but some with action none) so we can load them all at once - components, err := useCase.componentRepo.GetAll(ctx) - if err != nil { - return fmt.Errorf("cannot load component installations to apply component state: %w", err) - } - - for _, componentDiff := range blueprintSpec.StateDiff.ComponentDiffs { - err = useCase.applyComponentState(ctx, componentDiff, components[componentDiff.Name]) - if err != nil { - return fmt.Errorf("an error occurred while applying component state to the ecosystem: %w", err) - } - } - return nil -} - -func (useCase *ComponentInstallationUseCase) applyComponentState( - ctx context.Context, - componentDiff domain.ComponentDiff, - componentInstallation *ecosystem.ComponentInstallation, -) error { - logger := log.FromContext(ctx). - WithName("ComponentInstallationUseCase.applyComponentState"). - WithValues("component", componentDiff.Name, "diff", componentDiff.String()) - - for _, action := range componentDiff.NeededActions { - switch action { - case domain.ActionInstall: - logger.Info("install component") - newComponent := ecosystem.InstallComponent(common.QualifiedComponentName{ - Namespace: componentDiff.Expected.Namespace, - SimpleName: componentDiff.Name, - }, componentDiff.Expected.Version, componentDiff.Expected.DeployConfig) - return useCase.componentRepo.Create(ctx, newComponent) - case domain.ActionUninstall: - logger.Info("uninstall component") - return useCase.componentRepo.Delete(ctx, componentInstallation.Name.SimpleName) - case domain.ActionUpgrade: - componentInstallation.Upgrade(componentDiff.Expected.Version) - case domain.ActionUpdateComponentDeployConfig: - componentInstallation.UpdateDeployConfig(componentDiff.Expected.DeployConfig) - case domain.ActionSwitchComponentNamespace: - logger.Info("switch distribution namespace") - return errors.New(noDistributionNamespaceSwitchExplanationText) - case domain.ActionDowngrade: - logger.Info("downgrade component") - return fmt.Errorf(noDowngradesExplanationTextFmt, "components", "components") - default: - return fmt.Errorf("cannot perform unknown action %q", action) - } - } - - // If this routine did not terminate until this point, it is always an update. - if len(componentDiff.NeededActions) > 0 { - logger.Info("upgrade component") - return useCase.componentRepo.Update(ctx, componentInstallation) - } - - return nil -} diff --git a/pkg/application/componentInstallationUseCase_test.go b/pkg/application/componentInstallationUseCase_test.go deleted file mode 100644 index e196490a..00000000 --- a/pkg/application/componentInstallationUseCase_test.go +++ /dev/null @@ -1,626 +0,0 @@ -package application - -import ( - "context" - "fmt" - "github.com/Masterminds/semver/v3" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/common" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - "testing" - "time" -) - -const ( - componentName1 = "operator1" - testNamespace = "k8s" -) - -var ( - semVer3212, _ = semver.NewVersion("3.2.1-2") -) - -func TestNewComponentInstallationUseCase(t *testing.T) { - t.Run("success", func(t *testing.T) { - // given - blueprintSpecRepoMock := newMockBlueprintSpecRepository(t) - componentRepoMock := newMockComponentInstallationRepository(t) - healthConfigRepo := newMockHealthConfigProvider(t) - - // when - useCase := NewComponentInstallationUseCase(blueprintSpecRepoMock, componentRepoMock, healthConfigRepo) - - // then - assert.Equal(t, blueprintSpecRepoMock, useCase.blueprintSpecRepo) - assert.Equal(t, componentRepoMock, useCase.componentRepo) - assert.Same(t, healthConfigRepo, useCase.healthConfigProvider) - }) -} - -func TestComponentInstallationUseCase_ApplyComponentStates(t *testing.T) { - t.Run("success with no needed action", func(t *testing.T) { - // given - blueprintSpecRepoMock := newMockBlueprintSpecRepository(t) - componentRepoMock := newMockComponentInstallationRepository(t) - - expectedBlueprintSpec := &domain.BlueprintSpec{ - StateDiff: domain.StateDiff{ - ComponentDiffs: []domain.ComponentDiff{ - { - Name: componentName1, - NeededActions: []domain.Action{}, - Actual: domain.ComponentDiffState{ - Version: semVer3212, - }, - Expected: domain.ComponentDiffState{ - Version: semVer3212, - }, - }, - }, - }, - } - - allComponents := map[common.SimpleComponentName]*ecosystem.ComponentInstallation{ - componentName1: nil, - } - - blueprintSpecRepoMock.EXPECT().GetById(testCtx, blueprintId).Return(expectedBlueprintSpec, nil) - componentRepoMock.EXPECT().GetAll(testCtx).Return(allComponents, nil) - - sut := &ComponentInstallationUseCase{ - blueprintSpecRepo: blueprintSpecRepoMock, - componentRepo: componentRepoMock, - } - - // when - err := sut.ApplyComponentStates(testCtx, blueprintId) - - // then - require.NoError(t, err) - }) - - t.Run("should return error getting blueprint spec", func(t *testing.T) { - // given - blueprintSpecRepoMock := newMockBlueprintSpecRepository(t) - - blueprintSpecRepoMock.EXPECT().GetById(testCtx, blueprintId).Return(nil, assert.AnError) - - sut := &ComponentInstallationUseCase{ - blueprintSpecRepo: blueprintSpecRepoMock, - } - - // when - err := sut.ApplyComponentStates(testCtx, blueprintId) - - // then - require.Error(t, err) - assert.ErrorIs(t, err, assert.AnError) - assert.ErrorContains(t, err, "cannot load blueprint spec \"blueprint1\" to apply components") - }) - - t.Run("should return nil and do nothing with no component diffs", func(t *testing.T) { - // given - blueprintSpecRepoMock := newMockBlueprintSpecRepository(t) - - expectedBlueprintSpec := &domain.BlueprintSpec{} - - blueprintSpecRepoMock.EXPECT().GetById(testCtx, blueprintId).Return(expectedBlueprintSpec, nil) - - sut := &ComponentInstallationUseCase{ - blueprintSpecRepo: blueprintSpecRepoMock, - } - - // when - err := sut.ApplyComponentStates(testCtx, blueprintId) - - // then - require.NoError(t, err) - }) - - t.Run("should return error getting all components", func(t *testing.T) { - // given - blueprintSpecRepoMock := newMockBlueprintSpecRepository(t) - componentRepoMock := newMockComponentInstallationRepository(t) - - expectedBlueprintSpec := &domain.BlueprintSpec{ - StateDiff: domain.StateDiff{ - ComponentDiffs: []domain.ComponentDiff{ - {}, - }, - }, - } - - blueprintSpecRepoMock.EXPECT().GetById(testCtx, blueprintId).Return(expectedBlueprintSpec, nil) - componentRepoMock.EXPECT().GetAll(testCtx).Return(nil, assert.AnError) - - sut := &ComponentInstallationUseCase{ - blueprintSpecRepo: blueprintSpecRepoMock, - componentRepo: componentRepoMock, - } - - // when - err := sut.ApplyComponentStates(testCtx, blueprintId) - - // then - require.Error(t, err) - assert.ErrorIs(t, err, assert.AnError) - assert.ErrorContains(t, err, "cannot load component installations to apply component state") - }) - - t.Run("should return error with unknown action", func(t *testing.T) { - // given - blueprintSpecRepoMock := newMockBlueprintSpecRepository(t) - componentRepoMock := newMockComponentInstallationRepository(t) - - expectedBlueprintSpec := &domain.BlueprintSpec{ - StateDiff: domain.StateDiff{ - ComponentDiffs: []domain.ComponentDiff{ - { - Name: componentName1, - NeededActions: []domain.Action{"unknown"}, - }, - }, - }, - } - - allComponents := map[common.SimpleComponentName]*ecosystem.ComponentInstallation{ - componentName1: nil, - } - - blueprintSpecRepoMock.EXPECT().GetById(testCtx, blueprintId).Return(expectedBlueprintSpec, nil) - componentRepoMock.EXPECT().GetAll(testCtx).Return(allComponents, nil) - - sut := &ComponentInstallationUseCase{ - blueprintSpecRepo: blueprintSpecRepoMock, - componentRepo: componentRepoMock, - } - - // when - err := sut.ApplyComponentStates(testCtx, blueprintId) - - // then - require.Error(t, err) - assert.ErrorContains(t, err, "cannot perform unknown action \"unknown\"") - }) -} - -func TestComponentInstallationUseCase_applyComponentState(t *testing.T) { - t.Run("should create component on action install", func(t *testing.T) { - // given - blueprintSpecRepoMock := newMockBlueprintSpecRepository(t) - componentRepoMock := newMockComponentInstallationRepository(t) - - componentDiff := domain.ComponentDiff{ - Name: componentName1, - NeededActions: []domain.Action{domain.ActionInstall}, - Expected: domain.ComponentDiffState{ - Namespace: testNamespace, - Version: semVer3212, - }, - } - - componentInstallation := &ecosystem.ComponentInstallation{ - Name: common.QualifiedComponentName{SimpleName: componentName1, Namespace: testNamespace}, - ExpectedVersion: semVer3212, - } - - componentRepoMock.EXPECT().Create(testCtx, componentInstallation).Return(nil) - - sut := &ComponentInstallationUseCase{ - blueprintSpecRepo: blueprintSpecRepoMock, - componentRepo: componentRepoMock, - } - - // when - err := sut.applyComponentState(testCtx, componentDiff, componentInstallation) - - // then - require.NoError(t, err) - }) - - t.Run("should delete component on action uninstall", func(t *testing.T) { - // given - blueprintSpecRepoMock := newMockBlueprintSpecRepository(t) - componentRepoMock := newMockComponentInstallationRepository(t) - - componentDiff := domain.ComponentDiff{ - Name: componentName1, - NeededActions: []domain.Action{domain.ActionUninstall}, - } - - componentInstallation := &ecosystem.ComponentInstallation{ - Name: common.QualifiedComponentName{SimpleName: componentName1, Namespace: testNamespace}, - } - - componentRepoMock.EXPECT().Delete(testCtx, componentInstallation.Name.SimpleName).Return(nil) - - sut := &ComponentInstallationUseCase{ - blueprintSpecRepo: blueprintSpecRepoMock, - componentRepo: componentRepoMock, - } - - // when - err := sut.applyComponentState(testCtx, componentDiff, componentInstallation) - - // then - require.NoError(t, err) - }) - - t.Run("should update component on action upgrade", func(t *testing.T) { - // given - blueprintSpecRepoMock := newMockBlueprintSpecRepository(t) - componentRepoMock := newMockComponentInstallationRepository(t) - - componentDiff := domain.ComponentDiff{ - Name: componentName1, - Expected: domain.ComponentDiffState{ - Namespace: testNamespace, - Version: semVer3212, - }, - NeededActions: []domain.Action{domain.ActionUpgrade}, - } - - componentInstallation := &ecosystem.ComponentInstallation{ - Name: common.QualifiedComponentName{SimpleName: componentName1, Namespace: testNamespace}, - ExpectedVersion: semVer3212, - } - - componentRepoMock.EXPECT().Update(testCtx, componentInstallation).Return(nil) - - sut := &ComponentInstallationUseCase{ - blueprintSpecRepo: blueprintSpecRepoMock, - componentRepo: componentRepoMock, - } - - // when - err := sut.applyComponentState(testCtx, componentDiff, componentInstallation) - - // then - require.NoError(t, err) - }) - - t.Run("should update component with multiple actions", func(t *testing.T) { - // given - blueprintSpecRepoMock := newMockBlueprintSpecRepository(t) - componentRepoMock := newMockComponentInstallationRepository(t) - - componentDiff := domain.ComponentDiff{ - Name: componentName1, - Expected: domain.ComponentDiffState{ - Namespace: testNamespace, - Version: semVer3212, - DeployConfig: map[string]interface{}{ - "deployNamespace": "longhorn-system", - "overwriteConfig": map[string]string{ - "key": "value", - }, - }, - }, - NeededActions: []domain.Action{domain.ActionUpgrade, domain.ActionUpdateComponentDeployConfig}, - } - - componentInstallation := &ecosystem.ComponentInstallation{ - Name: common.QualifiedComponentName{SimpleName: componentName1, Namespace: testNamespace}, - ExpectedVersion: semVer3212, - DeployConfig: map[string]interface{}{ - "deployNamespace": "longhorn-system", - "overwriteConfig": map[string]string{ - "key": "value", - }, - }, - } - - componentRepoMock.EXPECT().Update(testCtx, componentInstallation).Return(nil) - - sut := &ComponentInstallationUseCase{ - blueprintSpecRepo: blueprintSpecRepoMock, - componentRepo: componentRepoMock, - } - - // when - err := sut.applyComponentState(testCtx, componentDiff, componentInstallation) - - // then - require.NoError(t, err) - }) - - t.Run("should return error on action downgrade", func(t *testing.T) { - // given - blueprintSpecRepoMock := newMockBlueprintSpecRepository(t) - componentRepoMock := newMockComponentInstallationRepository(t) - - componentDiff := domain.ComponentDiff{ - Name: componentName1, - NeededActions: []domain.Action{domain.ActionDowngrade}, - } - - componentInstallation := &ecosystem.ComponentInstallation{ - Name: common.QualifiedComponentName{SimpleName: componentName1, Namespace: testNamespace}, - } - - sut := &ComponentInstallationUseCase{ - blueprintSpecRepo: blueprintSpecRepoMock, - componentRepo: componentRepoMock, - } - - // when - err := sut.applyComponentState(testCtx, componentDiff, componentInstallation) - - // then - require.Error(t, err) - assert.ErrorContains(t, err, fmt.Sprintf(noDowngradesExplanationTextFmt, "components", "components")) - }) - - t.Run("should return error on action distribution namespace switch", func(t *testing.T) { - // given - blueprintSpecRepoMock := newMockBlueprintSpecRepository(t) - componentRepoMock := newMockComponentInstallationRepository(t) - - componentDiff := domain.ComponentDiff{ - Name: componentName1, - NeededActions: []domain.Action{domain.ActionSwitchComponentNamespace}, - } - - componentInstallation := &ecosystem.ComponentInstallation{ - Name: common.QualifiedComponentName{SimpleName: componentName1, Namespace: testNamespace}, - } - - sut := &ComponentInstallationUseCase{ - blueprintSpecRepo: blueprintSpecRepoMock, - componentRepo: componentRepoMock, - } - - // when - err := sut.applyComponentState(testCtx, componentDiff, componentInstallation) - - // then - require.Error(t, err) - assert.ErrorContains(t, err, noDistributionNamespaceSwitchExplanationText) - }) - - t.Run("should return no error on empty actions in diff", func(t *testing.T) { - // given - blueprintSpecRepoMock := newMockBlueprintSpecRepository(t) - componentRepoMock := newMockComponentInstallationRepository(t) - - componentDiff := domain.ComponentDiff{ - Name: componentName1, - NeededActions: []domain.Action{}, - } - - componentInstallation := &ecosystem.ComponentInstallation{ - Name: common.QualifiedComponentName{SimpleName: componentName1, Namespace: testNamespace}, - } - - sut := &ComponentInstallationUseCase{ - blueprintSpecRepo: blueprintSpecRepoMock, - componentRepo: componentRepoMock, - } - - // when - err := sut.applyComponentState(testCtx, componentDiff, componentInstallation) - - // then - require.NoError(t, err) - }) -} - -func TestComponentInstallationUseCase_CheckComponentHealth(t *testing.T) { - type fields struct { - componentRepoFn func(t *testing.T) componentInstallationRepository - healthConfigRepoFn func(t *testing.T) healthConfigProvider - } - tests := []struct { - name string - fields fields - want ecosystem.ComponentHealthResult - wantErr assert.ErrorAssertionFunc - }{ - { - name: "should fail to get installed components", - fields: fields{ - componentRepoFn: func(t *testing.T) componentInstallationRepository { - componentMock := newMockComponentInstallationRepository(t) - componentMock.EXPECT().GetAll(testCtx).Return(nil, assert.AnError) - return componentMock - }, - healthConfigRepoFn: func(t *testing.T) healthConfigProvider { - return newMockHealthConfigProvider(t) - }, - }, - want: ecosystem.ComponentHealthResult{}, - wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { - return assert.ErrorIs(t, err, assert.AnError, i) && - assert.ErrorContains(t, err, "cannot retrieve installed components", i) - }, - }, - { - name: "should fail to get required components", - fields: fields{ - componentRepoFn: func(t *testing.T) componentInstallationRepository { - componentMock := newMockComponentInstallationRepository(t) - componentMock.EXPECT().GetAll(testCtx). - Return(map[common.SimpleComponentName]*ecosystem.ComponentInstallation{}, nil) - return componentMock - }, - healthConfigRepoFn: func(t *testing.T) healthConfigProvider { - healthConfigMock := newMockHealthConfigProvider(t) - healthConfigMock.EXPECT().GetRequiredComponents(testCtx). - Return(nil, assert.AnError) - return healthConfigMock - }, - }, - want: ecosystem.ComponentHealthResult{}, - wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { - return assert.ErrorIs(t, err, assert.AnError, i) && - assert.ErrorContains(t, err, "cannot retrieve required components", i) - }, - }, - { - name: "should succeed", - fields: fields{ - componentRepoFn: func(t *testing.T) componentInstallationRepository { - componentMock := newMockComponentInstallationRepository(t) - componentMock.EXPECT().GetAll(testCtx). - Return(map[common.SimpleComponentName]*ecosystem.ComponentInstallation{ - "k8s-component-operator": { - Name: common.QualifiedComponentName{SimpleName: "k8s-component-operator", Namespace: testNamespace}, - Health: ecosystem.UnavailableHealthStatus}, - }, nil) - return componentMock - }, - healthConfigRepoFn: func(t *testing.T) healthConfigProvider { - healthConfigMock := newMockHealthConfigProvider(t) - healthConfigMock.EXPECT().GetRequiredComponents(testCtx). - Return([]ecosystem.RequiredComponent{{Name: "k8s-dogu-operator"}}, nil) - return healthConfigMock - }, - }, - want: ecosystem.ComponentHealthResult{ - ComponentsByStatus: map[ecosystem.HealthStatus][]common.SimpleComponentName{ - ecosystem.NotInstalledHealthStatus: {"k8s-dogu-operator"}, - ecosystem.UnavailableHealthStatus: {"k8s-component-operator"}, - }}, - wantErr: assert.NoError, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - useCase := &ComponentInstallationUseCase{ - componentRepo: tt.fields.componentRepoFn(t), - healthConfigProvider: tt.fields.healthConfigRepoFn(t), - } - got, err := useCase.CheckComponentHealth(testCtx) - tt.wantErr(t, err) - assert.Equal(t, tt.want, got) - }) - } -} - -func TestComponentInstallationUseCase_WaitForHealthyComponents(t *testing.T) { - type fields struct { - componentRepoFn func(t *testing.T) componentInstallationRepository - healthConfigRepoFn func(t *testing.T) healthConfigProvider - } - tests := []struct { - name string - ctx context.Context - fields fields - want ecosystem.ComponentHealthResult - wantErr assert.ErrorAssertionFunc - }{ - { - name: "should fail to get health check interval", - ctx: testCtx, - fields: fields{ - componentRepoFn: func(t *testing.T) componentInstallationRepository { - repoMock := newMockComponentInstallationRepository(t) - return repoMock - }, - healthConfigRepoFn: func(t *testing.T) healthConfigProvider { - providerMock := newMockHealthConfigProvider(t) - providerMock.EXPECT().GetWaitConfig(testCtx).Return(ecosystem.WaitConfig{}, assert.AnError) - return providerMock - }, - }, - want: ecosystem.ComponentHealthResult{}, - wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { - return assert.ErrorIs(t, err, assert.AnError, i) && - assert.ErrorContains(t, err, "failed to get health check interval", i) - }, - }, - { - name: "should fail to check component health", - ctx: testCtx, - fields: fields{ - componentRepoFn: func(t *testing.T) componentInstallationRepository { - repoMock := newMockComponentInstallationRepository(t) - repoMock.EXPECT().GetAll(mock.Anything).Return(nil, assert.AnError) - return repoMock - }, - healthConfigRepoFn: func(t *testing.T) healthConfigProvider { - providerMock := newMockHealthConfigProvider(t) - providerMock.EXPECT().GetWaitConfig(testCtx).Return(ecosystem.WaitConfig{Interval: time.Second}, nil) - return providerMock - }, - }, - want: ecosystem.ComponentHealthResult{}, - wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { - return assert.ErrorIs(t, err, assert.AnError, i) && - assert.ErrorContains(t, err, "stop waiting for component health", i) - }, - }, - { - name: "should fail after context cancellation", - ctx: func() context.Context { - ctx, cancel := context.WithCancel(testCtx) - cancel() - return ctx - }(), - fields: fields{ - componentRepoFn: func(t *testing.T) componentInstallationRepository { - componentMock := newMockComponentInstallationRepository(t) - componentMock.EXPECT().GetAll(mock.Anything). - Return(map[common.SimpleComponentName]*ecosystem.ComponentInstallation{}, nil) - return componentMock - }, - healthConfigRepoFn: func(t *testing.T) healthConfigProvider { - healthConfigMock := newMockHealthConfigProvider(t) - healthConfigMock.EXPECT().GetWaitConfig(mock.Anything).Return(ecosystem.WaitConfig{Interval: 1}, nil) - healthConfigMock.EXPECT().GetRequiredComponents(mock.Anything). - Return([]ecosystem.RequiredComponent{{Name: "k8s-dogu-operator"}}, nil) - return healthConfigMock - }, - }, - want: ecosystem.ComponentHealthResult{}, - wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { - return assert.ErrorContains(t, err, "stop waiting for component health: context canceled", i) - }, - }, - { - name: "should be successful after retry", - ctx: testCtx, - fields: fields{ - componentRepoFn: func(t *testing.T) componentInstallationRepository { - componentMock := newMockComponentInstallationRepository(t) - unsuccessfulCall := componentMock.EXPECT().GetAll(mock.Anything). - Return(map[common.SimpleComponentName]*ecosystem.ComponentInstallation{}, nil).Once() - componentMock.EXPECT().GetAll(mock.Anything). - Return(map[common.SimpleComponentName]*ecosystem.ComponentInstallation{ - "k8s-dogu-operator": { - - Name: common.QualifiedComponentName{SimpleName: "k8s-dogu-operator", Namespace: testNamespace}, Health: ecosystem.AvailableHealthStatus, - }, - }, nil). - Once().NotBefore(unsuccessfulCall) - return componentMock - }, - healthConfigRepoFn: func(t *testing.T) healthConfigProvider { - healthConfigMock := newMockHealthConfigProvider(t) - healthConfigMock.EXPECT().GetWaitConfig(testCtx).Return(ecosystem.WaitConfig{Interval: time.Second}, nil) - healthConfigMock.EXPECT().GetRequiredComponents(mock.Anything). - Return([]ecosystem.RequiredComponent{{Name: "k8s-dogu-operator"}}, nil) - return healthConfigMock - }, - }, - want: ecosystem.ComponentHealthResult{ComponentsByStatus: map[ecosystem.HealthStatus][]common.SimpleComponentName{ - ecosystem.AvailableHealthStatus: {"k8s-dogu-operator"}, - }}, - wantErr: assert.NoError, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - useCase := &ComponentInstallationUseCase{ - componentRepo: tt.fields.componentRepoFn(t), - healthConfigProvider: tt.fields.healthConfigRepoFn(t), - } - got, err := useCase.WaitForHealthyComponents(tt.ctx) - tt.wantErr(t, err) - assert.Equal(t, tt.want, got) - }) - } -} diff --git a/pkg/application/doguInstallationUseCase.go b/pkg/application/doguInstallationUseCase.go index aa0b97bc..99ec0f68 100644 --- a/pkg/application/doguInstallationUseCase.go +++ b/pkg/application/doguInstallationUseCase.go @@ -3,36 +3,44 @@ package application import ( "context" "fmt" + cescommons "github.com/cloudogu/ces-commons-lib/dogu" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domainservice" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/util" "golang.org/x/exp/maps" "sigs.k8s.io/controller-runtime/pkg/log" ) +const noDowngradesExplanationTextFmt = "downgrades are not allowed as the data model of the %s could have changed and " + + "doing rollbacks to older models is not supported. " + + "You can downgrade %s by restoring a backup. " + + "If you want an 'allow-downgrades' flag, issue a feature request" + type DoguInstallationUseCase struct { - blueprintSpecRepo blueprintSpecRepository - doguRepo doguInstallationRepository - waitConfigProvider healthWaitConfigProvider + blueprintSpecRepo blueprintSpecRepository + doguRepo doguInstallationRepository + doguConfigRepo doguConfigRepository + globalConfigRepo globalConfigRepository } func NewDoguInstallationUseCase( blueprintSpecRepo domainservice.BlueprintSpecRepository, doguRepo domainservice.DoguInstallationRepository, - waitConfigProvider domainservice.HealthWaitConfigProvider, + doguConfigRepo doguConfigRepository, + globalConfigRepo globalConfigRepository, ) *DoguInstallationUseCase { return &DoguInstallationUseCase{ - blueprintSpecRepo: blueprintSpecRepo, - doguRepo: doguRepo, - waitConfigProvider: waitConfigProvider, + blueprintSpecRepo: blueprintSpecRepo, + doguRepo: doguRepo, + doguConfigRepo: doguConfigRepo, + globalConfigRepo: globalConfigRepo, } } func (useCase *DoguInstallationUseCase) CheckDoguHealth(ctx context.Context) (ecosystem.DoguHealthResult, error) { logger := log.FromContext(ctx).WithName("DoguInstallationUseCase.CheckDoguHealth") - logger.Info("check dogu health...") + logger.V(2).Info("check dogu health...") installedDogus, err := useCase.doguRepo.GetAll(ctx) if err != nil { return ecosystem.DoguHealthResult{}, fmt.Errorf("cannot evaluate dogu health states: %w", err) @@ -41,69 +49,57 @@ func (useCase *DoguInstallationUseCase) CheckDoguHealth(ctx context.Context) (ec return ecosystem.CalculateDoguHealthResult(maps.Values(installedDogus)), nil } -func (useCase *DoguInstallationUseCase) WaitForHealthyDogus(ctx context.Context) (ecosystem.DoguHealthResult, error) { - logger := log.FromContext(ctx).WithName("DoguInstallationUseCase.WaitForHealthyDogus") - - waitConfig, err := useCase.waitConfigProvider.GetWaitConfig(ctx) +func (useCase *DoguInstallationUseCase) CheckDogusUpToDate(ctx context.Context) ([]cescommons.SimpleName, error) { + logger := log.FromContext(ctx).WithName("DoguInstallationUseCase.CheckDoguHealth") + logger.V(2).Info("check if dogus are up to date...") + installedDogus, err := useCase.doguRepo.GetAll(ctx) if err != nil { - return ecosystem.DoguHealthResult{}, fmt.Errorf("failed to get health check interval: %w", err) - } - - logger.Info("start waiting for dogu health") - healthResult, err := util.RetryUntilSuccessOrCancellation( - ctx, - waitConfig.Interval, - useCase.checkDoguHealthStatesRetryable, - ) - var result ecosystem.DoguHealthResult - if healthResult == nil { - result = ecosystem.DoguHealthResult{} - } else { - result = *healthResult + return nil, err } + globalConfig, err := useCase.globalConfigRepo.Get(ctx) if err != nil { - err = fmt.Errorf("stop waiting for dogu health: %w", err) - logger.Error(err, "stop waiting for dogu health because of an error or time out") - } else { - logger.Info("finished waiting for dogu health") + return nil, err } + globalConfigUpdateTime := globalConfig.LastUpdated - return result, err -} + var dogusNotUpToDate []cescommons.SimpleName -func (useCase *DoguInstallationUseCase) checkDoguHealthStatesRetryable(ctx context.Context) (result *ecosystem.DoguHealthResult, err error, shouldRetry bool) { - // use named return values to make their meaning clear - health, err := useCase.CheckDoguHealth(ctx) - if err != nil { - // no retry if error while loading dogus - return &ecosystem.DoguHealthResult{}, err, false + for doguName, dogu := range installedDogus { + versionUpToDate := dogu.IsVersionUpToDate() + if !versionUpToDate { + dogusNotUpToDate = append(dogusNotUpToDate, doguName) + continue + } + + doguConfig, err := useCase.doguConfigRepo.Get(ctx, doguName) + if err != nil { + return nil, err + } + doguConfigUpdateTime := doguConfig.LastUpdated + configUpToDate := dogu.IsConfigUpToDate(globalConfigUpdateTime, doguConfigUpdateTime) + if !configUpToDate { + dogusNotUpToDate = append(dogusNotUpToDate, doguName) + continue + } } - result = &health - shouldRetry = !health.AllHealthy() - return + + return dogusNotUpToDate, nil } // ApplyDoguStates applies the expected dogu state from the Blueprint to the ecosystem. // Fail-fast here, so that the possible damage is as small as possible. -func (useCase *DoguInstallationUseCase) ApplyDoguStates(ctx context.Context, blueprintId string) error { - logger := log.FromContext(ctx).WithName("DoguInstallationUseCase.ApplyDoguChanges"). - WithValues("blueprintId", blueprintId) - log.IntoContext(ctx, logger) - - blueprintSpec, err := useCase.blueprintSpecRepo.GetById(ctx, blueprintId) - if err != nil { - return fmt.Errorf("cannot load blueprint spec %q to install dogus: %w", blueprintId, err) - } - +func (useCase *DoguInstallationUseCase) ApplyDoguStates(ctx context.Context, blueprint *domain.BlueprintSpec) error { + logger := log.FromContext(ctx).WithName("DoguInstallationUseCase.ApplyDoguChanges") + logger.V(2).Info("apply dogu states") // DoguDiff contains all installed dogus anyway (but some with action none) so we can load them all at once dogus, err := useCase.doguRepo.GetAll(ctx) if err != nil { return fmt.Errorf("cannot load dogu installations to apply dogu state: %w", err) } - for _, doguDiff := range blueprintSpec.StateDiff.DoguDiffs { - err = useCase.applyDoguState(ctx, doguDiff, dogus[doguDiff.DoguName], blueprintSpec.Config) + for _, doguDiff := range blueprint.StateDiff.DoguDiffs { + err = useCase.applyDoguState(ctx, doguDiff, dogus[doguDiff.DoguName], blueprint.Config) if err != nil { return fmt.Errorf("an error occurred while applying dogu state to the ecosystem: %w", err) } @@ -133,6 +129,9 @@ func (useCase *DoguInstallationUseCase) applyDoguState( ) return useCase.doguRepo.Create(ctx, newDogu) case domain.ActionUninstall: + if doguInstallation == nil { + return &domainservice.NotFoundError{Message: fmt.Sprintf("dogu %q not found", doguDiff.DoguName)} + } logger.Info("uninstall dogu") return useCase.doguRepo.Delete(ctx, doguInstallation.Name.SimpleName) case domain.ActionUpgrade: @@ -155,17 +154,9 @@ func (useCase *DoguInstallationUseCase) applyDoguState( logger.Info("update minimum volume size for dogu") doguInstallation.UpdateMinVolumeSize(doguDiff.Expected.MinVolumeSize) continue - case domain.ActionUpdateDoguProxyBodySize: - logger.Info("update proxy body size for dogu") - doguInstallation.UpdateProxyBodySize(doguDiff.Expected.ReverseProxyConfig.MaxBodySize) - continue - case domain.ActionUpdateDoguProxyRewriteTarget: - logger.Info("update proxy rewrite target for dogu") - doguInstallation.UpdateProxyRewriteTarget(doguDiff.Expected.ReverseProxyConfig.RewriteTarget) - continue - case domain.ActionUpdateDoguProxyAdditionalConfig: - logger.Info("update proxy additional config for dogu") - doguInstallation.UpdateProxyAdditionalConfig(doguDiff.Expected.ReverseProxyConfig.AdditionalConfig) + case domain.ActionUpdateDoguReverseProxyConfig: + logger.Info("update proxy config for dogu") + doguInstallation.UpdateProxyConfig(doguDiff.Expected.ReverseProxyConfig) continue case domain.ActionUpdateAdditionalMounts: logger.Info("update additional mounts") @@ -179,6 +170,8 @@ func (useCase *DoguInstallationUseCase) applyDoguState( // If this routine did not terminate until this point, it is always an update. if len(doguDiff.NeededActions) > 0 { logger.Info("upgrade dogu") + // remove potential pause reconciliation flags here so that the dogu gets updates again + doguInstallation.SetReconciliationPaused(false) return useCase.doguRepo.Update(ctx, doguInstallation) } diff --git a/pkg/application/doguInstallationUseCase_test.go b/pkg/application/doguInstallationUseCase_test.go index 8e8cd135..ea06a58c 100644 --- a/pkg/application/doguInstallationUseCase_test.go +++ b/pkg/application/doguInstallationUseCase_test.go @@ -1,17 +1,20 @@ package application import ( - "context" "fmt" + "testing" + "time" + cescommons "github.com/cloudogu/ces-commons-lib/dogu" "github.com/cloudogu/cesapp-lib/core" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" + "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domainservice" + "github.com/cloudogu/k8s-registry-lib/config" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/api/resource" - "testing" - "time" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) const blueprintId = "blueprint1" @@ -23,24 +26,39 @@ var postgresqlQualifiedName = cescommons.QualifiedName{ Namespace: "official", SimpleName: "postgresql", } +var ldapQualifiedName = cescommons.QualifiedName{ + Namespace: "official", + SimpleName: "ldap", +} +var casQualifiedName = cescommons.QualifiedName{ + Namespace: "official", + SimpleName: "cas", +} + +var ( + rewriteTarget = "/" + additionalConfig = "additional" + subfolder = "subfolder" + subfolder2 = "secsubfolder" +) func TestDoguInstallationUseCase_applyDoguState(t *testing.T) { t.Run("action none", func(t *testing.T) { // given - sut := NewDoguInstallationUseCase(nil, nil, nil) + sut := NewDoguInstallationUseCase(nil, nil, nil, nil) // when err := sut.applyDoguState(testCtx, domain.DoguDiff{ DoguName: "postgresql", Actual: domain.DoguDiffState{ - Namespace: "official", - Version: version3211, - InstallationState: domain.TargetStatePresent, + Namespace: "official", + Version: &version3211, + Absent: false, }, Expected: domain.DoguDiffState{ - Namespace: "official", - Version: version3211, - InstallationState: domain.TargetStatePresent, + Namespace: "official", + Version: &version3211, + Absent: false, }, NeededActions: []domain.Action{}, }, &ecosystem.DoguInstallation{ @@ -55,27 +73,27 @@ func TestDoguInstallationUseCase_applyDoguState(t *testing.T) { t.Run("action install", func(t *testing.T) { volumeSize := resource.MustParse("2Gi") bodySize := resource.MustParse("2G") - config := ecosystem.ReverseProxyConfig{ + proxyConfig := ecosystem.ReverseProxyConfig{ MaxBodySize: &bodySize, - RewriteTarget: "/", - AdditionalConfig: "additional", + RewriteTarget: ecosystem.RewriteTarget(rewriteTarget), + AdditionalConfig: ecosystem.AdditionalConfig(additionalConfig), } additionalMounts := []ecosystem.AdditionalMount{ { SourceType: ecosystem.DataSourceConfigMap, Name: "configmap", Volume: "volume", - Subfolder: "different_subfolder", + Subfolder: subfolder, }, } doguRepoMock := newMockDoguInstallationRepository(t) doguRepoMock.EXPECT(). Create(testCtx, - ecosystem.InstallDogu(postgresqlQualifiedName, version3211, volumeSize, config, additionalMounts)). + ecosystem.InstallDogu(postgresqlQualifiedName, &version3211, &volumeSize, proxyConfig, additionalMounts)). Return(nil) - sut := NewDoguInstallationUseCase(nil, doguRepoMock, nil) + sut := NewDoguInstallationUseCase(nil, doguRepoMock, nil, nil) // when err := sut.applyDoguState( @@ -83,22 +101,22 @@ func TestDoguInstallationUseCase_applyDoguState(t *testing.T) { domain.DoguDiff{ DoguName: "postgresql", Actual: domain.DoguDiffState{ - Namespace: "official", - Version: version3211, - InstallationState: domain.TargetStateAbsent, + Namespace: "official", + Version: &version3211, + Absent: true, }, Expected: domain.DoguDiffState{ Namespace: "official", - Version: version3211, - InstallationState: domain.TargetStatePresent, - MinVolumeSize: volumeSize, - ReverseProxyConfig: config, + Version: &version3211, + Absent: false, + MinVolumeSize: &volumeSize, + ReverseProxyConfig: proxyConfig, AdditionalMounts: []ecosystem.AdditionalMount{ { SourceType: ecosystem.DataSourceConfigMap, Name: "configmap", Volume: "volume", - Subfolder: "different_subfolder", + Subfolder: subfolder, }, }, }, @@ -118,7 +136,7 @@ func TestDoguInstallationUseCase_applyDoguState(t *testing.T) { Delete(testCtx, cescommons.SimpleName("postgresql")). Return(nil) - sut := NewDoguInstallationUseCase(nil, doguRepoMock, nil) + sut := NewDoguInstallationUseCase(nil, doguRepoMock, nil, nil) // when err := sut.applyDoguState( @@ -138,6 +156,28 @@ func TestDoguInstallationUseCase_applyDoguState(t *testing.T) { require.NoError(t, err) }) + t.Run("action uninstall throws NotFoundError when dogu not found", func(t *testing.T) { + doguRepoMock := newMockDoguInstallationRepository(t) + + sut := NewDoguInstallationUseCase(nil, doguRepoMock, nil, nil) + + // when + err := sut.applyDoguState( + testCtx, + domain.DoguDiff{ + DoguName: "postgresql", + NeededActions: []domain.Action{domain.ActionUninstall}, + }, + nil, + domain.BlueprintConfiguration{}, + ) + + // then + require.Error(t, err) + var targetError *domainservice.NotFoundError + assert.ErrorAs(t, err, &targetError) + }) + t.Run("action upgrade", func(t *testing.T) { dogu := &ecosystem.DoguInstallation{ Name: postgresqlQualifiedName, @@ -148,7 +188,9 @@ func TestDoguInstallationUseCase_applyDoguState(t *testing.T) { Update(testCtx, dogu). Return(nil) - sut := NewDoguInstallationUseCase(nil, doguRepoMock, nil) + sut := NewDoguInstallationUseCase(nil, doguRepoMock, nil, nil) + + dogu.PauseReconciliation = true // test if it gets reset on update (the dogu in the EXPECT Update call has this to false) // when err := sut.applyDoguState( @@ -156,7 +198,7 @@ func TestDoguInstallationUseCase_applyDoguState(t *testing.T) { domain.DoguDiff{ DoguName: "postgresql", Expected: domain.DoguDiffState{ - Version: version3212, + Version: &version3212, }, NeededActions: []domain.Action{domain.ActionUpgrade}, }, @@ -176,7 +218,7 @@ func TestDoguInstallationUseCase_applyDoguState(t *testing.T) { Version: version3212, } - sut := NewDoguInstallationUseCase(nil, nil, nil) + sut := NewDoguInstallationUseCase(nil, nil, nil, nil) // when err := sut.applyDoguState( @@ -184,7 +226,7 @@ func TestDoguInstallationUseCase_applyDoguState(t *testing.T) { domain.DoguDiff{ DoguName: "postgresql", Expected: domain.DoguDiffState{ - Version: version3211, + Version: &version3211, }, NeededActions: []domain.Action{domain.ActionDowngrade}, }, @@ -202,18 +244,18 @@ func TestDoguInstallationUseCase_applyDoguState(t *testing.T) { expectedVolumeSize := resource.MustParse("3Gi") expectedDogu := &ecosystem.DoguInstallation{ Name: postgresqlQualifiedName, - MinVolumeSize: expectedVolumeSize, + MinVolumeSize: &expectedVolumeSize, } dogu := &ecosystem.DoguInstallation{ Name: postgresqlQualifiedName, - MinVolumeSize: volumeSize, + MinVolumeSize: &volumeSize, } doguRepoMock := newMockDoguInstallationRepository(t) doguRepoMock.EXPECT().Update(testCtx, expectedDogu).Return(nil) - sut := NewDoguInstallationUseCase(nil, doguRepoMock, nil) + sut := NewDoguInstallationUseCase(nil, doguRepoMock, nil, nil) // when err := sut.applyDoguState( @@ -221,7 +263,7 @@ func TestDoguInstallationUseCase_applyDoguState(t *testing.T) { domain.DoguDiff{ DoguName: "postgresql", Expected: domain.DoguDiffState{ - MinVolumeSize: expectedVolumeSize, + MinVolumeSize: &expectedVolumeSize, }, NeededActions: []domain.Action{domain.ActionUpdateDoguResourceMinVolumeSize}, }, @@ -253,7 +295,7 @@ func TestDoguInstallationUseCase_applyDoguState(t *testing.T) { doguRepoMock := newMockDoguInstallationRepository(t) doguRepoMock.EXPECT().Update(testCtx, expectedDogu).Return(nil) - sut := NewDoguInstallationUseCase(nil, doguRepoMock, nil) + sut := NewDoguInstallationUseCase(nil, doguRepoMock, nil, nil) // when err := sut.applyDoguState( @@ -265,7 +307,7 @@ func TestDoguInstallationUseCase_applyDoguState(t *testing.T) { MaxBodySize: &expectedProxyBodySize, }, }, - NeededActions: []domain.Action{domain.ActionUpdateDoguProxyBodySize}, + NeededActions: []domain.Action{domain.ActionUpdateDoguReverseProxyConfig}, }, dogu, domain.BlueprintConfiguration{}, @@ -276,26 +318,23 @@ func TestDoguInstallationUseCase_applyDoguState(t *testing.T) { }) t.Run("action update proxy rewrite target", func(t *testing.T) { - target := ecosystem.RewriteTarget("") - expectedTarget := ecosystem.RewriteTarget("/") + expectedTarget := ecosystem.RewriteTarget(rewriteTarget) expectedDogu := &ecosystem.DoguInstallation{ Name: postgresqlQualifiedName, ReverseProxyConfig: ecosystem.ReverseProxyConfig{ - RewriteTarget: expectedTarget, + RewriteTarget: ecosystem.RewriteTarget(rewriteTarget), }, } dogu := &ecosystem.DoguInstallation{ - Name: postgresqlQualifiedName, - ReverseProxyConfig: ecosystem.ReverseProxyConfig{ - RewriteTarget: target, - }, + Name: postgresqlQualifiedName, + ReverseProxyConfig: ecosystem.ReverseProxyConfig{}, } doguRepoMock := newMockDoguInstallationRepository(t) doguRepoMock.EXPECT().Update(testCtx, expectedDogu).Return(nil) - sut := NewDoguInstallationUseCase(nil, doguRepoMock, nil) + sut := NewDoguInstallationUseCase(nil, doguRepoMock, nil, nil) // when err := sut.applyDoguState( @@ -307,7 +346,7 @@ func TestDoguInstallationUseCase_applyDoguState(t *testing.T) { RewriteTarget: expectedTarget, }, }, - NeededActions: []domain.Action{domain.ActionUpdateDoguProxyRewriteTarget}, + NeededActions: []domain.Action{domain.ActionUpdateDoguReverseProxyConfig}, }, dogu, domain.BlueprintConfiguration{}, @@ -318,8 +357,7 @@ func TestDoguInstallationUseCase_applyDoguState(t *testing.T) { }) t.Run("action update proxy additional config", func(t *testing.T) { - additionalConfig := ecosystem.AdditionalConfig("") - expectedAdditionalConfig := ecosystem.AdditionalConfig("snippet") + expectedAdditionalConfig := ecosystem.AdditionalConfig(additionalConfig) expectedDogu := &ecosystem.DoguInstallation{ Name: postgresqlQualifiedName, ReverseProxyConfig: ecosystem.ReverseProxyConfig{ @@ -328,16 +366,14 @@ func TestDoguInstallationUseCase_applyDoguState(t *testing.T) { } dogu := &ecosystem.DoguInstallation{ - Name: postgresqlQualifiedName, - ReverseProxyConfig: ecosystem.ReverseProxyConfig{ - AdditionalConfig: additionalConfig, - }, + Name: postgresqlQualifiedName, + ReverseProxyConfig: ecosystem.ReverseProxyConfig{}, } doguRepoMock := newMockDoguInstallationRepository(t) doguRepoMock.EXPECT().Update(testCtx, expectedDogu).Return(nil) - sut := NewDoguInstallationUseCase(nil, doguRepoMock, nil) + sut := NewDoguInstallationUseCase(nil, doguRepoMock, nil, nil) // when err := sut.applyDoguState( @@ -349,7 +385,7 @@ func TestDoguInstallationUseCase_applyDoguState(t *testing.T) { AdditionalConfig: expectedAdditionalConfig, }, }, - NeededActions: []domain.Action{domain.ActionUpdateDoguProxyAdditionalConfig}, + NeededActions: []domain.Action{domain.ActionUpdateDoguReverseProxyConfig}, }, dogu, domain.BlueprintConfiguration{}, @@ -367,13 +403,13 @@ func TestDoguInstallationUseCase_applyDoguState(t *testing.T) { SourceType: ecosystem.DataSourceConfigMap, Name: "configmap", Volume: "volume", - Subfolder: "different_subfolder", + Subfolder: subfolder, }, { SourceType: ecosystem.DataSourceSecret, Name: "secret", Volume: "secvolume", - Subfolder: "secsubfolder", + Subfolder: subfolder2, }, }, } @@ -385,13 +421,13 @@ func TestDoguInstallationUseCase_applyDoguState(t *testing.T) { SourceType: ecosystem.DataSourceConfigMap, Name: "configmap", Volume: "volume", - Subfolder: "subfolder", + Subfolder: subfolder, }, { SourceType: ecosystem.DataSourceSecret, Name: "secret", Volume: "secvolume", - Subfolder: "secsubfolder", + Subfolder: subfolder2, }, }, } @@ -404,13 +440,13 @@ func TestDoguInstallationUseCase_applyDoguState(t *testing.T) { SourceType: ecosystem.DataSourceConfigMap, Name: "configmap", Volume: "volume", - Subfolder: "different_subfolder", + Subfolder: subfolder, }, { SourceType: ecosystem.DataSourceSecret, Name: "secret", Volume: "secvolume", - Subfolder: "secsubfolder", + Subfolder: subfolder2, }, }, }, @@ -420,7 +456,7 @@ func TestDoguInstallationUseCase_applyDoguState(t *testing.T) { doguRepoMock := newMockDoguInstallationRepository(t) doguRepoMock.EXPECT().Update(testCtx, expectedDogu).Return(nil) - sut := NewDoguInstallationUseCase(nil, doguRepoMock, nil) + sut := NewDoguInstallationUseCase(nil, doguRepoMock, nil, nil) // when err := sut.applyDoguState(testCtx, diff, dogu, domain.BlueprintConfiguration{}) @@ -434,14 +470,12 @@ func TestDoguInstallationUseCase_applyDoguState(t *testing.T) { expectedVolumeSize := resource.MustParse("3Gi") proxyBodySize := resource.MustParse("2G") expectedProxyBodySize := resource.MustParse("3G") - target := ecosystem.RewriteTarget("") - expectedTarget := ecosystem.RewriteTarget("/") - additionalConfig := ecosystem.AdditionalConfig("") - expectedAdditionalConfig := ecosystem.AdditionalConfig("snippet") + expectedTarget := ecosystem.RewriteTarget(rewriteTarget) + expectedAdditionalConfig := ecosystem.AdditionalConfig(additionalConfig) expectedDogu := &ecosystem.DoguInstallation{ Name: postgresqlQualifiedName, Version: version3212, - MinVolumeSize: expectedVolumeSize, + MinVolumeSize: &expectedVolumeSize, ReverseProxyConfig: ecosystem.ReverseProxyConfig{ MaxBodySize: &expectedProxyBodySize, RewriteTarget: expectedTarget, @@ -452,18 +486,16 @@ func TestDoguInstallationUseCase_applyDoguState(t *testing.T) { dogu := &ecosystem.DoguInstallation{ Name: postgresqlQualifiedName, Version: version3211, - MinVolumeSize: volumeSize, + MinVolumeSize: &volumeSize, ReverseProxyConfig: ecosystem.ReverseProxyConfig{ - MaxBodySize: &proxyBodySize, - RewriteTarget: target, - AdditionalConfig: additionalConfig, + MaxBodySize: &proxyBodySize, }, } doguRepoMock := newMockDoguInstallationRepository(t) doguRepoMock.EXPECT().Update(testCtx, expectedDogu).Return(nil) - sut := NewDoguInstallationUseCase(nil, doguRepoMock, nil) + sut := NewDoguInstallationUseCase(nil, doguRepoMock, nil, nil) // when err := sut.applyDoguState( @@ -471,8 +503,8 @@ func TestDoguInstallationUseCase_applyDoguState(t *testing.T) { domain.DoguDiff{ DoguName: "postgresql", Expected: domain.DoguDiffState{ - Version: version3212, - MinVolumeSize: expectedVolumeSize, + Version: &version3212, + MinVolumeSize: &expectedVolumeSize, ReverseProxyConfig: ecosystem.ReverseProxyConfig{ MaxBodySize: &expectedProxyBodySize, RewriteTarget: expectedTarget, @@ -481,9 +513,7 @@ func TestDoguInstallationUseCase_applyDoguState(t *testing.T) { }, NeededActions: []domain.Action{ domain.ActionUpgrade, - domain.ActionUpdateDoguProxyAdditionalConfig, - domain.ActionUpdateDoguProxyBodySize, - domain.ActionUpdateDoguProxyRewriteTarget, + domain.ActionUpdateDoguReverseProxyConfig, domain.ActionUpdateDoguResourceMinVolumeSize, }, }, @@ -501,7 +531,7 @@ func TestDoguInstallationUseCase_applyDoguState(t *testing.T) { Version: version3212, } - sut := NewDoguInstallationUseCase(nil, nil, nil) + sut := NewDoguInstallationUseCase(nil, nil, nil, nil) // when err := sut.applyDoguState( @@ -531,7 +561,7 @@ func TestDoguInstallationUseCase_applyDoguState(t *testing.T) { doguRepoMock := newMockDoguInstallationRepository(t) doguRepoMock.EXPECT().Update(testCtx, dogu).Return(nil) - sut := NewDoguInstallationUseCase(nil, doguRepoMock, nil) + sut := NewDoguInstallationUseCase(nil, doguRepoMock, nil, nil) // when err := sut.applyDoguState( @@ -556,7 +586,7 @@ func TestDoguInstallationUseCase_applyDoguState(t *testing.T) { t.Run("unknown action", func(t *testing.T) { // given - sut := NewDoguInstallationUseCase(nil, nil, nil) + sut := NewDoguInstallationUseCase(nil, nil, nil, nil) // when err := sut.applyDoguState( @@ -578,7 +608,7 @@ func TestDoguInstallationUseCase_applyDoguState(t *testing.T) { t.Run("should no fail with no actions", func(t *testing.T) { // given - sut := NewDoguInstallationUseCase(nil, nil, nil) + sut := NewDoguInstallationUseCase(nil, nil, nil, nil) // when err := sut.applyDoguState( @@ -600,34 +630,15 @@ func TestDoguInstallationUseCase_applyDoguState(t *testing.T) { } func TestDoguInstallationUseCase_ApplyDoguStates(t *testing.T) { - t.Run("cannot load blueprintSpec", func(t *testing.T) { - // given - blueprintSpecRepoMock := newMockBlueprintSpecRepository(t) - blueprintSpecRepoMock.EXPECT().GetById(testCtx, blueprintId).Return(nil, assert.AnError) - - doguRepoMock := newMockDoguInstallationRepository(t) - - sut := NewDoguInstallationUseCase(blueprintSpecRepoMock, doguRepoMock, nil) - - // when - err := sut.ApplyDoguStates(testCtx, blueprintId) - - // then - require.ErrorIs(t, err, assert.AnError) - }) - t.Run("cannot load doguInstallations", func(t *testing.T) { // given - blueprintSpecRepoMock := newMockBlueprintSpecRepository(t) - blueprintSpecRepoMock.EXPECT().GetById(testCtx, blueprintId).Return(nil, nil) - doguRepoMock := newMockDoguInstallationRepository(t) doguRepoMock.EXPECT().GetAll(testCtx).Return(nil, assert.AnError) - sut := NewDoguInstallationUseCase(blueprintSpecRepoMock, doguRepoMock, nil) + sut := NewDoguInstallationUseCase(nil, doguRepoMock, nil, nil) // when - err := sut.ApplyDoguStates(testCtx, blueprintId) + err := sut.ApplyDoguStates(testCtx, &domain.BlueprintSpec{}) // then require.ErrorIs(t, err, assert.AnError) @@ -636,8 +647,7 @@ func TestDoguInstallationUseCase_ApplyDoguStates(t *testing.T) { t.Run("success", func(t *testing.T) { // given - blueprintSpecRepoMock := newMockBlueprintSpecRepository(t) - blueprintSpecRepoMock.EXPECT().GetById(testCtx, blueprintId).Return(&domain.BlueprintSpec{ + blueprint := &domain.BlueprintSpec{ StateDiff: domain.StateDiff{ DoguDiffs: []domain.DoguDiff{ { @@ -647,15 +657,16 @@ func TestDoguInstallationUseCase_ApplyDoguStates(t *testing.T) { }, }, Config: domain.BlueprintConfiguration{}, - }, nil) + } + blueprintSpecRepoMock := newMockBlueprintSpecRepository(t) doguRepoMock := newMockDoguInstallationRepository(t) doguRepoMock.EXPECT().GetAll(testCtx).Return(map[cescommons.SimpleName]*ecosystem.DoguInstallation{}, nil) - sut := NewDoguInstallationUseCase(blueprintSpecRepoMock, doguRepoMock, nil) + sut := NewDoguInstallationUseCase(blueprintSpecRepoMock, doguRepoMock, nil, nil) // when - err := sut.ApplyDoguStates(testCtx, blueprintId) + err := sut.ApplyDoguStates(testCtx, blueprint) // then require.NoError(t, err) @@ -663,8 +674,7 @@ func TestDoguInstallationUseCase_ApplyDoguStates(t *testing.T) { t.Run("action error", func(t *testing.T) { // given - blueprintSpecRepoMock := newMockBlueprintSpecRepository(t) - blueprintSpecRepoMock.EXPECT().GetById(testCtx, blueprintId).Return(&domain.BlueprintSpec{ + blueprint := &domain.BlueprintSpec{ StateDiff: domain.StateDiff{ DoguDiffs: []domain.DoguDiff{ { @@ -674,7 +684,7 @@ func TestDoguInstallationUseCase_ApplyDoguStates(t *testing.T) { }, }, Config: domain.BlueprintConfiguration{}, - }, nil) + } doguRepoMock := newMockDoguInstallationRepository(t) doguRepoMock.EXPECT().GetAll(testCtx).Return(map[cescommons.SimpleName]*ecosystem.DoguInstallation{ @@ -685,10 +695,10 @@ func TestDoguInstallationUseCase_ApplyDoguStates(t *testing.T) { }, }, nil) - sut := NewDoguInstallationUseCase(blueprintSpecRepoMock, doguRepoMock, nil) + sut := NewDoguInstallationUseCase(nil, doguRepoMock, nil, nil) // when - err := sut.ApplyDoguStates(testCtx, blueprintId) + err := sut.ApplyDoguStates(testCtx, blueprint) // then require.ErrorContains(t, err, fmt.Sprintf(noDowngradesExplanationTextFmt, "dogu", "dogus")) @@ -696,106 +706,309 @@ func TestDoguInstallationUseCase_ApplyDoguStates(t *testing.T) { }) } -func TestDoguInstallationUseCase_WaitForHealthyDogus(t *testing.T) { - t.Run("ok", func(t *testing.T) { - t.Parallel() +func TestDoguInstallationUseCase_CheckDogusUpToDate(t *testing.T) { + timeMay := v1.NewTime(time.Date(2024, time.May, 23, 10, 0, 0, 0, time.UTC)) + timeJune := v1.NewTime(time.Date(2024, time.June, 23, 10, 0, 0, 0, time.UTC)) + timeJuly := v1.NewTime(time.Date(2024, time.July, 23, 10, 0, 0, 0, time.UTC)) + t.Run("is up to date", func(t *testing.T) { // given doguRepoMock := newMockDoguInstallationRepository(t) - timedCtx, cancel := context.WithTimeout(testCtx, 10*time.Millisecond) - defer cancel() - doguRepoMock.EXPECT().GetAll(timedCtx).Return(map[cescommons.SimpleName]*ecosystem.DoguInstallation{}, nil) + doguRepoMock.EXPECT().GetAll(testCtx).Return(map[cescommons.SimpleName]*ecosystem.DoguInstallation{ + "postgresql": { + Name: postgresqlQualifiedName, + Version: version3211, + InstalledVersion: version3211, + StartedAt: timeJuly, + }, + "ldap": { + Name: ldapQualifiedName, + Version: version3212, + InstalledVersion: version3212, + StartedAt: timeJune, + }, + }, nil) - waitConfigMock := newMockHealthWaitConfigProvider(t) - waitConfigMock.EXPECT().GetWaitConfig(timedCtx).Return(ecosystem.WaitConfig{Interval: time.Millisecond}, nil) + globalConfigRepoMock := newMockGlobalConfigRepository(t) + globalConf := config.GlobalConfig{ + Config: config.Config{ + LastUpdated: &timeMay, + }, + } + globalConfigRepoMock.EXPECT().Get(testCtx).Return(globalConf, nil) + doguConfigRepoMock := newMockDoguConfigRepository(t) + postgresDoguConf := config.DoguConfig{ + DoguName: postgresqlQualifiedName.SimpleName, + Config: config.Config{ + LastUpdated: &timeJune, + }, + } + ldapDoguConf := config.DoguConfig{ + DoguName: ldapQualifiedName.SimpleName, + Config: config.Config{ + LastUpdated: &timeMay, + }, + } + doguConfigRepoMock.EXPECT().Get(testCtx, postgresqlQualifiedName.SimpleName).Return(postgresDoguConf, nil) + doguConfigRepoMock.EXPECT().Get(testCtx, ldapQualifiedName.SimpleName).Return(ldapDoguConf, nil) - sut := DoguInstallationUseCase{ - blueprintSpecRepo: nil, - doguRepo: doguRepoMock, - waitConfigProvider: waitConfigMock, + useCase := &DoguInstallationUseCase{ + doguRepo: doguRepoMock, + doguConfigRepo: doguConfigRepoMock, + globalConfigRepo: globalConfigRepoMock, } // when - result, err := sut.WaitForHealthyDogus(timedCtx) - + dogusNotUpToDate, err := useCase.CheckDogusUpToDate(testCtx) // then require.NoError(t, err) - assert.True(t, result.AllHealthy()) + require.Empty(t, dogusNotUpToDate) }) - - t.Run("fail to get health check interval", func(t *testing.T) { - t.Parallel() + t.Run("version is not up to date", func(t *testing.T) { // given - waitConfigMock := newMockHealthWaitConfigProvider(t) - waitConfigMock.EXPECT().GetWaitConfig(testCtx).Return(ecosystem.WaitConfig{}, assert.AnError) + doguRepoMock := newMockDoguInstallationRepository(t) + doguRepoMock.EXPECT().GetAll(testCtx).Return(map[cescommons.SimpleName]*ecosystem.DoguInstallation{ + "postgresql": { + Name: postgresqlQualifiedName, + Version: version3211, + InstalledVersion: version3212, + StartedAt: timeJuly, + }, + }, nil) - sut := DoguInstallationUseCase{ - blueprintSpecRepo: nil, - doguRepo: nil, - waitConfigProvider: waitConfigMock, + globalConfigRepoMock := newMockGlobalConfigRepository(t) + globalConf := config.GlobalConfig{ + Config: config.Config{ + LastUpdated: &timeMay, + }, } + globalConfigRepoMock.EXPECT().Get(testCtx).Return(globalConf, nil) - // when - _, err := sut.WaitForHealthyDogus(testCtx) + useCase := &DoguInstallationUseCase{ + doguRepo: doguRepoMock, + doguConfigRepo: newMockDoguConfigRepository(t), + globalConfigRepo: globalConfigRepoMock, + } + // when + dogusNotUpToDate, err := useCase.CheckDogusUpToDate(testCtx) // then - require.Error(t, err) - assert.ErrorIs(t, err, assert.AnError) - assert.ErrorContains(t, err, "failed to get health check interval") + require.NoError(t, err) + assert.Len(t, dogusNotUpToDate, 1) + assert.Equal(t, dogusNotUpToDate[0], postgresqlQualifiedName.SimpleName) }) + t.Run("global config is not up to date", func(t *testing.T) { + // given + doguRepoMock := newMockDoguInstallationRepository(t) + doguRepoMock.EXPECT().GetAll(testCtx).Return(map[cescommons.SimpleName]*ecosystem.DoguInstallation{ + "postgresql": { + Name: postgresqlQualifiedName, + Version: version3211, + InstalledVersion: version3211, + StartedAt: timeJune, + }, + }, nil) + + globalConfigRepoMock := newMockGlobalConfigRepository(t) + globalConf := config.GlobalConfig{ + Config: config.Config{ + LastUpdated: &timeJuly, + }, + } + globalConfigRepoMock.EXPECT().Get(testCtx).Return(globalConf, nil) + doguConfigRepoMock := newMockDoguConfigRepository(t) + doguConf := config.DoguConfig{ + DoguName: postgresqlQualifiedName.SimpleName, + Config: config.Config{ + LastUpdated: &timeMay, + }, + } + doguConfigRepoMock.EXPECT().Get(testCtx, postgresqlQualifiedName.SimpleName).Return(doguConf, nil) + + useCase := &DoguInstallationUseCase{ + doguRepo: doguRepoMock, + doguConfigRepo: doguConfigRepoMock, + globalConfigRepo: globalConfigRepoMock, + } - t.Run("timeout", func(t *testing.T) { - t.Parallel() + // when + dogusNotUpToDate, err := useCase.CheckDogusUpToDate(testCtx) + // then + require.NoError(t, err) + assert.Len(t, dogusNotUpToDate, 1) + assert.Equal(t, dogusNotUpToDate[0], postgresqlQualifiedName.SimpleName) + }) + t.Run("dogu config is not up to date", func(t *testing.T) { // given doguRepoMock := newMockDoguInstallationRepository(t) - timedCtx, cancel := context.WithTimeout(testCtx, 0*time.Millisecond) - defer cancel() - // return unhealthy result - doguRepoMock.EXPECT().GetAll(timedCtx).Return(map[cescommons.SimpleName]*ecosystem.DoguInstallation{ - "postgresql": {Health: ecosystem.DoguStatusInstalling}, - }, nil).Maybe() + doguRepoMock.EXPECT().GetAll(testCtx).Return(map[cescommons.SimpleName]*ecosystem.DoguInstallation{ + "postgresql": { + Name: postgresqlQualifiedName, + Version: version3211, + InstalledVersion: version3211, + StartedAt: timeJune, + }, + }, nil) - waitConfigMock := newMockHealthWaitConfigProvider(t) - waitConfigMock.EXPECT().GetWaitConfig(timedCtx).Return(ecosystem.WaitConfig{Interval: 5 * time.Millisecond}, nil) + globalConfigRepoMock := newMockGlobalConfigRepository(t) + globalConf := config.GlobalConfig{ + Config: config.Config{ + LastUpdated: &timeMay, + }, + } + globalConfigRepoMock.EXPECT().Get(testCtx).Return(globalConf, nil) + doguConfigRepoMock := newMockDoguConfigRepository(t) + doguConf := config.DoguConfig{ + DoguName: postgresqlQualifiedName.SimpleName, + Config: config.Config{ + LastUpdated: &timeJuly, + }, + } + doguConfigRepoMock.EXPECT().Get(testCtx, postgresqlQualifiedName.SimpleName).Return(doguConf, nil) - sut := DoguInstallationUseCase{ - blueprintSpecRepo: nil, - doguRepo: doguRepoMock, - waitConfigProvider: waitConfigMock, + useCase := &DoguInstallationUseCase{ + doguRepo: doguRepoMock, + doguConfigRepo: doguConfigRepoMock, + globalConfigRepo: globalConfigRepoMock, } // when - result, err := sut.WaitForHealthyDogus(timedCtx) + dogusNotUpToDate, err := useCase.CheckDogusUpToDate(testCtx) + // then + require.NoError(t, err) + assert.Len(t, dogusNotUpToDate, 1) + assert.Equal(t, dogusNotUpToDate[0], postgresqlQualifiedName.SimpleName) + }) + t.Run("multiple dogus are not up to date", func(t *testing.T) { + // given + doguRepoMock := newMockDoguInstallationRepository(t) + doguRepoMock.EXPECT().GetAll(testCtx).Return(map[cescommons.SimpleName]*ecosystem.DoguInstallation{ + "postgresql": { // version is not up to date + Name: postgresqlQualifiedName, + Version: version3211, + InstalledVersion: version3212, + StartedAt: timeJuly, + }, + "ldap": { // dogu config is not up to date + Name: ldapQualifiedName, + Version: version3211, + InstalledVersion: version3211, + StartedAt: timeJune, + }, + "cas": { // global config is not up to date + Name: casQualifiedName, + Version: version3211, + InstalledVersion: version3211, + StartedAt: timeMay, + }, + }, nil) + + globalConfigRepoMock := newMockGlobalConfigRepository(t) + globalConf := config.GlobalConfig{ + Config: config.Config{ + LastUpdated: &timeJune, + }, + } + globalConfigRepoMock.EXPECT().Get(testCtx).Return(globalConf, nil) + doguConfigRepoMock := newMockDoguConfigRepository(t) + ldapDoguConf := config.DoguConfig{ + DoguName: postgresqlQualifiedName.SimpleName, + Config: config.Config{ + LastUpdated: &timeJuly, + }, + } + casDoguConf := config.DoguConfig{ + DoguName: postgresqlQualifiedName.SimpleName, + Config: config.Config{ + LastUpdated: &timeMay, + }, + } + doguConfigRepoMock.EXPECT().Get(testCtx, ldapQualifiedName.SimpleName).Return(ldapDoguConf, nil) + doguConfigRepoMock.EXPECT().Get(testCtx, casQualifiedName.SimpleName).Return(casDoguConf, nil) + useCase := &DoguInstallationUseCase{ + doguRepo: doguRepoMock, + doguConfigRepo: doguConfigRepoMock, + globalConfigRepo: globalConfigRepoMock, + } + + // when + dogusNotUpToDate, err := useCase.CheckDogusUpToDate(testCtx) // then - assert.Error(t, err) - assert.ErrorIs(t, err, context.DeadlineExceeded) - assert.Equal(t, ecosystem.DoguHealthResult{}, result) + require.NoError(t, err) + assert.Len(t, dogusNotUpToDate, 3) + assert.Contains(t, dogusNotUpToDate, postgresqlQualifiedName.SimpleName) + assert.Contains(t, dogusNotUpToDate, ldapQualifiedName.SimpleName) + assert.Contains(t, dogusNotUpToDate, casQualifiedName.SimpleName) }) - t.Run("cannot load dogus", func(t *testing.T) { - t.Parallel() + t.Run("error on dogu GetAll error", func(t *testing.T) { // given doguRepoMock := newMockDoguInstallationRepository(t) - timedCtx, cancel := context.WithTimeout(testCtx, 10*time.Millisecond) - defer cancel() - doguRepoMock.EXPECT().GetAll(timedCtx).Return(nil, assert.AnError).Maybe() + doguRepoMock.EXPECT().GetAll(testCtx).Return(nil, assert.AnError) - waitConfigMock := newMockHealthWaitConfigProvider(t) - waitConfigMock.EXPECT().GetWaitConfig(timedCtx).Return(ecosystem.WaitConfig{Interval: time.Millisecond}, nil) + globalConfigRepoMock := newMockGlobalConfigRepository(t) + doguConfigRepoMock := newMockDoguConfigRepository(t) - sut := DoguInstallationUseCase{ - blueprintSpecRepo: nil, - doguRepo: doguRepoMock, - waitConfigProvider: waitConfigMock, + useCase := &DoguInstallationUseCase{ + doguRepo: doguRepoMock, + doguConfigRepo: doguConfigRepoMock, + globalConfigRepo: globalConfigRepoMock, } // when - result, err := sut.WaitForHealthyDogus(timedCtx) + dogusNotUpToDate, err := useCase.CheckDogusUpToDate(testCtx) + // then + require.Error(t, err) + require.Nil(t, dogusNotUpToDate) + }) + t.Run("error on global config Get error", func(t *testing.T) { + // given + doguRepoMock := newMockDoguInstallationRepository(t) + doguRepoMock.EXPECT().GetAll(testCtx).Return(nil, nil) + globalConfigRepoMock := newMockGlobalConfigRepository(t) + globalConfigRepoMock.EXPECT().Get(testCtx).Return(config.GlobalConfig{}, assert.AnError) + doguConfigRepoMock := newMockDoguConfigRepository(t) + + useCase := &DoguInstallationUseCase{ + doguRepo: doguRepoMock, + doguConfigRepo: doguConfigRepoMock, + globalConfigRepo: globalConfigRepoMock, + } + + // when + dogusNotUpToDate, err := useCase.CheckDogusUpToDate(testCtx) // then - assert.Error(t, err) - assert.ErrorIs(t, err, assert.AnError) - assert.Equal(t, ecosystem.DoguHealthResult{}, result) + require.Error(t, err) + require.Nil(t, dogusNotUpToDate) }) + t.Run("error on dogu config Get error", func(t *testing.T) { + // given + doguRepoMock := newMockDoguInstallationRepository(t) + doguRepoMock.EXPECT().GetAll(testCtx).Return(map[cescommons.SimpleName]*ecosystem.DoguInstallation{ + "postgresql": { + Name: postgresqlQualifiedName, + Version: version3211, + InstalledVersion: version3211, + StartedAt: timeJuly, + }, + }, nil) + + globalConfigRepoMock := newMockGlobalConfigRepository(t) + globalConfigRepoMock.EXPECT().Get(testCtx).Return(config.GlobalConfig{}, nil) + doguConfigRepoMock := newMockDoguConfigRepository(t) + doguConfigRepoMock.EXPECT().Get(testCtx, postgresqlQualifiedName.SimpleName).Return(config.DoguConfig{}, assert.AnError) + + useCase := &DoguInstallationUseCase{ + doguRepo: doguRepoMock, + doguConfigRepo: doguConfigRepoMock, + globalConfigRepo: globalConfigRepoMock, + } + // when + dogusNotUpToDate, err := useCase.CheckDogusUpToDate(testCtx) + // then + require.Error(t, err) + require.Nil(t, dogusNotUpToDate) + }) } diff --git a/pkg/application/doguRestartUseCase.go b/pkg/application/doguRestartUseCase.go deleted file mode 100644 index 0f8a9e4b..00000000 --- a/pkg/application/doguRestartUseCase.go +++ /dev/null @@ -1,69 +0,0 @@ -package application - -import ( - "context" - "fmt" - cescommons "github.com/cloudogu/ces-commons-lib/dogu" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domainservice" - "sigs.k8s.io/controller-runtime/pkg/log" -) - -type DoguRestartUseCase struct { - doguInstallationRepository doguInstallationRepository - blueprintSpecRepo blueprintSpecRepository - restartRepository doguRestartRepository -} - -func NewDoguRestartUseCase(doguInstallationRepository doguInstallationRepository, blueprintSpecRepo blueprintSpecRepository, restartRepository doguRestartRepository) *DoguRestartUseCase { - return &DoguRestartUseCase{doguInstallationRepository: doguInstallationRepository, blueprintSpecRepo: blueprintSpecRepo, restartRepository: restartRepository} -} - -func (useCase *DoguRestartUseCase) TriggerDoguRestarts(ctx context.Context, blueprintId string) error { - logger := log.FromContext(ctx).WithName("DoguRestartUseCase.TriggerDoguRestarts") - blueprintSpec, err := useCase.blueprintSpecRepo.GetById(ctx, blueprintId) - if err != nil { - return fmt.Errorf("could not get blueprint spec by id: %q", err) - } - - logger.Info("searching for Dogus that need a restart...") - var dogusThatNeedARestart []cescommons.SimpleName - - if blueprintSpec.StateDiff.GlobalConfigDiffs.HasChanges() { - logger.Info("restarting all installed Dogus...") - err = useCase.restartAllInstalledDogus(ctx) - if err != nil { - return domainservice.NewInternalError(err, "could not restart all installed Dogus") - } - } else { - dogusThatNeedARestart = blueprintSpec.GetDogusThatNeedARestart() - if len(dogusThatNeedARestart) > 0 { - logger.Info("restarting Dogus...") - restartError := useCase.restartRepository.RestartAll(ctx, dogusThatNeedARestart) - if restartError != nil { - return domainservice.NewInternalError(err, "could not restart Dogus") - } - } else { - logger.Info("no Dogu restarts necessary") - } - } - - blueprintSpec.Status = domain.StatusPhaseRestartsTriggered - err = useCase.blueprintSpecRepo.Update(ctx, blueprintSpec) - if err != nil { - return domainservice.NewInternalError(err, "could not update blueprint spec") - } - return nil -} - -func (useCase *DoguRestartUseCase) restartAllInstalledDogus(ctx context.Context) error { - installedDogus, getInstalledDogusError := useCase.doguInstallationRepository.GetAll(ctx) - if getInstalledDogusError != nil { - return domainservice.NewInternalError(getInstalledDogusError, "could not get all installed Dogus") - } - var installedDogusSimpleNames []cescommons.SimpleName - for _, installation := range installedDogus { - installedDogusSimpleNames = append(installedDogusSimpleNames, installation.Name.SimpleName) - } - return useCase.restartRepository.RestartAll(ctx, installedDogusSimpleNames) -} diff --git a/pkg/application/doguRestartUseCase_test.go b/pkg/application/doguRestartUseCase_test.go deleted file mode 100644 index 4213afe6..00000000 --- a/pkg/application/doguRestartUseCase_test.go +++ /dev/null @@ -1,398 +0,0 @@ -package application - -import ( - "context" - cescommons "github.com/cloudogu/ces-commons-lib/dogu" - "github.com/cloudogu/cesapp-lib/core" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/common" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "testing" -) - -var ( - testDoguSimpleName = cescommons.SimpleName("testDogu1") -) - -func TestDoguRestartUseCase_TriggerDoguRestarts(t *testing.T) { - t.Run("no dogu restarts triggered on blueprint with empty state diff", func(t *testing.T) { - // given - testContext := context.Background() - testStateDiff := domain.StateDiff{ - DoguDiffs: domain.DoguDiffs{}, - ComponentDiffs: domain.ComponentDiffs{}, - DoguConfigDiffs: map[cescommons.SimpleName]domain.DoguConfigDiffs{}, - SensitiveDoguConfigDiffs: map[cescommons.SimpleName]domain.SensitiveDoguConfigDiffs{}, - GlobalConfigDiffs: domain.GlobalConfigDiffs{}, - } - testBlueprint := domain.BlueprintSpec{ - Id: testBlueprintId, - Blueprint: domain.Blueprint{}, - BlueprintMask: domain.BlueprintMask{}, - EffectiveBlueprint: domain.EffectiveBlueprint{}, - StateDiff: testStateDiff, - Config: domain.BlueprintConfiguration{}, - Status: "", - PersistenceContext: nil, - Events: nil, - } - installationRepository := newMockDoguInstallationRepository(t) - blueprintSpecRepo := newMockBlueprintSpecRepository(t) - restartRepository := newMockDoguRestartRepository(t) - blueprintSpecRepo.EXPECT().GetById(testContext, testBlueprintId).Return(&testBlueprint, nil) - blueprintSpecRepo.EXPECT().Update(testContext, &testBlueprint).Return(nil) - - restartUseCase := NewDoguRestartUseCase(installationRepository, blueprintSpecRepo, restartRepository) - - // when - err := restartUseCase.TriggerDoguRestarts(testContext, testBlueprintId) - - // then - require.NoError(t, err) - }) - - t.Run("dogu restarts triggered on blueprint with non-empty state diff", func(t *testing.T) { - // given - testContext := context.Background() - testStateDiff := domain.StateDiff{ - DoguDiffs: domain.DoguDiffs{}, - ComponentDiffs: domain.ComponentDiffs{}, - DoguConfigDiffs: map[cescommons.SimpleName]domain.DoguConfigDiffs{}, - SensitiveDoguConfigDiffs: map[cescommons.SimpleName]domain.SensitiveDoguConfigDiffs{}, - GlobalConfigDiffs: domain.GlobalConfigDiffs{{ - Key: "testkey", - Actual: domain.GlobalConfigValueState{Value: "changed", Exists: true}, - Expected: domain.GlobalConfigValueState{Value: "initial", Exists: true}, - NeededAction: domain.ConfigActionSet, - }}, - } - testBlueprint := domain.BlueprintSpec{ - Id: testBlueprintId, - Blueprint: domain.Blueprint{}, - BlueprintMask: domain.BlueprintMask{}, - EffectiveBlueprint: domain.EffectiveBlueprint{}, - StateDiff: testStateDiff, - Config: domain.BlueprintConfiguration{}, - Status: "", - PersistenceContext: nil, - Events: nil, - } - installedDogu := ecosystem.DoguInstallation{ - Name: cescommons.QualifiedName{Namespace: "testing", SimpleName: testDoguSimpleName}, - Version: core.Version{Raw: "1.0.0-1", Major: 1, Extra: 1}, - Status: "installed", - Health: ecosystem.AvailableHealthStatus, - UpgradeConfig: ecosystem.UpgradeConfig{AllowNamespaceSwitch: false}, - PersistenceContext: nil, - } - installedDogus := map[cescommons.SimpleName]*ecosystem.DoguInstallation{ - testDoguSimpleName: &installedDogu, - } - dogusThatNeedARestart := []cescommons.SimpleName{testDoguSimpleName} - installationRepository := newMockDoguInstallationRepository(t) - blueprintSpecRepo := newMockBlueprintSpecRepository(t) - restartRepository := newMockDoguRestartRepository(t) - blueprintSpecRepo.EXPECT().GetById(testContext, testBlueprintId).Return(&testBlueprint, nil) - installationRepository.EXPECT().GetAll(testContext).Return(installedDogus, nil) - restartRepository.EXPECT().RestartAll(testContext, dogusThatNeedARestart).Return(nil) - blueprintSpecRepo.EXPECT().Update(testContext, &testBlueprint).Return(nil) - - restartUseCase := NewDoguRestartUseCase(installationRepository, blueprintSpecRepo, restartRepository) - - // when - err := restartUseCase.TriggerDoguRestarts(testContext, testBlueprintId) - - // then - require.NoError(t, err) - }) - - t.Run("fail on get all dogus from repository error", func(t *testing.T) { - // given - testContext := context.Background() - testStateDiff := domain.StateDiff{ - DoguDiffs: domain.DoguDiffs{}, - ComponentDiffs: domain.ComponentDiffs{}, - DoguConfigDiffs: map[cescommons.SimpleName]domain.DoguConfigDiffs{}, - SensitiveDoguConfigDiffs: map[cescommons.SimpleName]domain.SensitiveDoguConfigDiffs{}, - GlobalConfigDiffs: domain.GlobalConfigDiffs{{ - Key: "testkey", - Actual: domain.GlobalConfigValueState{Value: "changed", Exists: true}, - Expected: domain.GlobalConfigValueState{Value: "initial", Exists: true}, - NeededAction: domain.ConfigActionSet, - }}, - } - testBlueprint := domain.BlueprintSpec{ - Id: testBlueprintId, - Blueprint: domain.Blueprint{}, - BlueprintMask: domain.BlueprintMask{}, - EffectiveBlueprint: domain.EffectiveBlueprint{}, - StateDiff: testStateDiff, - Config: domain.BlueprintConfiguration{}, - Status: "", - PersistenceContext: nil, - Events: nil, - } - installationRepository := newMockDoguInstallationRepository(t) - blueprintSpecRepo := newMockBlueprintSpecRepository(t) - restartRepository := newMockDoguRestartRepository(t) - blueprintSpecRepo.EXPECT().GetById(testContext, testBlueprintId).Return(&testBlueprint, nil) - installationRepository.EXPECT().GetAll(testContext).Return(map[cescommons.SimpleName]*ecosystem.DoguInstallation{}, assert.AnError) - - restartUseCase := NewDoguRestartUseCase(installationRepository, blueprintSpecRepo, restartRepository) - - // when - err := restartUseCase.TriggerDoguRestarts(testContext, testBlueprintId) - - // then - require.Error(t, err) - assert.ErrorContains(t, err, "could not restart all installed Dogus: could not get all installed Dogus:") - }) - - t.Run("fail on repository restart all error", func(t *testing.T) { - // given - testContext := context.Background() - testStateDiff := domain.StateDiff{ - DoguDiffs: domain.DoguDiffs{}, - ComponentDiffs: domain.ComponentDiffs{}, - DoguConfigDiffs: map[cescommons.SimpleName]domain.DoguConfigDiffs{}, - SensitiveDoguConfigDiffs: map[cescommons.SimpleName]domain.SensitiveDoguConfigDiffs{}, - GlobalConfigDiffs: domain.GlobalConfigDiffs{{ - Key: "testkey", - Actual: domain.GlobalConfigValueState{Value: "changed", Exists: true}, - Expected: domain.GlobalConfigValueState{Value: "initial", Exists: true}, - NeededAction: domain.ConfigActionSet, - }}, - } - testBlueprint := domain.BlueprintSpec{ - Id: testBlueprintId, - Blueprint: domain.Blueprint{}, - BlueprintMask: domain.BlueprintMask{}, - EffectiveBlueprint: domain.EffectiveBlueprint{}, - StateDiff: testStateDiff, - Config: domain.BlueprintConfiguration{}, - Status: "", - PersistenceContext: nil, - Events: nil, - } - installedDogu := ecosystem.DoguInstallation{ - Name: cescommons.QualifiedName{Namespace: "testing", SimpleName: testDoguSimpleName}, - Version: core.Version{Raw: "1.0.0-1", Major: 1, Extra: 1}, - Status: "installed", - Health: ecosystem.AvailableHealthStatus, - UpgradeConfig: ecosystem.UpgradeConfig{AllowNamespaceSwitch: false}, - PersistenceContext: nil, - } - installedDogus := map[cescommons.SimpleName]*ecosystem.DoguInstallation{ - testDoguSimpleName: &installedDogu, - } - dogusThatNeedARestart := []cescommons.SimpleName{testDoguSimpleName} - installationRepository := newMockDoguInstallationRepository(t) - blueprintSpecRepo := newMockBlueprintSpecRepository(t) - restartRepository := newMockDoguRestartRepository(t) - blueprintSpecRepo.EXPECT().GetById(testContext, testBlueprintId).Return(&testBlueprint, nil) - installationRepository.EXPECT().GetAll(testContext).Return(installedDogus, nil) - restartRepository.EXPECT().RestartAll(testContext, dogusThatNeedARestart).Return(assert.AnError) - restartUseCase := NewDoguRestartUseCase(installationRepository, blueprintSpecRepo, restartRepository) - - // when - err := restartUseCase.TriggerDoguRestarts(testContext, testBlueprintId) - - // then - require.Error(t, err) - assert.ErrorContains(t, err, "could not restart all installed Dogus") - }) - - t.Run("restart some dogus", func(t *testing.T) { - // given - - testContext := context.Background() - testStateDiff := domain.StateDiff{ - DoguDiffs: domain.DoguDiffs{}, - ComponentDiffs: domain.ComponentDiffs{}, - DoguConfigDiffs: map[cescommons.SimpleName]domain.DoguConfigDiffs{ - testDoguSimpleName: {{ - Key: common.DoguConfigKey{DoguName: testDoguSimpleName, Key: "testKey"}, - Actual: domain.DoguConfigValueState{Value: "changed", Exists: true}, - Expected: domain.DoguConfigValueState{Value: "initial", Exists: true}, - NeededAction: domain.ConfigActionSet}}, - }, - SensitiveDoguConfigDiffs: map[cescommons.SimpleName]domain.SensitiveDoguConfigDiffs{}, - GlobalConfigDiffs: domain.GlobalConfigDiffs{}, - } - testDogu := domain.Dogu{ - Name: cescommons.QualifiedName{SimpleName: testDoguSimpleName, Namespace: "testing"}, - Version: core.Version{Raw: "1.0.0-1", Major: 1, Extra: 1}, - TargetState: 0, - } - testBlueprint := domain.BlueprintSpec{ - Id: testBlueprintId, - Blueprint: domain.Blueprint{}, - BlueprintMask: domain.BlueprintMask{}, - EffectiveBlueprint: domain.EffectiveBlueprint{Dogus: []domain.Dogu{testDogu}}, - StateDiff: testStateDiff, - Config: domain.BlueprintConfiguration{}, - Status: "", - PersistenceContext: nil, - Events: nil, - } - dogusThatNeedARestart := []cescommons.SimpleName{testDoguSimpleName} - installationRepository := newMockDoguInstallationRepository(t) - blueprintSpecRepo := newMockBlueprintSpecRepository(t) - restartRepository := newMockDoguRestartRepository(t) - blueprintSpecRepo.EXPECT().GetById(testContext, testBlueprintId).Return(&testBlueprint, nil) - restartRepository.EXPECT().RestartAll(testContext, dogusThatNeedARestart).Return(nil) - restartUseCase := NewDoguRestartUseCase(installationRepository, blueprintSpecRepo, restartRepository) - blueprintSpecRepo.EXPECT().Update(testContext, &testBlueprint).Return(nil) - - // when - err := restartUseCase.TriggerDoguRestarts(testContext, testBlueprintId) - - // then - require.NoError(t, err) - }) - - t.Run("fail on dogu restart for some dogus", func(t *testing.T) { - // given - testContext := context.Background() - testStateDiff := domain.StateDiff{ - DoguDiffs: domain.DoguDiffs{}, - ComponentDiffs: domain.ComponentDiffs{}, - DoguConfigDiffs: map[cescommons.SimpleName]domain.DoguConfigDiffs{ - testDoguSimpleName: {{ - Key: common.DoguConfigKey{DoguName: testDoguSimpleName, Key: "testKey"}, - Actual: domain.DoguConfigValueState{Value: "changed", Exists: true}, - Expected: domain.DoguConfigValueState{Value: "initial", Exists: true}, - NeededAction: domain.ConfigActionSet}}, - }, - SensitiveDoguConfigDiffs: map[cescommons.SimpleName]domain.SensitiveDoguConfigDiffs{}, - GlobalConfigDiffs: domain.GlobalConfigDiffs{}, - } - testDogu := domain.Dogu{ - Name: cescommons.QualifiedName{SimpleName: testDoguSimpleName, Namespace: "testing"}, - Version: core.Version{Raw: "1.0.0-1", Major: 1, Extra: 1}, - TargetState: 0, - } - testBlueprint := domain.BlueprintSpec{ - Id: testBlueprintId, - Blueprint: domain.Blueprint{}, - BlueprintMask: domain.BlueprintMask{}, - EffectiveBlueprint: domain.EffectiveBlueprint{Dogus: []domain.Dogu{testDogu}}, - StateDiff: testStateDiff, - Config: domain.BlueprintConfiguration{}, - Status: "", - PersistenceContext: nil, - Events: nil, - } - dogusThatNeedARestart := []cescommons.SimpleName{testDoguSimpleName} - installationRepository := newMockDoguInstallationRepository(t) - blueprintSpecRepo := newMockBlueprintSpecRepository(t) - restartRepository := newMockDoguRestartRepository(t) - blueprintSpecRepo.EXPECT().GetById(testContext, testBlueprintId).Return(&testBlueprint, nil) - restartRepository.EXPECT().RestartAll(testContext, dogusThatNeedARestart).Return(assert.AnError) - restartUseCase := NewDoguRestartUseCase(installationRepository, blueprintSpecRepo, restartRepository) - - // when - err := restartUseCase.TriggerDoguRestarts(testContext, testBlueprintId) - - // then - require.Error(t, err) - assert.ErrorContains(t, err, "could not restart Dogus") - }) - - t.Run("fail on error in blueprint spec update", func(t *testing.T) { - // given - testContext := context.Background() - testStateDiff := domain.StateDiff{ - DoguDiffs: domain.DoguDiffs{}, - ComponentDiffs: domain.ComponentDiffs{}, - DoguConfigDiffs: map[cescommons.SimpleName]domain.DoguConfigDiffs{ - testDoguSimpleName: {{ - Key: common.DoguConfigKey{DoguName: testDoguSimpleName, Key: "testKey"}, - Actual: domain.DoguConfigValueState{Value: "changed", Exists: true}, - Expected: domain.DoguConfigValueState{Value: "initial", Exists: true}, - NeededAction: domain.ConfigActionSet}}, - }, - SensitiveDoguConfigDiffs: map[cescommons.SimpleName]domain.SensitiveDoguConfigDiffs{}, - GlobalConfigDiffs: domain.GlobalConfigDiffs{}, - } - testDogu := domain.Dogu{ - Name: cescommons.QualifiedName{SimpleName: testDoguSimpleName, Namespace: "testing"}, - Version: core.Version{Raw: "1.0.0-1", Major: 1, Extra: 1}, - TargetState: 0, - } - testBlueprint := domain.BlueprintSpec{ - Id: testBlueprintId, - Blueprint: domain.Blueprint{}, - BlueprintMask: domain.BlueprintMask{}, - EffectiveBlueprint: domain.EffectiveBlueprint{Dogus: []domain.Dogu{testDogu}}, - StateDiff: testStateDiff, - Config: domain.BlueprintConfiguration{}, - Status: "", - PersistenceContext: nil, - Events: nil, - } - dogusThatNeedARestart := []cescommons.SimpleName{testDoguSimpleName} - installationRepository := newMockDoguInstallationRepository(t) - blueprintSpecRepo := newMockBlueprintSpecRepository(t) - restartRepository := newMockDoguRestartRepository(t) - blueprintSpecRepo.EXPECT().GetById(testContext, testBlueprintId).Return(&testBlueprint, nil) - restartRepository.EXPECT().RestartAll(testContext, dogusThatNeedARestart).Return(nil) - restartUseCase := NewDoguRestartUseCase(installationRepository, blueprintSpecRepo, restartRepository) - blueprintSpecRepo.EXPECT().Update(testContext, &testBlueprint).Return(assert.AnError) - - // when - err := restartUseCase.TriggerDoguRestarts(testContext, testBlueprintId) - - // then - require.Error(t, err) - assert.ErrorContains(t, err, "could not update blueprint spec") - }) - - t.Run("fail on error when getting blueprint", func(t *testing.T) { - // given - testContext := context.Background() - testStateDiff := domain.StateDiff{ - DoguDiffs: domain.DoguDiffs{}, - ComponentDiffs: domain.ComponentDiffs{}, - DoguConfigDiffs: map[cescommons.SimpleName]domain.DoguConfigDiffs{ - testDoguSimpleName: {{ - Key: common.DoguConfigKey{DoguName: testDoguSimpleName, Key: "testKey"}, - Actual: domain.DoguConfigValueState{Value: "changed", Exists: true}, - Expected: domain.DoguConfigValueState{Value: "initial", Exists: true}, - NeededAction: domain.ConfigActionSet}}, - }, - SensitiveDoguConfigDiffs: map[cescommons.SimpleName]domain.SensitiveDoguConfigDiffs{}, - GlobalConfigDiffs: domain.GlobalConfigDiffs{}, - } - testDogu := domain.Dogu{ - Name: cescommons.QualifiedName{SimpleName: testDoguSimpleName, Namespace: "testing"}, - Version: core.Version{Raw: "1.0.0-1", Major: 1, Extra: 1}, - TargetState: 0, - } - testBlueprint := domain.BlueprintSpec{ - Id: testBlueprintId, - Blueprint: domain.Blueprint{}, - BlueprintMask: domain.BlueprintMask{}, - EffectiveBlueprint: domain.EffectiveBlueprint{Dogus: []domain.Dogu{testDogu}}, - StateDiff: testStateDiff, - Config: domain.BlueprintConfiguration{}, - Status: "", - PersistenceContext: nil, - Events: nil, - } - installationRepository := newMockDoguInstallationRepository(t) - blueprintSpecRepo := newMockBlueprintSpecRepository(t) - restartRepository := newMockDoguRestartRepository(t) - blueprintSpecRepo.EXPECT().GetById(testContext, testBlueprintId).Return(&testBlueprint, assert.AnError) - restartUseCase := NewDoguRestartUseCase(installationRepository, blueprintSpecRepo, restartRepository) - - // when - err := restartUseCase.TriggerDoguRestarts(testContext, testBlueprintId) - - // then - require.Error(t, err) - assert.ErrorContains(t, err, "could not get blueprint spec by id") - }) -} diff --git a/pkg/application/dogusUpToDateUseCase.go b/pkg/application/dogusUpToDateUseCase.go new file mode 100644 index 00000000..2d41a6bf --- /dev/null +++ b/pkg/application/dogusUpToDateUseCase.go @@ -0,0 +1,51 @@ +package application + +import ( + "context" + "errors" + "fmt" + + "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +// DogusUpToDateUseCase checks if all dogus are up to date, meaning they are on the desired version and configuration. +type DogusUpToDateUseCase struct { + repo blueprintSpecRepository + doguInstallUseCase doguInstallationUseCase +} + +func NewDogusUpToDateUseCase( + repo blueprintSpecRepository, + doguInstallUseCase doguInstallationUseCase, +) *DogusUpToDateUseCase { + return &DogusUpToDateUseCase{ + repo: repo, + doguInstallUseCase: doguInstallUseCase, + } +} + +// CheckDogus checks that all dogs are up to date. +// returns domain.DogusNotUpToDateError if the dogu config or installed version are not up to date yet or +// returns domainservice.ConflictError if there was a concurrent update to the blueprint or +// returns a domainservice.InternalError if there was an unspecified error while collecting or modifying the ecosystem state. +func (useCase *DogusUpToDateUseCase) CheckDogus(ctx context.Context, blueprint *domain.BlueprintSpec) error { + logger := log.FromContext(ctx).WithName("DogusUpToDateUseCase.CheckDogus") + + dogusNotUpToDate, err := useCase.doguInstallUseCase.CheckDogusUpToDate(ctx) + if err != nil { + return err + } + if len(dogusNotUpToDate) > 0 { + // event and error + blueprint.Events = append(blueprint.Events, domain.DogusNotUpToDateEvent{DogusNotUpToDate: dogusNotUpToDate}) + updateErr := useCase.repo.Update(ctx, blueprint) + if updateErr != nil { + return fmt.Errorf("cannot update status while checking dogus: %w", errors.Join(updateErr, err)) + } + return &domain.DogusNotUpToDateError{Message: fmt.Sprintf("following dogus are not up to date yet: %v", dogusNotUpToDate)} + } + + logger.V(2).Info("all dogus are up to date") + return nil +} diff --git a/pkg/application/dogusUpToDateUseCase_test.go b/pkg/application/dogusUpToDateUseCase_test.go new file mode 100644 index 00000000..8769772d --- /dev/null +++ b/pkg/application/dogusUpToDateUseCase_test.go @@ -0,0 +1,109 @@ +package application + +import ( + "testing" + + cescommons "github.com/cloudogu/ces-commons-lib/dogu" + "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDogusUpToDateUseCase_CheckDogus(t *testing.T) { + t.Run("all dogus up to date", func(t *testing.T) { + blueprint := &domain.BlueprintSpec{ + Conditions: []domain.Condition{}, + } + + repoMock := newMockBlueprintSpecRepository(t) + doguInstallUseCaseMock := newMockDoguInstallationUseCase(t) + dogusNotUpToDate := []cescommons.SimpleName{} + doguInstallUseCaseMock.EXPECT().CheckDogusUpToDate(testCtx).Return(dogusNotUpToDate, nil) + useCase := NewDogusUpToDateUseCase(repoMock, doguInstallUseCaseMock) + + err := useCase.CheckDogus(testCtx, blueprint) + + require.NoError(t, err) + assert.Empty(t, blueprint.Events) + }) + t.Run("multiple dogus not up to date", func(t *testing.T) { + blueprint := &domain.BlueprintSpec{ + Conditions: []domain.Condition{}, + } + + repoMock := newMockBlueprintSpecRepository(t) + repoMock.EXPECT().Update(testCtx, blueprint).Return(nil) + doguInstallUseCaseMock := newMockDoguInstallationUseCase(t) + dogusNotUpToDate := []cescommons.SimpleName{ldap, postfix} + doguInstallUseCaseMock.EXPECT().CheckDogusUpToDate(testCtx).Return(dogusNotUpToDate, nil) + useCase := NewDogusUpToDateUseCase(repoMock, doguInstallUseCaseMock) + + err := useCase.CheckDogus(testCtx, blueprint) + + require.Error(t, err) + var expectedErrorType *domain.DogusNotUpToDateError + assert.ErrorAs(t, err, &expectedErrorType) + assert.ErrorContains(t, err, "following dogus are not up to date yet:") + assert.ErrorContains(t, err, ldap.String()) + assert.ErrorContains(t, err, postfix.String()) + require.Equal(t, 1, len(blueprint.Events)) + assert.Equal(t, domain.DogusNotUpToDateEvent{DogusNotUpToDate: dogusNotUpToDate}, blueprint.Events[0]) + require.Empty(t, blueprint.Conditions) + }) + + t.Run("no update without not up to date dogus", func(t *testing.T) { + blueprint := &domain.BlueprintSpec{ + Conditions: []domain.Condition{}, + } + + repoMock := newMockBlueprintSpecRepository(t) + doguInstallUseCaseMock := newMockDoguInstallationUseCase(t) + doguInstallUseCaseMock.EXPECT().CheckDogusUpToDate(testCtx).Return([]cescommons.SimpleName{}, nil) + useCase := NewDogusUpToDateUseCase(repoMock, doguInstallUseCaseMock) + + err := useCase.CheckDogus(testCtx, blueprint) + require.NoError(t, err) + require.Empty(t, blueprint.Events) + require.Empty(t, blueprint.Conditions) + }) + + t.Run("fail to check dogus", func(t *testing.T) { + blueprint := &domain.BlueprintSpec{ + Conditions: []domain.Condition{}, + } + + repoMock := newMockBlueprintSpecRepository(t) + doguInstallUseCaseMock := newMockDoguInstallationUseCase(t) + doguInstallUseCaseMock.EXPECT().CheckDogusUpToDate(testCtx).Return(nil, assert.AnError) + useCase := NewDogusUpToDateUseCase(repoMock, doguInstallUseCaseMock) + + err := useCase.CheckDogus(testCtx, blueprint) + + require.ErrorIs(t, err, assert.AnError) + require.Equal(t, 0, len(blueprint.Events)) + }) + + t.Run("fail to update blueprint", func(t *testing.T) { + blueprint := &domain.BlueprintSpec{ + Conditions: []domain.Condition{}, + StateDiff: domain.StateDiff{ + DoguDiffs: domain.DoguDiffs{ + {NeededActions: []domain.Action{domain.ActionInstall}}, + }, + }, + } + + repoMock := newMockBlueprintSpecRepository(t) + repoMock.EXPECT().Update(testCtx, blueprint).Return(assert.AnError) + doguInstallUseCaseMock := newMockDoguInstallationUseCase(t) + dogusNotUpToDate := []cescommons.SimpleName{"ldap"} + doguInstallUseCaseMock.EXPECT().CheckDogusUpToDate(testCtx).Return(dogusNotUpToDate, nil) + useCase := NewDogusUpToDateUseCase(repoMock, doguInstallUseCaseMock) + + err := useCase.CheckDogus(testCtx, blueprint) + + require.ErrorIs(t, err, assert.AnError) + require.Equal(t, 1, len(blueprint.Events)) + assert.Equal(t, domain.DogusNotUpToDateEvent{DogusNotUpToDate: dogusNotUpToDate}, blueprint.Events[0]) + }) +} diff --git a/pkg/application/ecosystemConfigUseCase.go b/pkg/application/ecosystemConfigUseCase.go index ceaf0e2b..d1eb9ee8 100644 --- a/pkg/application/ecosystemConfigUseCase.go +++ b/pkg/application/ecosystemConfigUseCase.go @@ -4,13 +4,16 @@ import ( "context" "errors" "fmt" + "maps" + "slices" + cescommons "github.com/cloudogu/ces-commons-lib/dogu" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/common" + "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" + "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domainservice" "github.com/cloudogu/k8s-registry-lib/config" - "maps" "sigs.k8s.io/controller-runtime/pkg/log" - "slices" ) type EcosystemConfigUseCase struct { @@ -18,74 +21,82 @@ type EcosystemConfigUseCase struct { doguConfigRepository doguConfigRepository sensitiveDoguConfigRepository sensitiveDoguConfigRepository globalConfigRepository globalConfigRepository + doguInstallationRepository doguInstallationRepository } -func NewEcosystemConfigUseCase( - blueprintRepository blueprintSpecRepository, - doguConfigRepository doguConfigRepository, - sensitiveDoguConfigRepository sensitiveDoguConfigRepository, - globalConfigRepository globalConfigRepository, -) *EcosystemConfigUseCase { +func NewEcosystemConfigUseCase(blueprintRepository blueprintSpecRepository, doguConfigRepository doguConfigRepository, sensitiveDoguConfigRepository sensitiveDoguConfigRepository, globalConfigRepository globalConfigRepository, doguInstallationRepository domainservice.DoguInstallationRepository) *EcosystemConfigUseCase { return &EcosystemConfigUseCase{ blueprintRepository: blueprintRepository, doguConfigRepository: doguConfigRepository, sensitiveDoguConfigRepository: sensitiveDoguConfigRepository, globalConfigRepository: globalConfigRepository, + doguInstallationRepository: doguInstallationRepository, } } // ApplyConfig fetches the dogu and global config stateDiff of the blueprint and applies these keys to the repositories. -func (useCase *EcosystemConfigUseCase) ApplyConfig(ctx context.Context, blueprintId string) error { - logger := log.FromContext(ctx).WithName("EcosystemConfigUseCase.ApplyConfig"). - WithValues("blueprintId", blueprintId) +func (useCase *EcosystemConfigUseCase) ApplyConfig(ctx context.Context, blueprint *domain.BlueprintSpec) error { + logger := log.FromContext(ctx).WithName("EcosystemConfigUseCase.ApplyConfig") - blueprintSpec, err := useCase.blueprintRepository.GetById(ctx, blueprintId) + err := useCase.pauseReconciliationForDogus(ctx, blueprint.StateDiff) if err != nil { - return fmt.Errorf("cannot load blueprint to apply config: %w", err) + return useCase.handleFailedApplyEcosystemConfig(ctx, blueprint, fmt.Errorf("could not pause reconciliation for some dogus: %w", err)) } - - doguConfigDiffs := blueprintSpec.StateDiff.DoguConfigDiffs - isEmptyDoguDiff := len(doguConfigDiffs) == 0 - if isEmptyDoguDiff { - logger.Info("dogu config diffs are empty...") + err = applyDoguConfigDiffs(ctx, useCase.doguConfigRepository, blueprint.StateDiff.DoguConfigDiffs) + if err != nil { + return useCase.handleFailedApplyEcosystemConfig(ctx, blueprint, fmt.Errorf("could not apply normal dogu config: %w", err)) } - - sensitiveDoguConfigDiffs := blueprintSpec.StateDiff.SensitiveDoguConfigDiffs - isEmptySensitiveDiff := len(sensitiveDoguConfigDiffs) == 0 - if isEmptySensitiveDiff { - logger.Info("sensitive dogu config diffs are empty...") + err = applyDoguConfigDiffs(ctx, useCase.sensitiveDoguConfigRepository, blueprint.StateDiff.SensitiveDoguConfigDiffs) + if err != nil { + return useCase.handleFailedApplyEcosystemConfig(ctx, blueprint, fmt.Errorf("could not apply sensitive dogu config: %w", err)) } - - globalConfigDiffs := blueprintSpec.StateDiff.GlobalConfigDiffs - isEmptyGlobalDiff := len(globalConfigDiffs) == 0 - if isEmptyGlobalDiff { - logger.Info("global config diffs are empty...") + err = useCase.applyGlobalConfigDiffs(ctx, blueprint.StateDiff.GlobalConfigDiffs.GetGlobalConfigDiffsByAction()) + if err != nil { + return useCase.handleFailedApplyEcosystemConfig(ctx, blueprint, fmt.Errorf("could not apply global config: %w", err)) } - if isEmptyDoguDiff && isEmptyGlobalDiff && isEmptySensitiveDiff { - return useCase.markConfigApplied(ctx, blueprintSpec) - } + if blueprint.StateDiff.HasConfigChanges() { + blueprint.MarkEcosystemConfigApplied() + repoErr := useCase.blueprintRepository.Update(ctx, blueprint) - err = useCase.markApplyConfigStart(ctx, blueprintSpec) - if err != nil { - return useCase.handleFailedApplyEcosystemConfig(ctx, blueprintSpec, err) + if repoErr != nil { + repoErr = errors.Join(repoErr, err) + logger.Error(repoErr, "cannot update blueprint events") + return fmt.Errorf("cannot update blueprint events: %w", repoErr) + } } + return nil +} - // do not apply further configs if error happens, we don't want to corrupt the system more than needed. - err = applyDoguConfigDiffs(ctx, useCase.doguConfigRepository, blueprintSpec.StateDiff.DoguConfigDiffs) - if err != nil { - return useCase.handleFailedApplyEcosystemConfig(ctx, blueprintSpec, fmt.Errorf("could not apply normal dogu config: %w", err)) - } - err = applyDoguConfigDiffs(ctx, useCase.sensitiveDoguConfigRepository, blueprintSpec.StateDiff.SensitiveDoguConfigDiffs) - if err != nil { - return useCase.handleFailedApplyEcosystemConfig(ctx, blueprintSpec, fmt.Errorf("could not apply sensitive dogu config: %w", err)) - } - err = useCase.applyGlobalConfigDiffs(ctx, globalConfigDiffs.GetGlobalConfigDiffsByAction()) +func (useCase *EcosystemConfigUseCase) pauseReconciliationForDogus(ctx context.Context, diff domain.StateDiff) error { + allDogus, err := useCase.doguInstallationRepository.GetAll(ctx) if err != nil { - return useCase.handleFailedApplyEcosystemConfig(ctx, blueprintSpec, fmt.Errorf("could not apply global config: %w", err)) + return fmt.Errorf("error while attempting to load dogus: %w", err) + } + globalConfigChanges := diff.GlobalConfigDiffs.HasChanges() + for _, dogu := range allDogus { + doguDiff := findDoguDiff(diff, dogu) + if slices.Contains(doguDiff.NeededActions, domain.ActionUpgrade) && + (globalConfigChanges || + diff.DoguConfigDiffs[dogu.Name.SimpleName].HasChanges() || + diff.SensitiveDoguConfigDiffs[dogu.Name.SimpleName].HasChanges()) { + dogu.SetReconciliationPaused(true) + err = useCase.doguInstallationRepository.Update(ctx, dogu) + if err != nil { + return fmt.Errorf("could not pause reconciliation for dogu: %w", err) + } + } } + return nil +} - return useCase.markConfigApplied(ctx, blueprintSpec) +func findDoguDiff(diff domain.StateDiff, dogu *ecosystem.DoguInstallation) domain.DoguDiff { + for _, doguDiff := range diff.DoguDiffs { + if doguDiff.DoguName == dogu.Name.SimpleName { + return doguDiff + } + } + return domain.DoguDiff{} } func (useCase *EcosystemConfigUseCase) applyGlobalConfigDiffs(ctx context.Context, globalConfigDiffsByAction map[domain.ConfigAction][]domain.GlobalConfigEntryDiff) error { @@ -100,7 +111,11 @@ func (useCase *EcosystemConfigUseCase) applyGlobalConfigDiffs(ctx context.Contex entryDiffsToSet := globalConfigDiffsByAction[domain.ConfigActionSet] for _, diff := range entryDiffsToSet { var err error - updatedEntries, err = updatedEntries.Set(diff.Key, common.GlobalConfigValue(diff.Expected.Value)) + val := "" + if diff.Expected.Value != nil { + val = *diff.Expected.Value + } + updatedEntries, err = updatedEntries.Set(diff.Key, common.GlobalConfigValue(val)) errs = append(errs, err) } @@ -165,38 +180,23 @@ func saveDoguConfigs( return nil } -func (useCase *EcosystemConfigUseCase) markApplyConfigStart(ctx context.Context, blueprintSpec *domain.BlueprintSpec) error { - blueprintSpec.StartApplyEcosystemConfig() - err := useCase.blueprintRepository.Update(ctx, blueprintSpec) - if err != nil { - return fmt.Errorf("cannot mark blueprint as applying config: %w", err) - } - return nil -} - -func (useCase *EcosystemConfigUseCase) handleFailedApplyEcosystemConfig(ctx context.Context, blueprintSpec *domain.BlueprintSpec, err error) error { +func (useCase *EcosystemConfigUseCase) handleFailedApplyEcosystemConfig(ctx context.Context, blueprint *domain.BlueprintSpec, err error) error { logger := log.FromContext(ctx). WithName("EcosystemConfigUseCase.handleFailedApplyEcosystemConfig"). - WithValues("blueprintId", blueprintSpec.Id) + WithValues("blueprintId", blueprint.Id) - blueprintSpec.MarkApplyEcosystemConfigFailed(err) - repoErr := useCase.blueprintRepository.Update(ctx, blueprintSpec) + // sets condition + changed := blueprint.SetLastApplySucceededConditionOnError(domain.ReasonLastApplyErrorAtConfig, err) + if changed { + repoErr := useCase.blueprintRepository.Update(ctx, blueprint) - if repoErr != nil { - repoErr = errors.Join(repoErr, err) - logger.Error(repoErr, "cannot mark blueprint config apply as failed") - return fmt.Errorf("cannot mark blueprint config apply as failed while handling %q status: %w", blueprintSpec.Status, repoErr) - } - return nil -} - -func (useCase *EcosystemConfigUseCase) markConfigApplied(ctx context.Context, blueprintSpec *domain.BlueprintSpec) error { - blueprintSpec.MarkEcosystemConfigApplied() - err := useCase.blueprintRepository.Update(ctx, blueprintSpec) - if err != nil { - return fmt.Errorf("failed to mark ecosystem config applied: %w", err) + if repoErr != nil { + repoErr = errors.Join(repoErr, err) + logger.Error(repoErr, "cannot mark blueprint config apply as failed") + return fmt.Errorf("cannot mark blueprint config apply as failed: %w", repoErr) + } } - return nil + return err } // applyDiff merges the given changes from the doguConfigDiff in the DoguConfig. @@ -208,7 +208,11 @@ func applyDiff(doguConfig config.DoguConfig, diffs []domain.DoguConfigEntryDiff) var err error switch diff.NeededAction { case domain.ConfigActionSet: - updatedEntries, err = updatedEntries.Set(diff.Key.Key, config.Value(diff.Expected.Value)) + val := "" + if diff.Expected.Value != nil { + val = *diff.Expected.Value + } + updatedEntries, err = updatedEntries.Set(diff.Key.Key, config.Value(val)) case domain.ConfigActionRemove: updatedEntries = updatedEntries.Delete(diff.Key.Key) } diff --git a/pkg/application/ecosystemConfigUseCase_test.go b/pkg/application/ecosystemConfigUseCase_test.go index a2bc27a3..e9e18ab6 100644 --- a/pkg/application/ecosystemConfigUseCase_test.go +++ b/pkg/application/ecosystemConfigUseCase_test.go @@ -1,22 +1,25 @@ package application import ( + "context" + "maps" + "testing" + cescommons "github.com/cloudogu/ces-commons-lib/dogu" liberrors "github.com/cloudogu/ces-commons-lib/errors" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/common" + "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" "github.com/cloudogu/k8s-registry-lib/config" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "maps" - "testing" + "k8s.io/apimachinery/pkg/api/meta" ) const ( - redmine = cescommons.SimpleName("redmine") - cas = cescommons.SimpleName("cas") - testBlueprintID = "blueprint1" + redmine = cescommons.SimpleName("redmine") + cas = cescommons.SimpleName("cas") ) var emptyDoguList []cescommons.SimpleName @@ -31,7 +34,7 @@ func TestEcosystemConfigUseCase_ApplyConfig(t *testing.T) { sensitiveRedmineDiff := getSensitiveDoguConfigEntryDiffForAction("key", "value", redmine, domain.ConfigActionSet) sensitiveCasDiff := getSensitiveDoguConfigEntryDiffForAction("key", "value", cas, domain.ConfigActionSet) - spec := &domain.BlueprintSpec{ + blueprint := &domain.BlueprintSpec{ StateDiff: domain.StateDiff{ DoguDiffs: []domain.DoguDiff{ { @@ -92,96 +95,96 @@ func TestEcosystemConfigUseCase_ApplyConfig(t *testing.T) { globalConfigRepoMock.EXPECT().Get(testCtx).Return(globalConfig, nil) globalConfigRepoMock.EXPECT().Update(testCtx, mock.Anything).Return(globalConfig, nil) - blueprintRepoMock.EXPECT().GetById(testCtx, testBlueprintID).Return(spec, nil) - blueprintRepoMock.EXPECT().Update(testCtx, mock.Anything).Return(nil).Times(2) + blueprintRepoMock.EXPECT().Update(testCtx, mock.Anything).Return(nil) + doguInstallaltionRepoMock := newMockDoguInstallationRepository(t) + doguInstallaltionRepoMock.EXPECT().GetAll(testCtx).Return(nil, nil) - sut := NewEcosystemConfigUseCase(blueprintRepoMock, doguConfigMock, sensitiveDoguConfigMock, globalConfigRepoMock) + sut := NewEcosystemConfigUseCase(blueprintRepoMock, doguConfigMock, sensitiveDoguConfigMock, globalConfigRepoMock, doguInstallaltionRepoMock) // when - err := sut.ApplyConfig(testCtx, testBlueprintID) + err := sut.ApplyConfig(testCtx, blueprint) // then require.NoError(t, err) + require.Len(t, blueprint.Events, 1) + assert.Equal(t, domain.EcosystemConfigAppliedEvent{}, blueprint.Events[0]) }) - t.Run("should return error on fetch blueprint error", func(t *testing.T) { - // given - blueprintRepoMock := newMockBlueprintSpecRepository(t) - - blueprintRepoMock.EXPECT().GetById(testCtx, testBlueprintID).Return(nil, assert.AnError) - - sut := EcosystemConfigUseCase{blueprintRepository: blueprintRepoMock} - - // when - err := sut.ApplyConfig(testCtx, testBlueprintID) - - // then - require.Error(t, err) - assert.ErrorContains(t, err, "cannot load blueprint to apply config") - assert.ErrorIs(t, err, assert.AnError) - }) - - t.Run("mark applied if diffs are empty", func(t *testing.T) { + t.Run("pause reconciliation for dogus with config and version changes", func(t *testing.T) { // given blueprintRepoMock := newMockBlueprintSpecRepository(t) + doguConfigMock := newMockDoguConfigRepository(t) + sensitiveDoguConfigMock := newMockSensitiveDoguConfigRepository(t) + globalConfigRepoMock := newMockGlobalConfigRepository(t) - spec := &domain.BlueprintSpec{ + sensitiveCasDiff := getSensitiveDoguConfigEntryDiffForAction("key", "value", cas, domain.ConfigActionSet) + blueprint := &domain.BlueprintSpec{ StateDiff: domain.StateDiff{ - DoguConfigDiffs: map[cescommons.SimpleName]domain.DoguConfigDiffs{}, - SensitiveDoguConfigDiffs: map[cescommons.SimpleName]domain.SensitiveDoguConfigDiffs{}, - GlobalConfigDiffs: domain.GlobalConfigDiffs{}, + DoguDiffs: []domain.DoguDiff{ + { + DoguName: redmine, + NeededActions: []domain.Action{domain.ActionUpgrade}, + }, + { + DoguName: cas, + NeededActions: []domain.Action{domain.ActionUpgrade}, + }, + }, + DoguConfigDiffs: map[cescommons.SimpleName]domain.DoguConfigDiffs{ + redmine: { + getSetDoguConfigEntryDiff("key", "value", redmine), + }, + }, + SensitiveDoguConfigDiffs: map[cescommons.SimpleName]domain.SensitiveDoguConfigDiffs{ + cas: { + sensitiveCasDiff, + }, + }, }, } - blueprintRepoMock.EXPECT().GetById(testCtx, testBlueprintID).Return(spec, nil) - blueprintRepoMock.EXPECT().Update(testCtx, mock.Anything).Return(nil).Times(1) - - sut := EcosystemConfigUseCase{blueprintRepository: blueprintRepoMock} - - // when - err := sut.ApplyConfig(testCtx, testBlueprintID) + // Just check if the routine hits the repos. Check values in concrete test of methods. + doguConfigMock.EXPECT(). + GetAllExisting(testCtx, []cescommons.SimpleName{redmine}). + Return(map[cescommons.SimpleName]config.DoguConfig{ + redmine: config.CreateDoguConfig(redmine, map[config.Key]config.Value{}), + }, nil) + doguConfigMock.EXPECT().UpdateOrCreate(testCtx, mock.Anything).Return(config.DoguConfig{}, nil) - // then - require.NoError(t, err) - assert.Equal(t, domain.StatusPhaseEcosystemConfigApplied, spec.Status) - }) + sensitiveDoguConfigMock.EXPECT(). + GetAllExisting(testCtx, []cescommons.SimpleName{cas}). + Return(map[cescommons.SimpleName]config.DoguConfig{ + cas: config.CreateDoguConfig(cas, map[config.Key]config.Value{}), + }, nil) + sensitiveDoguConfigMock.EXPECT().UpdateOrCreate(testCtx, mock.Anything).Return(config.DoguConfig{}, nil) - t.Run("should return on mark apply config start error", func(t *testing.T) { - // given - blueprintRepoMock := newMockBlueprintSpecRepository(t) + globalConfigRepoMock.EXPECT().Get(testCtx).Return(config.GlobalConfig{}, nil) - spec := &domain.BlueprintSpec{ - StateDiff: domain.StateDiff{ - DoguConfigDiffs: map[cescommons.SimpleName]domain.DoguConfigDiffs{}, - SensitiveDoguConfigDiffs: map[cescommons.SimpleName]domain.SensitiveDoguConfigDiffs{}, - GlobalConfigDiffs: domain.GlobalConfigDiffs{ - getSetGlobalConfigEntryDiff("key", "value"), - }, - }, + blueprintRepoMock.EXPECT().Update(testCtx, mock.Anything).Return(nil) + doguInstallaltionRepoMock := newMockDoguInstallationRepository(t) + dogus := map[cescommons.SimpleName]*ecosystem.DoguInstallation{ + redmine: {Name: cescommons.QualifiedName{SimpleName: redmine, Namespace: "namespace"}}, + cas: {Name: cescommons.QualifiedName{SimpleName: cas, Namespace: "namespace"}}, } - - blueprintRepoMock.EXPECT().GetById(testCtx, testBlueprintID).Return(spec, nil) - blueprintRepoMock.EXPECT().Update(testCtx, mock.Anything).Return(assert.AnError).Times(1) - blueprintRepoMock.EXPECT().Update(testCtx, mock.Anything).Return(nil).Times(1) - - sut := EcosystemConfigUseCase{blueprintRepository: blueprintRepoMock} + doguInstallaltionRepoMock.EXPECT().GetAll(testCtx).Return(dogus, nil) + doguInstallaltionRepoMock.EXPECT().Update(testCtx, mock.Anything).Run(func(ctx context.Context, dogu *ecosystem.DoguInstallation) { + assert.True(t, dogu.PauseReconciliation) + }).Return(nil).Times(2) + sut := NewEcosystemConfigUseCase(blueprintRepoMock, doguConfigMock, sensitiveDoguConfigMock, globalConfigRepoMock, doguInstallaltionRepoMock) // when - err := sut.ApplyConfig(testCtx, testBlueprintID) + err := sut.ApplyConfig(testCtx, blueprint) // then require.NoError(t, err) - assert.Equal(t, spec.Status, domain.StatusPhaseApplyEcosystemConfigFailed) + require.Len(t, blueprint.Events, 1) + assert.Equal(t, domain.EcosystemConfigAppliedEvent{}, blueprint.Events[0]) }) t.Run("error applying dogu config", func(t *testing.T) { // given - blueprintRepoMock := newMockBlueprintSpecRepository(t) - doguConfigMock := newMockDoguConfigRepository(t) - sensitiveDoguConfigMock := newMockSensitiveDoguConfigRepository(t) - globalConfigMock := newMockGlobalConfigRepository(t) - - spec := &domain.BlueprintSpec{ + blueprint := &domain.BlueprintSpec{ + Conditions: []domain.Condition{}, StateDiff: domain.StateDiff{ DoguConfigDiffs: map[cescommons.SimpleName]domain.DoguConfigDiffs{ redmine: { @@ -194,6 +197,10 @@ func TestEcosystemConfigUseCase_ApplyConfig(t *testing.T) { }, } + blueprintRepoMock := newMockBlueprintSpecRepository(t) + sensitiveDoguConfigMock := newMockSensitiveDoguConfigRepository(t) + globalConfigMock := newMockGlobalConfigRepository(t) + doguConfigMock := newMockDoguConfigRepository(t) // Just check if the routine hits the repos. Check values in concrete test of methods. doguConfigMock.EXPECT(). GetAllExisting(testCtx, []cescommons.SimpleName{cas, redmine}). @@ -202,24 +209,23 @@ func TestEcosystemConfigUseCase_ApplyConfig(t *testing.T) { cas: config.CreateDoguConfig(cas, map[config.Key]config.Value{}), }, nil) doguConfigMock.EXPECT().UpdateOrCreate(testCtx, mock.Anything).Return(config.DoguConfig{}, assert.AnError).Times(1) + blueprintRepoMock.EXPECT().Update(testCtx, mock.Anything).Return(nil) + doguInstallaltionRepoMock := newMockDoguInstallationRepository(t) + doguInstallaltionRepoMock.EXPECT().GetAll(testCtx).Return(nil, nil) - blueprintRepoMock.EXPECT().GetById(testCtx, testBlueprintID).Return(spec, nil) - blueprintRepoMock.EXPECT().Update(testCtx, mock.Anything).Return(nil).Times(2) - - sut := NewEcosystemConfigUseCase(blueprintRepoMock, doguConfigMock, sensitiveDoguConfigMock, globalConfigMock) + sut := NewEcosystemConfigUseCase(blueprintRepoMock, doguConfigMock, sensitiveDoguConfigMock, globalConfigMock, doguInstallaltionRepoMock) // when - err := sut.ApplyConfig(testCtx, testBlueprintID) + err := sut.ApplyConfig(testCtx, blueprint) // then - require.NoError(t, err) - assert.Equal(t, domain.StatusPhaseApplyEcosystemConfigFailed, spec.Status) - require.Len(t, spec.Events, 2) - assert.Equal(t, domain.ApplyEcosystemConfigEvent{}, spec.Events[0]) - assert.Contains(t, spec.Events[1].Message(), "could not apply normal dogu config") - // cannot check for dogu name here as the order of the events is not fixed. It could be either redmine or cas - assert.Contains(t, spec.Events[1].Message(), "could not persist config for dogu") - assert.Contains(t, spec.Events[1].Message(), "assert.AnError general error for testing") + require.Error(t, err) + assert.ErrorIs(t, err, assert.AnError) + assert.ErrorContains(t, err, "could not apply normal dogu config") + assert.True(t, meta.IsStatusConditionFalse(blueprint.Conditions, domain.ConditionLastApplySucceeded)) + + require.Len(t, blueprint.Events, 1) + assert.Equal(t, domain.NewExecutionFailedEvent(err), blueprint.Events[0]) }) t.Run("error applying sensitive config", func(t *testing.T) { // given @@ -228,7 +234,8 @@ func TestEcosystemConfigUseCase_ApplyConfig(t *testing.T) { sensitiveDoguConfigMock := newMockSensitiveDoguConfigRepository(t) globalConfigMock := newMockGlobalConfigRepository(t) - spec := &domain.BlueprintSpec{ + blueprint := &domain.BlueprintSpec{ + Conditions: []domain.Condition{}, StateDiff: domain.StateDiff{ SensitiveDoguConfigDiffs: map[cescommons.SimpleName]domain.DoguConfigDiffs{ redmine: { @@ -252,24 +259,27 @@ func TestEcosystemConfigUseCase_ApplyConfig(t *testing.T) { cas: config.CreateDoguConfig(cas, map[config.Key]config.Value{}), }, nil) sensitiveDoguConfigMock.EXPECT().UpdateOrCreate(testCtx, mock.Anything).Return(config.DoguConfig{}, assert.AnError).Times(1) - - blueprintRepoMock.EXPECT().GetById(testCtx, testBlueprintID).Return(spec, nil) - blueprintRepoMock.EXPECT().Update(testCtx, mock.Anything).Return(nil).Times(2) - - sut := NewEcosystemConfigUseCase(blueprintRepoMock, doguConfigMock, sensitiveDoguConfigMock, globalConfigMock) + blueprintRepoMock.EXPECT().Update(testCtx, mock.Anything).Return(nil) + doguInstallaltionRepoMock := newMockDoguInstallationRepository(t) + doguInstallaltionRepoMock.EXPECT().GetAll(testCtx).Return(nil, nil) + sut := NewEcosystemConfigUseCase(blueprintRepoMock, doguConfigMock, sensitiveDoguConfigMock, globalConfigMock, doguInstallaltionRepoMock) // when - err := sut.ApplyConfig(testCtx, testBlueprintID) + err := sut.ApplyConfig(testCtx, blueprint) // then - require.NoError(t, err) - assert.Equal(t, domain.StatusPhaseApplyEcosystemConfigFailed, spec.Status) - require.Len(t, spec.Events, 2) - assert.Equal(t, domain.ApplyEcosystemConfigEvent{}, spec.Events[0]) - assert.Contains(t, spec.Events[1].Message(), "could not apply sensitive dogu config") + require.Error(t, err) + assert.ErrorIs(t, err, assert.AnError) + assert.ErrorContains(t, err, "could not apply sensitive dogu config") + + assert.True(t, meta.IsStatusConditionFalse(blueprint.Conditions, domain.ConditionLastApplySucceeded)) + + require.Len(t, blueprint.Events, 1) + assert.Equal(t, domain.NewExecutionFailedEvent(err), blueprint.Events[0]) + assert.Contains(t, blueprint.Events[0].Message(), "could not apply sensitive dogu config") // cannot check for dogu name here as the order of the events is not fixed. It could be either redmine or cas - assert.Contains(t, spec.Events[1].Message(), "could not persist config for dogu") - assert.Contains(t, spec.Events[1].Message(), "assert.AnError general error for testing") + assert.Contains(t, blueprint.Events[0].Message(), "could not persist config for dogu") + assert.Contains(t, blueprint.Events[0].Message(), "assert.AnError general error for testing") }) t.Run("error applying global config", func(t *testing.T) { // given @@ -278,7 +288,8 @@ func TestEcosystemConfigUseCase_ApplyConfig(t *testing.T) { sensitiveDoguConfigMock := newMockSensitiveDoguConfigRepository(t) globalConfigMock := newMockGlobalConfigRepository(t) - spec := &domain.BlueprintSpec{ + blueprint := &domain.BlueprintSpec{ + Conditions: []domain.Condition{}, StateDiff: domain.StateDiff{ DoguConfigDiffs: map[cescommons.SimpleName]domain.DoguConfigDiffs{}, SensitiveDoguConfigDiffs: map[cescommons.SimpleName]domain.SensitiveDoguConfigDiffs{}, @@ -302,21 +313,245 @@ func TestEcosystemConfigUseCase_ApplyConfig(t *testing.T) { globalConfigMock.EXPECT().Get(testCtx).Return(globalConfig, nil) globalConfigMock.EXPECT().Update(testCtx, mock.Anything).Return(globalConfig, assert.AnError) - blueprintRepoMock.EXPECT().GetById(testCtx, testBlueprintID).Return(spec, nil) - blueprintRepoMock.EXPECT().Update(testCtx, mock.Anything).Return(nil).Times(2) + blueprintRepoMock.EXPECT().Update(testCtx, blueprint).Return(nil) + doguInstallaltionRepoMock := newMockDoguInstallationRepository(t) + doguInstallaltionRepoMock.EXPECT().GetAll(testCtx).Return(nil, nil) + sut := NewEcosystemConfigUseCase(blueprintRepoMock, doguConfigMock, sensitiveDoguConfigMock, globalConfigMock, doguInstallaltionRepoMock) + + // when + err := sut.ApplyConfig(testCtx, blueprint) + + // then + require.Error(t, err) + assert.ErrorIs(t, err, assert.AnError) + assert.ErrorContains(t, err, "could not apply global config") + + assert.True(t, meta.IsStatusConditionFalse(blueprint.Conditions, domain.ConditionLastApplySucceeded)) - sut := NewEcosystemConfigUseCase(blueprintRepoMock, doguConfigMock, sensitiveDoguConfigMock, globalConfigMock) + require.Len(t, blueprint.Events, 1) + assert.Equal(t, domain.NewExecutionFailedEvent(err), blueprint.Events[0]) + assert.Contains(t, blueprint.Events[0].Message(), "could not apply global config") + assert.Contains(t, blueprint.Events[0].Message(), "assert.AnError general error for testing") + }) +} + +func TestEcosystemConfigUseCase_pauseReconciliationForDogus(t *testing.T) { + t.Run("pause reconciliation for multiple dogus when dogu config changes", func(t *testing.T) { + // given + stateDiff := domain.StateDiff{ + DoguDiffs: []domain.DoguDiff{ + { + DoguName: redmine, + NeededActions: []domain.Action{domain.ActionUpgrade}, + }, + { + DoguName: cas, + NeededActions: []domain.Action{domain.ActionUpgrade}, + }, + { + DoguName: postfix, + NeededActions: []domain.Action{}, // no action, so no pause required + }, + }, + DoguConfigDiffs: map[cescommons.SimpleName]domain.DoguConfigDiffs{ + redmine: { + getSetDoguConfigEntryDiff("key", "value", redmine), + }, + cas: { + getSetDoguConfigEntryDiff("key", "value", cas), + }, + postfix: { + getSetDoguConfigEntryDiff("key", "value", postfix), + }, + }, + } + + doguInstallaltionRepoMock := newMockDoguInstallationRepository(t) + dogus := map[cescommons.SimpleName]*ecosystem.DoguInstallation{ + redmine: {Name: cescommons.QualifiedName{SimpleName: redmine, Namespace: "namespace"}}, + cas: {Name: cescommons.QualifiedName{SimpleName: cas, Namespace: "namespace"}}, + postfix: {Name: cescommons.QualifiedName{SimpleName: postfix, Namespace: "namespace"}}, + } + doguInstallaltionRepoMock.EXPECT().GetAll(testCtx).Return(dogus, nil) + doguInstallaltionRepoMock.EXPECT().Update(testCtx, mock.Anything).Run(func(ctx context.Context, dogu *ecosystem.DoguInstallation) { + assert.True(t, dogu.PauseReconciliation) + }).Return(nil).Times(2) // when - err := sut.ApplyConfig(testCtx, testBlueprintID) + sut := NewEcosystemConfigUseCase(nil, nil, nil, nil, doguInstallaltionRepoMock) + err := sut.pauseReconciliationForDogus(testCtx, stateDiff) // then require.NoError(t, err) - assert.Equal(t, domain.StatusPhaseApplyEcosystemConfigFailed, spec.Status) - require.Len(t, spec.Events, 2) - assert.Equal(t, domain.ApplyEcosystemConfigEvent{}, spec.Events[0]) - assert.Contains(t, spec.Events[1].Message(), "could not apply global config") - assert.Contains(t, spec.Events[1].Message(), "assert.AnError general error for testing") + }) + + t.Run("pause reconciliation for multiple dogus when sensitive config changes", func(t *testing.T) { + // given + stateDiff := domain.StateDiff{ + DoguDiffs: []domain.DoguDiff{ + { + DoguName: redmine, + NeededActions: []domain.Action{domain.ActionUpgrade}, + }, + { + DoguName: cas, + NeededActions: []domain.Action{domain.ActionUpgrade}, + }, + { + DoguName: postfix, + NeededActions: []domain.Action{}, // no action, so no pause required + }, + }, + SensitiveDoguConfigDiffs: map[cescommons.SimpleName]domain.SensitiveDoguConfigDiffs{ + redmine: { + getSetDoguConfigEntryDiff("key", "value", redmine), + }, + cas: { + getSetDoguConfigEntryDiff("key", "value", cas), + }, + postfix: { + getSetDoguConfigEntryDiff("key", "value", postfix), + }, + }, + } + + doguInstallaltionRepoMock := newMockDoguInstallationRepository(t) + dogus := map[cescommons.SimpleName]*ecosystem.DoguInstallation{ + redmine: {Name: cescommons.QualifiedName{SimpleName: redmine, Namespace: "namespace"}}, + cas: {Name: cescommons.QualifiedName{SimpleName: cas, Namespace: "namespace"}}, + postfix: {Name: cescommons.QualifiedName{SimpleName: postfix, Namespace: "namespace"}}, + } + doguInstallaltionRepoMock.EXPECT().GetAll(testCtx).Return(dogus, nil) + doguInstallaltionRepoMock.EXPECT().Update(testCtx, mock.Anything).Run(func(ctx context.Context, dogu *ecosystem.DoguInstallation) { + assert.True(t, dogu.PauseReconciliation) + }).Return(nil).Times(2) + + // when + sut := NewEcosystemConfigUseCase(nil, nil, nil, nil, doguInstallaltionRepoMock) + err := sut.pauseReconciliationForDogus(testCtx, stateDiff) + + // then + require.NoError(t, err) + }) + + t.Run("pause reconciliation for multiple dogus when global config changes", func(t *testing.T) { + // given + stateDiff := domain.StateDiff{ + DoguDiffs: []domain.DoguDiff{ + { + DoguName: redmine, + NeededActions: []domain.Action{domain.ActionUpgrade}, + }, + { + DoguName: cas, + NeededActions: []domain.Action{domain.ActionUpgrade}, + }, + { + DoguName: postfix, + NeededActions: []domain.Action{}, // no action, so no pause required + }, + }, + GlobalConfigDiffs: domain.GlobalConfigDiffs{ + getSetGlobalConfigEntryDiff("key", "value"), + }, + } + + doguInstallaltionRepoMock := newMockDoguInstallationRepository(t) + dogus := map[cescommons.SimpleName]*ecosystem.DoguInstallation{ + redmine: {Name: cescommons.QualifiedName{SimpleName: redmine, Namespace: "namespace"}}, + cas: {Name: cescommons.QualifiedName{SimpleName: cas, Namespace: "namespace"}}, + postfix: {Name: cescommons.QualifiedName{SimpleName: postfix, Namespace: "namespace"}}, + } + doguInstallaltionRepoMock.EXPECT().GetAll(testCtx).Return(dogus, nil) + doguInstallaltionRepoMock.EXPECT().Update(testCtx, mock.Anything).Run(func(ctx context.Context, dogu *ecosystem.DoguInstallation) { + assert.True(t, dogu.PauseReconciliation) + }).Return(nil).Times(2) + + // when + sut := NewEcosystemConfigUseCase(nil, nil, nil, nil, doguInstallaltionRepoMock) + err := sut.pauseReconciliationForDogus(testCtx, stateDiff) + + // then + require.NoError(t, err) + }) + + t.Run("do not pause reconciliation for dogus without config changes", func(t *testing.T) { + // given + stateDiff := domain.StateDiff{ + DoguDiffs: []domain.DoguDiff{ + { + DoguName: redmine, + NeededActions: []domain.Action{domain.ActionUpgrade}, + }, + { + DoguName: cas, + NeededActions: []domain.Action{domain.ActionUpgrade}, + }, + { + DoguName: postfix, + NeededActions: []domain.Action{}, // no action, so no pause required + }, + }, + } + + doguInstallaltionRepoMock := newMockDoguInstallationRepository(t) + dogus := map[cescommons.SimpleName]*ecosystem.DoguInstallation{ + redmine: {Name: cescommons.QualifiedName{SimpleName: redmine, Namespace: "namespace"}}, + cas: {Name: cescommons.QualifiedName{SimpleName: cas, Namespace: "namespace"}}, + postfix: {Name: cescommons.QualifiedName{SimpleName: postfix, Namespace: "namespace"}}, + } + doguInstallaltionRepoMock.EXPECT().GetAll(testCtx).Return(dogus, nil) + // No Update calls + + // when + sut := NewEcosystemConfigUseCase(nil, nil, nil, nil, doguInstallaltionRepoMock) + err := sut.pauseReconciliationForDogus(testCtx, stateDiff) + + // then + require.NoError(t, err) + }) + + t.Run("error on get all dogus error", func(t *testing.T) { + // given + doguInstallaltionRepoMock := newMockDoguInstallationRepository(t) + doguInstallaltionRepoMock.EXPECT().GetAll(testCtx).Return(nil, assert.AnError) + + // when + sut := NewEcosystemConfigUseCase(nil, nil, nil, nil, doguInstallaltionRepoMock) + err := sut.pauseReconciliationForDogus(testCtx, domain.StateDiff{}) + + // then + require.Error(t, err) + assert.ErrorContains(t, err, "error while attempting to load dogus") + }) + + t.Run("error on update error", func(t *testing.T) { + // given + stateDiff := domain.StateDiff{ + DoguDiffs: []domain.DoguDiff{ + { + DoguName: redmine, + NeededActions: []domain.Action{domain.ActionUpgrade}, + }, + }, + GlobalConfigDiffs: domain.GlobalConfigDiffs{ + getSetGlobalConfigEntryDiff("key", "value"), + }, + } + dogus := map[cescommons.SimpleName]*ecosystem.DoguInstallation{ + redmine: {Name: cescommons.QualifiedName{SimpleName: redmine, Namespace: "namespace"}}, + } + doguInstallaltionRepoMock := newMockDoguInstallationRepository(t) + doguInstallaltionRepoMock.EXPECT().GetAll(testCtx).Return(dogus, nil) + doguInstallaltionRepoMock.EXPECT().Update(testCtx, mock.Anything).Return(assert.AnError) + + // when + sut := NewEcosystemConfigUseCase(nil, nil, nil, nil, doguInstallaltionRepoMock) + err := sut.pauseReconciliationForDogus(testCtx, stateDiff) + + // then + require.Error(t, err) + assert.ErrorContains(t, err, "could not pause reconciliation for dogu") }) } @@ -345,9 +580,9 @@ func TestEcosystemConfigUseCase_applyDoguConfigDiffs(t *testing.T) { "key1": "val1", "key2": "val2", }).Config - updatedConfig, err := updatedConfig.Set(diff1.Key.Key, config.Value(diff1.Expected.Value)) + updatedConfig, err := updatedConfig.Set(diff1.Key.Key, config.Value(*diff1.Expected.Value)) require.NoError(t, err) - updatedConfig, err = updatedConfig.Set(diff2.Key.Key, config.Value(diff2.Expected.Value)) + updatedConfig, err = updatedConfig.Set(diff2.Key.Key, config.Value(*diff2.Expected.Value)) require.NoError(t, err) doguConfigMock.EXPECT(). @@ -471,7 +706,7 @@ func TestEcosystemConfigUseCase_applyGlobalConfigDiffs(t *testing.T) { t.Run("should save diffs with action set", func(t *testing.T) { // given globalConfigMock := newMockGlobalConfigRepository(t) - sut := NewEcosystemConfigUseCase(nil, nil, nil, globalConfigMock) + sut := NewEcosystemConfigUseCase(nil, nil, nil, globalConfigMock, nil) diff1 := getSetGlobalConfigEntryDiff("key1", "value1") diff2 := getSetGlobalConfigEntryDiff("key2", "value2") byAction := map[domain.ConfigAction][]domain.GlobalConfigEntryDiff{domain.ConfigActionSet: {diff1, diff2}} @@ -479,9 +714,9 @@ func TestEcosystemConfigUseCase_applyGlobalConfigDiffs(t *testing.T) { entries, _ := config.MapToEntries(map[string]any{}) globalConfig := config.CreateGlobalConfig(entries) - updatedEntries, err := globalConfig.Set(diff1.Key, common.GlobalConfigValue(diff1.Expected.Value)) + updatedEntries, err := globalConfig.Set(diff1.Key, common.GlobalConfigValue(*diff1.Expected.Value)) require.NoError(t, err) - updatedEntries, err = updatedEntries.Set(diff2.Key, common.GlobalConfigValue(diff2.Expected.Value)) + updatedEntries, err = updatedEntries.Set(diff2.Key, common.GlobalConfigValue(*diff2.Expected.Value)) require.NoError(t, err) globalConfigMock.EXPECT().Get(testCtx).Return(globalConfig, nil) @@ -497,7 +732,7 @@ func TestEcosystemConfigUseCase_applyGlobalConfigDiffs(t *testing.T) { t.Run("should delete diffs with action remove", func(t *testing.T) { // given globalConfigMock := newMockGlobalConfigRepository(t) - sut := NewEcosystemConfigUseCase(nil, nil, nil, globalConfigMock) + sut := NewEcosystemConfigUseCase(nil, nil, nil, globalConfigMock, nil) diff1 := getRemoveGlobalConfigEntryDiff("key") diff2 := getRemoveGlobalConfigEntryDiff("key1") byAction := map[domain.ConfigAction][]domain.GlobalConfigEntryDiff{domain.ConfigActionRemove: {diff1, diff2}} @@ -521,7 +756,7 @@ func TestEcosystemConfigUseCase_applyGlobalConfigDiffs(t *testing.T) { t.Run("should return nil on action none", func(t *testing.T) { // given globalConfigMock := newMockGlobalConfigRepository(t) - sut := NewEcosystemConfigUseCase(nil, nil, nil, globalConfigMock) + sut := NewEcosystemConfigUseCase(nil, nil, nil, globalConfigMock, nil) diff1 := domain.GlobalConfigEntryDiff{ NeededAction: domain.ConfigActionNone, } @@ -542,7 +777,7 @@ func TestEcosystemConfigUseCase_applyGlobalConfigDiffs(t *testing.T) { t.Run("err when get fails", func(t *testing.T) { // given globalConfigMock := newMockGlobalConfigRepository(t) - sut := NewEcosystemConfigUseCase(nil, nil, nil, globalConfigMock) + sut := NewEcosystemConfigUseCase(nil, nil, nil, globalConfigMock, nil) diff1 := domain.GlobalConfigEntryDiff{ NeededAction: domain.ConfigActionSet, } @@ -563,94 +798,12 @@ func TestEcosystemConfigUseCase_applyGlobalConfigDiffs(t *testing.T) { }) } -func TestEcosystemConfigUseCase_markConfigApplied(t *testing.T) { - t.Run("should set applied status and event", func(t *testing.T) { - // given - spec := &domain.BlueprintSpec{} - expectedSpec := &domain.BlueprintSpec{} - expectedSpec.Status = domain.StatusPhaseEcosystemConfigApplied - expectedSpec.Events = append(spec.Events, domain.EcosystemConfigAppliedEvent{}) - blueprintRepoMock := newMockBlueprintSpecRepository(t) - - blueprintRepoMock.EXPECT().Update(testCtx, expectedSpec).Return(nil) - - sut := EcosystemConfigUseCase{blueprintRepository: blueprintRepoMock} - - // when - err := sut.markConfigApplied(testCtx, spec) - - // then - require.NoError(t, err) - }) - - t.Run("should return an error on update error", func(t *testing.T) { - // given - spec := &domain.BlueprintSpec{} - expectedSpec := &domain.BlueprintSpec{} - expectedSpec.Status = domain.StatusPhaseEcosystemConfigApplied - expectedSpec.Events = append(spec.Events, domain.EcosystemConfigAppliedEvent{}) - blueprintRepoMock := newMockBlueprintSpecRepository(t) - - blueprintRepoMock.EXPECT().Update(testCtx, expectedSpec).Return(assert.AnError) - - sut := EcosystemConfigUseCase{blueprintRepository: blueprintRepoMock} - - // when - err := sut.markConfigApplied(testCtx, spec) - - // then - require.Error(t, err) - assert.ErrorContains(t, err, "failed to mark ecosystem config applied") - assert.ErrorIs(t, err, assert.AnError) - }) -} - -func TestEcosystemConfigUseCase_markApplyConfigStart(t *testing.T) { - t.Run("should set status and event apply config", func(t *testing.T) { - // given - spec := &domain.BlueprintSpec{} - expectedSpec := &domain.BlueprintSpec{} - expectedSpec.Status = domain.StatusPhaseApplyEcosystemConfig - expectedSpec.Events = append(spec.Events, domain.ApplyEcosystemConfigEvent{}) - blueprintRepoMock := newMockBlueprintSpecRepository(t) - - blueprintRepoMock.EXPECT().Update(testCtx, expectedSpec).Return(nil) - - sut := EcosystemConfigUseCase{blueprintRepository: blueprintRepoMock} - - // when - err := sut.markApplyConfigStart(testCtx, spec) - - // then - require.NoError(t, err) - }) - - t.Run("should return an error on update error", func(t *testing.T) { - // given - spec := &domain.BlueprintSpec{} - expectedSpec := &domain.BlueprintSpec{} - expectedSpec.Status = domain.StatusPhaseApplyEcosystemConfig - expectedSpec.Events = append(spec.Events, domain.ApplyEcosystemConfigEvent{}) - blueprintRepoMock := newMockBlueprintSpecRepository(t) - - blueprintRepoMock.EXPECT().Update(testCtx, expectedSpec).Return(assert.AnError) - - sut := EcosystemConfigUseCase{blueprintRepository: blueprintRepoMock} - - // when - err := sut.markApplyConfigStart(testCtx, spec) - - // then - require.Error(t, err) - assert.ErrorContains(t, err, "cannot mark blueprint as applying config") - assert.ErrorIs(t, err, assert.AnError) - }) -} - func TestEcosystemConfigUseCase_handleFailedApplyEcosystemConfig(t *testing.T) { - t.Run("should set applied status and event", func(t *testing.T) { + t.Run("should set applied condition and event", func(t *testing.T) { // given - spec := &domain.BlueprintSpec{} + blueprint := &domain.BlueprintSpec{ + Conditions: []domain.Condition{}, + } blueprintRepoMock := newMockBlueprintSpecRepository(t) blueprintRepoMock.EXPECT().Update(testCtx, mock.IsType(&domain.BlueprintSpec{})).Return(nil) @@ -658,17 +811,22 @@ func TestEcosystemConfigUseCase_handleFailedApplyEcosystemConfig(t *testing.T) { sut := EcosystemConfigUseCase{blueprintRepository: blueprintRepoMock} // when - err := sut.handleFailedApplyEcosystemConfig(testCtx, spec, assert.AnError) + err := sut.handleFailedApplyEcosystemConfig(testCtx, blueprint, assert.AnError) // then - require.NoError(t, err) - assert.Equal(t, domain.StatusPhaseApplyEcosystemConfigFailed, spec.Status) - assert.IsType(t, domain.ApplyEcosystemConfigFailedEvent{}, spec.Events[0]) + require.Error(t, err) + condition := meta.FindStatusCondition(blueprint.Conditions, domain.ConditionLastApplySucceeded) + require.NotNil(t, condition) + assert.Equal(t, err.Error(), condition.Message) + assert.True(t, meta.IsStatusConditionFalse(blueprint.Conditions, domain.ConditionLastApplySucceeded)) + assert.IsType(t, domain.ExecutionFailedEvent{}, blueprint.Events[0]) }) t.Run("should return error on update error", func(t *testing.T) { // given - spec := &domain.BlueprintSpec{} + spec := &domain.BlueprintSpec{ + Conditions: []domain.Condition{}, + } blueprintRepoMock := newMockBlueprintSpecRepository(t) blueprintRepoMock.EXPECT().Update(testCtx, mock.IsType(&domain.BlueprintSpec{})).Return(assert.AnError) @@ -680,7 +838,8 @@ func TestEcosystemConfigUseCase_handleFailedApplyEcosystemConfig(t *testing.T) { // then require.Error(t, err) - assert.ErrorContains(t, err, "cannot mark blueprint config apply as failed while handling \"applyEcosystemConfigFailed\" status") + assert.ErrorContains(t, err, "cannot mark blueprint config apply as failed") + assert.ErrorContains(t, err, assert.AnError.Error()) assert.ErrorIs(t, err, assert.AnError) }) } @@ -694,7 +853,7 @@ func TestNewEcosystemConfigUseCase(t *testing.T) { globalConfigMock := newMockGlobalConfigRepository(t) // when - useCase := NewEcosystemConfigUseCase(blueprintRepoMock, doguConfigMock, sensitiveDoguConfigMock, globalConfigMock) + useCase := NewEcosystemConfigUseCase(blueprintRepoMock, doguConfigMock, sensitiveDoguConfigMock, globalConfigMock, nil) // then assert.Equal(t, blueprintRepoMock, useCase.blueprintRepository) @@ -711,7 +870,7 @@ func getSetDoguConfigEntryDiff(key, value string, doguName cescommons.SimpleName DoguName: doguName, }, Expected: domain.DoguConfigValueState{ - Value: value, + Value: &value, }, NeededAction: domain.ConfigActionSet, } @@ -729,12 +888,12 @@ func getRemoveDoguConfigEntryDiff(key string, doguName cescommons.SimpleName) do func getSensitiveDoguConfigEntryDiffForAction(key, value string, doguName cescommons.SimpleName, action domain.ConfigAction) domain.SensitiveDoguConfigEntryDiff { return domain.SensitiveDoguConfigEntryDiff{ - Key: common.SensitiveDoguConfigKey{ + Key: common.DoguConfigKey{ Key: config.Key(key), DoguName: doguName, }, Expected: domain.DoguConfigValueState{ - Value: value, + Value: &value, }, NeededAction: action, } @@ -742,7 +901,7 @@ func getSensitiveDoguConfigEntryDiffForAction(key, value string, doguName cescom func getRemoveSensitiveDoguConfigEntryDiff(key string, doguName cescommons.SimpleName) domain.SensitiveDoguConfigEntryDiff { return domain.SensitiveDoguConfigEntryDiff{ - Key: common.SensitiveDoguConfigKey{ + Key: common.DoguConfigKey{ Key: config.Key(key), DoguName: doguName, }, @@ -754,7 +913,7 @@ func getSetGlobalConfigEntryDiff(key, value string) domain.GlobalConfigEntryDiff return domain.GlobalConfigEntryDiff{ Key: common.GlobalConfigKey(key), Expected: domain.GlobalConfigValueState{ - Value: value, + Value: &value, }, NeededAction: domain.ConfigActionSet, } diff --git a/pkg/application/ecosystemHealthUseCase.go b/pkg/application/ecosystemHealthUseCase.go index 77e31256..033c3ceb 100644 --- a/pkg/application/ecosystemHealthUseCase.go +++ b/pkg/application/ecosystemHealthUseCase.go @@ -3,42 +3,71 @@ package application import ( "context" "errors" + "fmt" + + "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" + "sigs.k8s.io/controller-runtime/pkg/log" ) type EcosystemHealthUseCase struct { - doguUseCase doguInstallationUseCase - componentUseCase componentInstallationUseCase + doguUseCase doguInstallationUseCase + blueprintRepo blueprintSpecRepository } -func NewEcosystemHealthUseCase( - doguUseCase doguInstallationUseCase, - componentUseCase componentInstallationUseCase, -) *EcosystemHealthUseCase { +func NewEcosystemHealthUseCase(doguUseCase doguInstallationUseCase, blueprintRepo blueprintSpecRepository) *EcosystemHealthUseCase { return &EcosystemHealthUseCase{ - doguUseCase: doguUseCase, - componentUseCase: componentUseCase, + doguUseCase: doguUseCase, + blueprintRepo: blueprintRepo, + } +} + +// CheckEcosystemHealth checks the ecosystem health once and sets the health condition accordingly. +// Returns the health result. +// Returns a domain.UnhealthyEcosystemError and the ecosystem.HealthResult if the ecosystem is unhealthy or +// returns a domainservice.ConflictError if there was a conflicting update to the blueprint or +// returns a domainservice.InternalError if the health status could not be determined or the there was any another problem. +func (useCase *EcosystemHealthUseCase) CheckEcosystemHealth( + ctx context.Context, + blueprint *domain.BlueprintSpec, +) (ecosystem.HealthResult, error) { + health, determineHealthError := useCase.getEcosystemHealth( + ctx, + blueprint.Config.IgnoreDoguHealth, + ) + healthChanged := blueprint.HandleHealthResult(health, determineHealthError) + if healthChanged { + updateErr := useCase.blueprintRepo.Update(ctx, blueprint) + if updateErr != nil { + return ecosystem.HealthResult{}, fmt.Errorf( + "could not update health condition after health check: %w", + errors.Join(updateErr, determineHealthError), + ) + } } + if determineHealthError == nil && !health.AllHealthy() { + return health, domain.NewUnhealthyEcosystemError(nil, "ecosystem is unhealthy", health) + } + + return health, determineHealthError } -// CheckEcosystemHealth checks the ecosystem health once. +// getEcosystemHealth checks the ecosystem health once. // Returns a HealthResult even if parts are unhealthy or // returns an error if the health state could not be fetched. -func (useCase *EcosystemHealthUseCase) CheckEcosystemHealth(ctx context.Context, ignoreDoguHealth bool, ignoreComponentHealth bool) (ecosystem.HealthResult, error) { +func (useCase *EcosystemHealthUseCase) getEcosystemHealth( + ctx context.Context, + ignoreDoguHealth bool, +) (ecosystem.HealthResult, error) { + logger := log.FromContext(ctx).WithName("EcosystemHealthUseCase.getEcosystemHealth") + logger.V(1).Info("check ecosystem health...") var doguHealth ecosystem.DoguHealthResult var doguHealthErr error if !ignoreDoguHealth { doguHealth, doguHealthErr = useCase.doguUseCase.CheckDoguHealth(ctx) } - var componentHealth ecosystem.ComponentHealthResult - var componentHealthErr error - if !ignoreComponentHealth { - componentHealth, componentHealthErr = useCase.componentUseCase.CheckComponentHealth(ctx) - } - return ecosystem.HealthResult{ - DoguHealth: doguHealth, - ComponentHealth: componentHealth, - }, errors.Join(doguHealthErr, componentHealthErr) + DoguHealth: doguHealth, + }, doguHealthErr } diff --git a/pkg/application/ecosystemHealthUseCase_test.go b/pkg/application/ecosystemHealthUseCase_test.go index 572c544d..8f0d6c6f 100644 --- a/pkg/application/ecosystemHealthUseCase_test.go +++ b/pkg/application/ecosystemHealthUseCase_test.go @@ -1,126 +1,196 @@ package application import ( + "testing" + cescommons "github.com/cloudogu/ces-commons-lib/dogu" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/common" + "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "testing" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" ) +var ( + healthyDogu = map[ecosystem.HealthStatus][]cescommons.SimpleName{ + ecosystem.AvailableHealthStatus: {"postgresql"}, + } + mixedDoguHealth = map[ecosystem.HealthStatus][]cescommons.SimpleName{ + ecosystem.AvailableHealthStatus: {"postgresql"}, + ecosystem.UnavailableHealthStatus: {"postfix"}, + ecosystem.PendingHealthStatus: {"scm"}, + } +) + func TestNewEcosystemHealthUseCase(t *testing.T) { doguUseCase := newMockDoguInstallationUseCase(t) - componentUseCase := newMockComponentInstallationUseCase(t) - useCase := NewEcosystemHealthUseCase(doguUseCase, componentUseCase) + blueprintRepo := newMockBlueprintSpecRepository(t) + useCase := NewEcosystemHealthUseCase(doguUseCase, blueprintRepo) assert.Same(t, doguUseCase, useCase.doguUseCase) - assert.Same(t, componentUseCase, useCase.componentUseCase) } func TestEcosystemHealthUseCase_CheckEcosystemHealth(t *testing.T) { - t.Run("ok", func(t *testing.T) { - doguHealth := ecosystem.DoguHealthResult{ - DogusByStatus: map[ecosystem.HealthStatus][]cescommons.SimpleName{ - ecosystem.AvailableHealthStatus: {"postgresql"}, - ecosystem.UnavailableHealthStatus: {"postfix"}, - ecosystem.PendingHealthStatus: {"scm"}, + t.Run("all healthy", func(t *testing.T) { + blueprint := &domain.BlueprintSpec{ + Conditions: []domain.Condition{}, + Config: domain.BlueprintConfiguration{ + IgnoreDoguHealth: false, }, } - componentHealth := ecosystem.ComponentHealthResult{ - ComponentsByStatus: map[ecosystem.HealthStatus][]common.SimpleComponentName{ - ecosystem.NotInstalledHealthStatus: {"k8s-dogu-operator"}, - ecosystem.UnavailableHealthStatus: {"k8s-etcd"}, - ecosystem.PendingHealthStatus: {"k8s-service-discovery"}, - ecosystem.AvailableHealthStatus: {"k8s-component-operator"}, - }, + + doguHealth := ecosystem.DoguHealthResult{ + DogusByStatus: healthyDogu, } doguUseCase := newMockDoguInstallationUseCase(t) doguUseCase.EXPECT().CheckDoguHealth(mock.Anything).Return(doguHealth, nil) - componentUseCase := newMockComponentInstallationUseCase(t) - componentUseCase.EXPECT().CheckComponentHealth(testCtx).Return(componentHealth, nil) - useCase := NewEcosystemHealthUseCase(doguUseCase, componentUseCase) + blueprintRepo := newMockBlueprintSpecRepository(t) + blueprintRepo.EXPECT().Update(testCtx, blueprint).Return(nil) + useCase := NewEcosystemHealthUseCase(doguUseCase, blueprintRepo) - health, err := useCase.CheckEcosystemHealth(testCtx, false, false) + health, err := useCase.CheckEcosystemHealth(testCtx, blueprint) require.NoError(t, err) - assert.Equal(t, ecosystem.HealthResult{DoguHealth: doguHealth, ComponentHealth: componentHealth}, health) + assert.Equal(t, ecosystem.HealthResult{DoguHealth: doguHealth}, health) + assert.True(t, meta.IsStatusConditionTrue(blueprint.Conditions, domain.ConditionEcosystemHealthy)) }) - t.Run("ok, ignore dogu health", func(t *testing.T) { - componentHealth := ecosystem.ComponentHealthResult{ - ComponentsByStatus: map[ecosystem.HealthStatus][]common.SimpleComponentName{ - ecosystem.NotInstalledHealthStatus: {"k8s-dogu-operator"}, - ecosystem.UnavailableHealthStatus: {"k8s-etcd"}, - ecosystem.PendingHealthStatus: {"k8s-service-discovery"}, - ecosystem.AvailableHealthStatus: {"k8s-component-operator"}, + t.Run("unhealthy", func(t *testing.T) { + blueprint := &domain.BlueprintSpec{ + Conditions: []domain.Condition{}, + Config: domain.BlueprintConfiguration{ + IgnoreDoguHealth: false, }, } - componentUseCase := newMockComponentInstallationUseCase(t) - componentUseCase.EXPECT().CheckComponentHealth(testCtx).Return(componentHealth, nil) - useCase := NewEcosystemHealthUseCase(nil, componentUseCase) - health, err := useCase.CheckEcosystemHealth(testCtx, true, false) + doguHealth := ecosystem.DoguHealthResult{ + DogusByStatus: mixedDoguHealth, + } + doguUseCase := newMockDoguInstallationUseCase(t) + doguUseCase.EXPECT().CheckDoguHealth(mock.Anything).Return(doguHealth, nil) + blueprintRepo := newMockBlueprintSpecRepository(t) + blueprintRepo.EXPECT().Update(testCtx, blueprint).Return(nil) + useCase := NewEcosystemHealthUseCase(doguUseCase, blueprintRepo) + + health, err := useCase.CheckEcosystemHealth(testCtx, blueprint) - require.NoError(t, err) - assert.Equal(t, ecosystem.HealthResult{ComponentHealth: componentHealth}, health) + assert.Error(t, err) + assert.ErrorContains(t, err, "ecosystem is unhealthy") + assert.ErrorContains(t, err, "2 dogu(s) are unhealthy: postfix, scm") + assert.Equal(t, ecosystem.HealthResult{DoguHealth: doguHealth}, health) + assert.True(t, meta.IsStatusConditionFalse(blueprint.Conditions, domain.ConditionEcosystemHealthy)) }) - t.Run("ok, ignore component health", func(t *testing.T) { - doguHealth := ecosystem.DoguHealthResult{ - DogusByStatus: map[ecosystem.HealthStatus][]cescommons.SimpleName{ - ecosystem.AvailableHealthStatus: {"postgresql"}, - ecosystem.UnavailableHealthStatus: {"postfix"}, - ecosystem.PendingHealthStatus: {"scm"}, + t.Run("error updating blueprint", func(t *testing.T) { + blueprint := &domain.BlueprintSpec{ + Conditions: []domain.Condition{}, + Config: domain.BlueprintConfiguration{ + IgnoreDoguHealth: false, }, } + + doguHealth := ecosystem.DoguHealthResult{ + DogusByStatus: mixedDoguHealth, + } doguUseCase := newMockDoguInstallationUseCase(t) doguUseCase.EXPECT().CheckDoguHealth(mock.Anything).Return(doguHealth, nil) - useCase := NewEcosystemHealthUseCase(doguUseCase, nil) + blueprintRepo := newMockBlueprintSpecRepository(t) + blueprintRepo.EXPECT().Update(testCtx, blueprint).Return(assert.AnError) + useCase := NewEcosystemHealthUseCase(doguUseCase, blueprintRepo) - health, err := useCase.CheckEcosystemHealth(testCtx, false, true) + health, err := useCase.CheckEcosystemHealth(testCtx, blueprint) - require.NoError(t, err) - assert.Equal(t, ecosystem.HealthResult{DoguHealth: doguHealth}, health) + assert.ErrorIs(t, err, assert.AnError) + assert.ErrorContains(t, err, "could not update health condition after health check") + assert.Equal(t, ecosystem.HealthResult{}, health) }) - t.Run("error checking dogu health", func(t *testing.T) { - componentHealth := ecosystem.ComponentHealthResult{ - ComponentsByStatus: map[ecosystem.HealthStatus][]common.SimpleComponentName{ - ecosystem.NotInstalledHealthStatus: {"k8s-dogu-operator"}, - ecosystem.UnavailableHealthStatus: {"k8s-etcd"}, - ecosystem.PendingHealthStatus: {"k8s-service-discovery"}, - ecosystem.AvailableHealthStatus: {"k8s-component-operator"}, + t.Run("error getting health", func(t *testing.T) { + blueprint := &domain.BlueprintSpec{ + Conditions: []domain.Condition{}, + Config: domain.BlueprintConfiguration{ + IgnoreDoguHealth: false, }, } - componentUseCase := newMockComponentInstallationUseCase(t) - componentUseCase.EXPECT().CheckComponentHealth(testCtx).Return(componentHealth, nil) + + doguHealth := ecosystem.DoguHealthResult{} + doguUseCase := newMockDoguInstallationUseCase(t) - doguUseCase.EXPECT().CheckDoguHealth(mock.Anything).Return(ecosystem.DoguHealthResult{}, assert.AnError) - useCase := NewEcosystemHealthUseCase(doguUseCase, componentUseCase) + doguUseCase.EXPECT().CheckDoguHealth(mock.Anything).Return(doguHealth, assert.AnError) + blueprintRepo := newMockBlueprintSpecRepository(t) + blueprintRepo.EXPECT().Update(testCtx, blueprint).Return(nil) + useCase := NewEcosystemHealthUseCase(doguUseCase, blueprintRepo) - _, err := useCase.CheckEcosystemHealth(testCtx, false, false) + _, err := useCase.CheckEcosystemHealth(testCtx, blueprint) - require.ErrorIs(t, err, assert.AnError) + assert.ErrorIs(t, err, assert.AnError) + assert.True(t, meta.IsStatusConditionPresentAndEqual( + blueprint.Conditions, domain.ConditionEcosystemHealthy, metav1.ConditionUnknown, + )) }) - t.Run("error checking component health", func(t *testing.T) { - doguHealth := ecosystem.DoguHealthResult{ - DogusByStatus: map[ecosystem.HealthStatus][]cescommons.SimpleName{ - ecosystem.AvailableHealthStatus: {"postgresql"}, - ecosystem.UnavailableHealthStatus: {"postfix"}, - ecosystem.PendingHealthStatus: {"scm"}, + t.Run("no update without health change", func(t *testing.T) { + blueprint := &domain.BlueprintSpec{ + Conditions: []domain.Condition{}, + Config: domain.BlueprintConfiguration{ + IgnoreDoguHealth: false, }, } + + doguHealth := ecosystem.DoguHealthResult{ + DogusByStatus: mixedDoguHealth, + } + doguUseCase := newMockDoguInstallationUseCase(t) + doguUseCase.EXPECT().CheckDoguHealth(mock.Anything).Return(doguHealth, nil).Twice() + blueprintRepo := newMockBlueprintSpecRepository(t) + blueprintRepo.EXPECT().Update(testCtx, blueprint).Return(nil).Once() + useCase := NewEcosystemHealthUseCase(doguUseCase, blueprintRepo) + + _, err := useCase.CheckEcosystemHealth(testCtx, blueprint) + assert.ErrorContains(t, err, "ecosystem is unhealthy") + assert.True(t, meta.IsStatusConditionFalse(blueprint.Conditions, domain.ConditionEcosystemHealthy)) + _, err = useCase.CheckEcosystemHealth(testCtx, blueprint) //no repo.Update called again + assert.ErrorContains(t, err, "ecosystem is unhealthy") + assert.True(t, meta.IsStatusConditionFalse(blueprint.Conditions, domain.ConditionEcosystemHealthy)) + }) +} + +func TestEcosystemHealthUseCase_getEcosystemHealth(t *testing.T) { + t.Run("ok", func(t *testing.T) { + doguHealth := ecosystem.DoguHealthResult{ + DogusByStatus: mixedDoguHealth, + } doguUseCase := newMockDoguInstallationUseCase(t) doguUseCase.EXPECT().CheckDoguHealth(mock.Anything).Return(doguHealth, nil) - componentUseCase := newMockComponentInstallationUseCase(t) - componentUseCase.EXPECT().CheckComponentHealth(testCtx).Return(ecosystem.ComponentHealthResult{}, assert.AnError) - useCase := NewEcosystemHealthUseCase(doguUseCase, componentUseCase) + blueprintRepo := newMockBlueprintSpecRepository(t) + useCase := NewEcosystemHealthUseCase(doguUseCase, blueprintRepo) + + health, err := useCase.getEcosystemHealth(testCtx, false) + + require.NoError(t, err) + assert.Equal(t, ecosystem.HealthResult{DoguHealth: doguHealth}, health) + }) + + t.Run("ok, ignore dogu health", func(t *testing.T) { + blueprintRepo := newMockBlueprintSpecRepository(t) + useCase := NewEcosystemHealthUseCase(nil, blueprintRepo) + + health, err := useCase.getEcosystemHealth(testCtx, true) + + require.NoError(t, err) + assert.Equal(t, ecosystem.HealthResult{}, health) + }) + + t.Run("error checking dogu health", func(t *testing.T) { + doguUseCase := newMockDoguInstallationUseCase(t) + doguUseCase.EXPECT().CheckDoguHealth(mock.Anything).Return(ecosystem.DoguHealthResult{}, assert.AnError) + blueprintRepo := newMockBlueprintSpecRepository(t) + useCase := NewEcosystemHealthUseCase(doguUseCase, blueprintRepo) - _, err := useCase.CheckEcosystemHealth(testCtx, false, false) + _, err := useCase.getEcosystemHealth(testCtx, false) require.ErrorIs(t, err, assert.AnError) }) diff --git a/pkg/application/effectiveBlueprintUseCase.go b/pkg/application/effectiveBlueprintUseCase.go index b2ddf691..40ee9d48 100644 --- a/pkg/application/effectiveBlueprintUseCase.go +++ b/pkg/application/effectiveBlueprintUseCase.go @@ -3,9 +3,9 @@ package application import ( "context" "fmt" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domainservice" - "sigs.k8s.io/controller-runtime/pkg/log" + "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" + "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domainservice" ) type EffectiveBlueprintUseCase struct { @@ -20,19 +20,9 @@ func NewEffectiveBlueprintUseCase(blueprintSpecRepo domainservice.BlueprintSpecR // returns a domainservice.NotFoundError if the blueprintId does not correspond to a blueprintSpec or // a domainservice.InternalError if there is any error while loading or persisting the blueprintSpec or // a domainservice.ConflictError if there was a concurrent write. -func (useCase *EffectiveBlueprintUseCase) CalculateEffectiveBlueprint(ctx context.Context, blueprintId string) error { - logger := log.FromContext(ctx).WithName("EffectiveBlueprintUseCase.CalculateEffectiveBlueprint"). - WithValues("blueprintId", blueprintId) - - logger.Info("getting blueprint spec for effective blueprint calculation") - blueprintSpec, err := useCase.blueprintSpecRepo.GetById(ctx, blueprintId) - if err != nil { - return fmt.Errorf("cannot load blueprint spec to calculate effective blueprint: %w", err) - } - - logger.Info("calculate effective blueprint", "blueprintStatus", blueprintSpec.Status) - calcError := blueprintSpec.CalculateEffectiveBlueprint() - err = useCase.blueprintSpecRepo.Update(ctx, blueprintSpec) +func (useCase *EffectiveBlueprintUseCase) CalculateEffectiveBlueprint(ctx context.Context, blueprint *domain.BlueprintSpec) error { + calcError := blueprint.CalculateEffectiveBlueprint() + err := useCase.blueprintSpecRepo.Update(ctx, blueprint) if err != nil { return fmt.Errorf("cannot save blueprint spec after calculating the effective blueprint: %w", err) } diff --git a/pkg/application/effectiveBlueprintUseCase_test.go b/pkg/application/effectiveBlueprintUseCase_test.go index 34e51547..acae7cd0 100644 --- a/pkg/application/effectiveBlueprintUseCase_test.go +++ b/pkg/application/effectiveBlueprintUseCase_test.go @@ -13,91 +13,40 @@ import ( func TestBlueprintSpecUseCase_CalculateEffectiveBlueprint_ok(t *testing.T) { // given + blueprint := &domain.BlueprintSpec{ + Id: "testBlueprint1", + } + repoMock := newMockBlueprintSpecRepository(t) ctx := context.Background() useCase := NewEffectiveBlueprintUseCase(repoMock) - repoMock.EXPECT().GetById(ctx, "testBlueprint1").Return(&domain.BlueprintSpec{ - Id: "testBlueprint1", - Status: domain.StatusPhaseValidated, - }, nil) - repoMock.EXPECT().Update(ctx, &domain.BlueprintSpec{ - Id: "testBlueprint1", - Blueprint: domain.Blueprint{}, - BlueprintMask: domain.BlueprintMask{}, - EffectiveBlueprint: domain.EffectiveBlueprint{}, - StateDiff: domain.StateDiff{}, - Status: domain.StatusPhaseEffectiveBlueprintGenerated, - Events: []domain.Event{domain.EffectiveBlueprintCalculatedEvent{}}, - }).Return(nil) + repoMock.EXPECT().Update(ctx, blueprint).Return(nil) // when - err := useCase.CalculateEffectiveBlueprint(ctx, "testBlueprint1") + err := useCase.CalculateEffectiveBlueprint(ctx, blueprint) // then require.NoError(t, err) + assert.Equal(t, 0, len(blueprint.Events)) + assert.Equal(t, blueprint.EffectiveBlueprint, domain.EffectiveBlueprint{}) } func TestBlueprintSpecUseCase_CalculateEffectiveBlueprint_repoError(t *testing.T) { - t.Run("blueprint spec not found", func(t *testing.T) { - //given - repoMock := newMockBlueprintSpecRepository(t) - ctx := context.Background() - useCase := NewEffectiveBlueprintUseCase(repoMock) - - repoMock.EXPECT().GetById(ctx, "testBlueprint1").Return(nil, &domainservice.NotFoundError{Message: "test-error"}) - - //when - err := useCase.CalculateEffectiveBlueprint(ctx, "testBlueprint1") - - //then - require.Error(t, err) - var errorToCheck *domainservice.NotFoundError - assert.ErrorAs(t, err, &errorToCheck) - assert.ErrorContains(t, err, "cannot load blueprint spec to calculate effective blueprint: test-error") - }) - - t.Run("internal error while loading", func(t *testing.T) { - //given - repoMock := newMockBlueprintSpecRepository(t) - ctx := context.Background() - useCase := NewEffectiveBlueprintUseCase(repoMock) - - repoMock.EXPECT().GetById(ctx, "testBlueprint1").Return(nil, &domainservice.InternalError{Message: "test-error"}) - - //when - err := useCase.CalculateEffectiveBlueprint(ctx, "testBlueprint1") - - //then - require.Error(t, err) - var errorToCheck *domainservice.InternalError - assert.ErrorAs(t, err, &errorToCheck) - assert.ErrorContains(t, err, "cannot load blueprint spec to calculate effective blueprint: test-error") - }) - t.Run("cannot save", func(t *testing.T) { //given + blueprint := &domain.BlueprintSpec{ + Id: "testBlueprint1", + } + repoMock := newMockBlueprintSpecRepository(t) ctx := context.Background() useCase := NewEffectiveBlueprintUseCase(repoMock) - repoMock.EXPECT().GetById(ctx, "testBlueprint1").Return(&domain.BlueprintSpec{ - Id: "testBlueprint1", - Status: domain.StatusPhaseValidated, - }, nil) - - repoMock.EXPECT().Update(ctx, &domain.BlueprintSpec{ - Id: "testBlueprint1", - Blueprint: domain.Blueprint{}, - BlueprintMask: domain.BlueprintMask{}, - EffectiveBlueprint: domain.EffectiveBlueprint{}, - StateDiff: domain.StateDiff{}, - Status: domain.StatusPhaseEffectiveBlueprintGenerated, - Events: []domain.Event{domain.EffectiveBlueprintCalculatedEvent{}}, - }).Return(&domainservice.InternalError{Message: "test-error"}) + repoMock.EXPECT().Update(ctx, blueprint).Return(&domainservice.InternalError{Message: "test-error"}) //when - err := useCase.CalculateEffectiveBlueprint(ctx, "testBlueprint1") + err := useCase.CalculateEffectiveBlueprint(ctx, blueprint) //then require.Error(t, err) diff --git a/pkg/application/initiaiteBlueprintStatusUseCase.go b/pkg/application/initiaiteBlueprintStatusUseCase.go new file mode 100644 index 00000000..b1989824 --- /dev/null +++ b/pkg/application/initiaiteBlueprintStatusUseCase.go @@ -0,0 +1,49 @@ +package application + +import ( + "context" + "fmt" + + "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// InitiateBlueprintStatusUseCase contains all use cases which are needed to initiate the +// blueprint status after the determining the state diff. +// +// Use cases: +// - InitateConditions +type InitiateBlueprintStatusUseCase struct { + repo blueprintSpecRepository +} + +func NewInitiateBlueprintStatusUseCase( + repo blueprintSpecRepository, +) *InitiateBlueprintStatusUseCase { + return &InitiateBlueprintStatusUseCase{ + repo: repo, + } +} + +// InitateConditions handles the initial setting of the conditions to unknown if they are not set yet. +// returns a domainservice.InternalError on any error. +func (useCase *InitiateBlueprintStatusUseCase) InitateConditions(ctx context.Context, blueprint *domain.BlueprintSpec) error { + if len(blueprint.Conditions) != len(domain.BlueprintConditions) { + for _, condition := range domain.BlueprintConditions { + if meta.FindStatusCondition(blueprint.Conditions, condition) == nil { + meta.SetStatusCondition(&blueprint.Conditions, metav1.Condition{ + Type: condition, + Status: metav1.ConditionUnknown, + Reason: "InitialSyncPending", + Message: "controller has not determined this condition yet", + }) + } + } + err := useCase.repo.Update(ctx, blueprint) + if err != nil { + return fmt.Errorf("cannot save blueprint spec %q after initially setting the conditions to unknown: %w", blueprint.Id, err) + } + } + return nil +} diff --git a/pkg/application/initiaiteBlueprintStatusUseCase_test.go b/pkg/application/initiaiteBlueprintStatusUseCase_test.go new file mode 100644 index 00000000..1071612a --- /dev/null +++ b/pkg/application/initiaiteBlueprintStatusUseCase_test.go @@ -0,0 +1,161 @@ +package application + +import ( + "context" + "testing" + + "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestInitiateBlueprintStatusUseCase_InitateConditions(t *testing.T) { + //type expectFn func(m *mockBlueprintSpecRepository) *mock.Call + type args struct { + blueprint *domain.BlueprintSpec + } + tests := []struct { + name string + args args + wantUnknownConditions []string + wantErr error + }{ + { + name: "nil conditions", + args: args{ + blueprint: &domain.BlueprintSpec{ + Conditions: nil, + }, + }, + wantUnknownConditions: domain.BlueprintConditions, + wantErr: nil, + }, + { + name: "empty conditions", + args: args{ + blueprint: &domain.BlueprintSpec{ + Conditions: []domain.Condition{}, + }, + }, + wantUnknownConditions: domain.BlueprintConditions, + wantErr: nil, + }, + { + name: "some conditions", + args: args{ + blueprint: &domain.BlueprintSpec{ + Conditions: []domain.Condition{ + { + Type: domain.ConditionValid, + }, + { + Type: domain.ConditionLastApplySucceeded, + }, + }, + }, + }, + wantUnknownConditions: []string{domain.ConditionExecutable, domain.ConditionEcosystemHealthy, domain.ConditionCompleted}, + wantErr: nil, + }, + { + name: "all conditions", + args: args{ + blueprint: &domain.BlueprintSpec{ + Conditions: []domain.Condition{ + { + Type: domain.ConditionValid, + }, + { + Type: domain.ConditionExecutable, + }, + { + Type: domain.ConditionEcosystemHealthy, + }, + { + Type: domain.ConditionCompleted, + }, + { + Type: domain.ConditionLastApplySucceeded, + }, + }, + }, + }, + wantUnknownConditions: nil, + wantErr: nil, + }, + { + name: "update error", + args: args{ + blueprint: &domain.BlueprintSpec{ + Conditions: nil, + }, + }, + wantUnknownConditions: domain.BlueprintConditions, + wantErr: assert.AnError, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.TODO() + repoMock := newMockBlueprintSpecRepository(t) + if len(tt.wantUnknownConditions) > 0 { + repoMock.EXPECT().Update(ctx, mock.AnythingOfType("*domain.BlueprintSpec")).RunAndReturn(func(ctx context.Context, bp *domain.BlueprintSpec) error { + assert.Len(t, bp.Conditions, len(domain.BlueprintConditions)) + for _, condition := range tt.wantUnknownConditions { + assert.True(t, meta.IsStatusConditionPresentAndEqual(bp.Conditions, condition, metav1.ConditionUnknown)) + bpCondition := meta.FindStatusCondition(bp.Conditions, condition) + assert.Equal(t, "InitialSyncPending", bpCondition.Reason) + assert.Equal(t, "controller has not determined this condition yet", bpCondition.Message) + } + return tt.wantErr + }) + } + + useCase := &InitiateBlueprintStatusUseCase{ + repo: repoMock, + } + err := useCase.InitateConditions(ctx, tt.args.blueprint) + if tt.wantErr != nil { + assert.ErrorIs(t, err, tt.wantErr) + assert.ErrorContains(t, err, "cannot save blueprint spec \"\" after initially setting the conditions to unknown") + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestNewInitiateBlueprintStatusUseCase(t *testing.T) { + type args struct { + repo blueprintSpecRepository + } + tests := []struct { + name string + args args + want *InitiateBlueprintStatusUseCase + }{ + { + name: "nil repo", + args: args{ + repo: nil, + }, + want: &InitiateBlueprintStatusUseCase{}, + }, + { + name: "repo", + args: args{ + repo: &mockBlueprintSpecRepository{}, + }, + want: &InitiateBlueprintStatusUseCase{ + repo: &mockBlueprintSpecRepository{}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, NewInitiateBlueprintStatusUseCase(tt.args.repo), "NewInitiateBlueprintStatusUseCase(%v)", tt.args.repo) + }) + } +} diff --git a/pkg/application/interfaces.go b/pkg/application/interfaces.go index 54cae7c6..6f52d3b0 100644 --- a/pkg/application/interfaces.go +++ b/pkg/application/interfaces.go @@ -2,90 +2,66 @@ package application import ( "context" + + cescommons "github.com/cloudogu/ces-commons-lib/dogu" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domainservice" ) +type initialBlueprintStatusUseCase interface { + InitateConditions(ctx context.Context, blueprint *domain.BlueprintSpec) error +} + type blueprintSpecValidationUseCase interface { - ValidateBlueprintSpecStatically(ctx context.Context, blueprintId string) error - ValidateBlueprintSpecDynamically(ctx context.Context, blueprintId string) error + ValidateBlueprintSpecStatically(ctx context.Context, blueprint *domain.BlueprintSpec) error + ValidateBlueprintSpecDynamically(ctx context.Context, blueprint *domain.BlueprintSpec) error } type effectiveBlueprintUseCase interface { - CalculateEffectiveBlueprint(ctx context.Context, blueprintId string) error + CalculateEffectiveBlueprint(ctx context.Context, blueprint *domain.BlueprintSpec) error } type stateDiffUseCase interface { - DetermineStateDiff(ctx context.Context, blueprintId string) error + DetermineStateDiff(ctx context.Context, blueprint *domain.BlueprintSpec) error } type doguInstallationUseCase interface { CheckDoguHealth(ctx context.Context) (ecosystem.DoguHealthResult, error) - WaitForHealthyDogus(ctx context.Context) (ecosystem.DoguHealthResult, error) - ApplyDoguStates(ctx context.Context, blueprintId string) error -} - -type doguRestartUseCase interface { - TriggerDoguRestarts(ctx context.Context, blueprintid string) error + CheckDogusUpToDate(ctx context.Context) ([]cescommons.SimpleName, error) + ApplyDoguStates(ctx context.Context, blueprint *domain.BlueprintSpec) error } -type componentInstallationUseCase interface { - ApplyComponentStates(ctx context.Context, blueprintId string) error - CheckComponentHealth(ctx context.Context) (ecosystem.ComponentHealthResult, error) - WaitForHealthyComponents(ctx context.Context) (ecosystem.ComponentHealthResult, error) - applyComponentState(context.Context, domain.ComponentDiff, *ecosystem.ComponentInstallation) error +type applyDogusUseCase interface { + ApplyDogus(ctx context.Context, blueprint *domain.BlueprintSpec) (bool, error) } -type applyBlueprintSpecUseCase interface { - CheckEcosystemHealthUpfront(ctx context.Context, blueprintId string) error - CheckEcosystemHealthAfterwards(ctx context.Context, blueprintId string) error - PreProcessBlueprintApplication(ctx context.Context, blueprintId string) error - PostProcessBlueprintApplication(ctx context.Context, blueprintId string) error - ApplyBlueprintSpec(ctx context.Context, blueprintId string) error +type completeBlueprintUseCase interface { + CompleteBlueprint(ctx context.Context, blueprint *domain.BlueprintSpec) error } type ecosystemHealthUseCase interface { - CheckEcosystemHealth(ctx context.Context, ignoreDoguHealth bool, ignoreComponentHealth bool) (ecosystem.HealthResult, error) + CheckEcosystemHealth(context.Context, *domain.BlueprintSpec) (ecosystem.HealthResult, error) } -type selfUpgradeUseCase interface { - HandleSelfUpgrade(ctx context.Context, blueprintId string) error +type dogusUpToDateUseCase interface { + CheckDogus(ctx context.Context, blueprint *domain.BlueprintSpec) error } type ecosystemConfigUseCase interface { - ApplyConfig(ctx context.Context, blueprintId string) error + ApplyConfig(ctx context.Context, blueprint *domain.BlueprintSpec) error } type doguInstallationRepository interface { domainservice.DoguInstallationRepository } -//nolint:unused -//goland:noinspection GoUnusedType -type componentInstallationRepository interface { - domainservice.ComponentInstallationRepository -} - //nolint:unused //goland:noinspection GoUnusedType type blueprintSpecRepository interface { domainservice.BlueprintSpecRepository } -type requiredComponentsProvider interface { - domainservice.RequiredComponentsProvider -} - -type healthWaitConfigProvider interface { - domainservice.HealthWaitConfigProvider -} - -type healthConfigProvider interface { - requiredComponentsProvider - healthWaitConfigProvider -} - // interface duplication for mocks //nolint:unused @@ -110,10 +86,6 @@ type sensitiveConfigRefReader interface { domainservice.SensitiveConfigRefReader } -type doguRestartRepository interface { - domainservice.DoguRestartRepository -} - // validateDependenciesDomainUseCase is an interface for the domain service for better testability type validateDependenciesDomainUseCase interface { ValidateDependenciesForAllDogus(ctx context.Context, effectiveBlueprint domain.EffectiveBlueprint) error diff --git a/pkg/application/mock_applyBlueprintSpecUseCase_test.go b/pkg/application/mock_applyBlueprintSpecUseCase_test.go deleted file mode 100644 index 70d780c8..00000000 --- a/pkg/application/mock_applyBlueprintSpecUseCase_test.go +++ /dev/null @@ -1,271 +0,0 @@ -// Code generated by mockery v2.53.3. DO NOT EDIT. - -package application - -import ( - context "context" - - mock "github.com/stretchr/testify/mock" -) - -// mockApplyBlueprintSpecUseCase is an autogenerated mock type for the applyBlueprintSpecUseCase type -type mockApplyBlueprintSpecUseCase struct { - mock.Mock -} - -type mockApplyBlueprintSpecUseCase_Expecter struct { - mock *mock.Mock -} - -func (_m *mockApplyBlueprintSpecUseCase) EXPECT() *mockApplyBlueprintSpecUseCase_Expecter { - return &mockApplyBlueprintSpecUseCase_Expecter{mock: &_m.Mock} -} - -// ApplyBlueprintSpec provides a mock function with given fields: ctx, blueprintId -func (_m *mockApplyBlueprintSpecUseCase) ApplyBlueprintSpec(ctx context.Context, blueprintId string) error { - ret := _m.Called(ctx, blueprintId) - - if len(ret) == 0 { - panic("no return value specified for ApplyBlueprintSpec") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { - r0 = rf(ctx, blueprintId) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// mockApplyBlueprintSpecUseCase_ApplyBlueprintSpec_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ApplyBlueprintSpec' -type mockApplyBlueprintSpecUseCase_ApplyBlueprintSpec_Call struct { - *mock.Call -} - -// ApplyBlueprintSpec is a helper method to define mock.On call -// - ctx context.Context -// - blueprintId string -func (_e *mockApplyBlueprintSpecUseCase_Expecter) ApplyBlueprintSpec(ctx interface{}, blueprintId interface{}) *mockApplyBlueprintSpecUseCase_ApplyBlueprintSpec_Call { - return &mockApplyBlueprintSpecUseCase_ApplyBlueprintSpec_Call{Call: _e.mock.On("ApplyBlueprintSpec", ctx, blueprintId)} -} - -func (_c *mockApplyBlueprintSpecUseCase_ApplyBlueprintSpec_Call) Run(run func(ctx context.Context, blueprintId string)) *mockApplyBlueprintSpecUseCase_ApplyBlueprintSpec_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string)) - }) - return _c -} - -func (_c *mockApplyBlueprintSpecUseCase_ApplyBlueprintSpec_Call) Return(_a0 error) *mockApplyBlueprintSpecUseCase_ApplyBlueprintSpec_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *mockApplyBlueprintSpecUseCase_ApplyBlueprintSpec_Call) RunAndReturn(run func(context.Context, string) error) *mockApplyBlueprintSpecUseCase_ApplyBlueprintSpec_Call { - _c.Call.Return(run) - return _c -} - -// CheckEcosystemHealthAfterwards provides a mock function with given fields: ctx, blueprintId -func (_m *mockApplyBlueprintSpecUseCase) CheckEcosystemHealthAfterwards(ctx context.Context, blueprintId string) error { - ret := _m.Called(ctx, blueprintId) - - if len(ret) == 0 { - panic("no return value specified for CheckEcosystemHealthAfterwards") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { - r0 = rf(ctx, blueprintId) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// mockApplyBlueprintSpecUseCase_CheckEcosystemHealthAfterwards_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CheckEcosystemHealthAfterwards' -type mockApplyBlueprintSpecUseCase_CheckEcosystemHealthAfterwards_Call struct { - *mock.Call -} - -// CheckEcosystemHealthAfterwards is a helper method to define mock.On call -// - ctx context.Context -// - blueprintId string -func (_e *mockApplyBlueprintSpecUseCase_Expecter) CheckEcosystemHealthAfterwards(ctx interface{}, blueprintId interface{}) *mockApplyBlueprintSpecUseCase_CheckEcosystemHealthAfterwards_Call { - return &mockApplyBlueprintSpecUseCase_CheckEcosystemHealthAfterwards_Call{Call: _e.mock.On("CheckEcosystemHealthAfterwards", ctx, blueprintId)} -} - -func (_c *mockApplyBlueprintSpecUseCase_CheckEcosystemHealthAfterwards_Call) Run(run func(ctx context.Context, blueprintId string)) *mockApplyBlueprintSpecUseCase_CheckEcosystemHealthAfterwards_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string)) - }) - return _c -} - -func (_c *mockApplyBlueprintSpecUseCase_CheckEcosystemHealthAfterwards_Call) Return(_a0 error) *mockApplyBlueprintSpecUseCase_CheckEcosystemHealthAfterwards_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *mockApplyBlueprintSpecUseCase_CheckEcosystemHealthAfterwards_Call) RunAndReturn(run func(context.Context, string) error) *mockApplyBlueprintSpecUseCase_CheckEcosystemHealthAfterwards_Call { - _c.Call.Return(run) - return _c -} - -// CheckEcosystemHealthUpfront provides a mock function with given fields: ctx, blueprintId -func (_m *mockApplyBlueprintSpecUseCase) CheckEcosystemHealthUpfront(ctx context.Context, blueprintId string) error { - ret := _m.Called(ctx, blueprintId) - - if len(ret) == 0 { - panic("no return value specified for CheckEcosystemHealthUpfront") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { - r0 = rf(ctx, blueprintId) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// mockApplyBlueprintSpecUseCase_CheckEcosystemHealthUpfront_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CheckEcosystemHealthUpfront' -type mockApplyBlueprintSpecUseCase_CheckEcosystemHealthUpfront_Call struct { - *mock.Call -} - -// CheckEcosystemHealthUpfront is a helper method to define mock.On call -// - ctx context.Context -// - blueprintId string -func (_e *mockApplyBlueprintSpecUseCase_Expecter) CheckEcosystemHealthUpfront(ctx interface{}, blueprintId interface{}) *mockApplyBlueprintSpecUseCase_CheckEcosystemHealthUpfront_Call { - return &mockApplyBlueprintSpecUseCase_CheckEcosystemHealthUpfront_Call{Call: _e.mock.On("CheckEcosystemHealthUpfront", ctx, blueprintId)} -} - -func (_c *mockApplyBlueprintSpecUseCase_CheckEcosystemHealthUpfront_Call) Run(run func(ctx context.Context, blueprintId string)) *mockApplyBlueprintSpecUseCase_CheckEcosystemHealthUpfront_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string)) - }) - return _c -} - -func (_c *mockApplyBlueprintSpecUseCase_CheckEcosystemHealthUpfront_Call) Return(_a0 error) *mockApplyBlueprintSpecUseCase_CheckEcosystemHealthUpfront_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *mockApplyBlueprintSpecUseCase_CheckEcosystemHealthUpfront_Call) RunAndReturn(run func(context.Context, string) error) *mockApplyBlueprintSpecUseCase_CheckEcosystemHealthUpfront_Call { - _c.Call.Return(run) - return _c -} - -// PostProcessBlueprintApplication provides a mock function with given fields: ctx, blueprintId -func (_m *mockApplyBlueprintSpecUseCase) PostProcessBlueprintApplication(ctx context.Context, blueprintId string) error { - ret := _m.Called(ctx, blueprintId) - - if len(ret) == 0 { - panic("no return value specified for PostProcessBlueprintApplication") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { - r0 = rf(ctx, blueprintId) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// mockApplyBlueprintSpecUseCase_PostProcessBlueprintApplication_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PostProcessBlueprintApplication' -type mockApplyBlueprintSpecUseCase_PostProcessBlueprintApplication_Call struct { - *mock.Call -} - -// PostProcessBlueprintApplication is a helper method to define mock.On call -// - ctx context.Context -// - blueprintId string -func (_e *mockApplyBlueprintSpecUseCase_Expecter) PostProcessBlueprintApplication(ctx interface{}, blueprintId interface{}) *mockApplyBlueprintSpecUseCase_PostProcessBlueprintApplication_Call { - return &mockApplyBlueprintSpecUseCase_PostProcessBlueprintApplication_Call{Call: _e.mock.On("PostProcessBlueprintApplication", ctx, blueprintId)} -} - -func (_c *mockApplyBlueprintSpecUseCase_PostProcessBlueprintApplication_Call) Run(run func(ctx context.Context, blueprintId string)) *mockApplyBlueprintSpecUseCase_PostProcessBlueprintApplication_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string)) - }) - return _c -} - -func (_c *mockApplyBlueprintSpecUseCase_PostProcessBlueprintApplication_Call) Return(_a0 error) *mockApplyBlueprintSpecUseCase_PostProcessBlueprintApplication_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *mockApplyBlueprintSpecUseCase_PostProcessBlueprintApplication_Call) RunAndReturn(run func(context.Context, string) error) *mockApplyBlueprintSpecUseCase_PostProcessBlueprintApplication_Call { - _c.Call.Return(run) - return _c -} - -// PreProcessBlueprintApplication provides a mock function with given fields: ctx, blueprintId -func (_m *mockApplyBlueprintSpecUseCase) PreProcessBlueprintApplication(ctx context.Context, blueprintId string) error { - ret := _m.Called(ctx, blueprintId) - - if len(ret) == 0 { - panic("no return value specified for PreProcessBlueprintApplication") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { - r0 = rf(ctx, blueprintId) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// mockApplyBlueprintSpecUseCase_PreProcessBlueprintApplication_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PreProcessBlueprintApplication' -type mockApplyBlueprintSpecUseCase_PreProcessBlueprintApplication_Call struct { - *mock.Call -} - -// PreProcessBlueprintApplication is a helper method to define mock.On call -// - ctx context.Context -// - blueprintId string -func (_e *mockApplyBlueprintSpecUseCase_Expecter) PreProcessBlueprintApplication(ctx interface{}, blueprintId interface{}) *mockApplyBlueprintSpecUseCase_PreProcessBlueprintApplication_Call { - return &mockApplyBlueprintSpecUseCase_PreProcessBlueprintApplication_Call{Call: _e.mock.On("PreProcessBlueprintApplication", ctx, blueprintId)} -} - -func (_c *mockApplyBlueprintSpecUseCase_PreProcessBlueprintApplication_Call) Run(run func(ctx context.Context, blueprintId string)) *mockApplyBlueprintSpecUseCase_PreProcessBlueprintApplication_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string)) - }) - return _c -} - -func (_c *mockApplyBlueprintSpecUseCase_PreProcessBlueprintApplication_Call) Return(_a0 error) *mockApplyBlueprintSpecUseCase_PreProcessBlueprintApplication_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *mockApplyBlueprintSpecUseCase_PreProcessBlueprintApplication_Call) RunAndReturn(run func(context.Context, string) error) *mockApplyBlueprintSpecUseCase_PreProcessBlueprintApplication_Call { - _c.Call.Return(run) - return _c -} - -// newMockApplyBlueprintSpecUseCase creates a new instance of mockApplyBlueprintSpecUseCase. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func newMockApplyBlueprintSpecUseCase(t interface { - mock.TestingT - Cleanup(func()) -}) *mockApplyBlueprintSpecUseCase { - mock := &mockApplyBlueprintSpecUseCase{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/application/mock_applyDogusUseCase_test.go b/pkg/application/mock_applyDogusUseCase_test.go new file mode 100644 index 00000000..151e7b47 --- /dev/null +++ b/pkg/application/mock_applyDogusUseCase_test.go @@ -0,0 +1,94 @@ +// Code generated by mockery v2.53.3. DO NOT EDIT. + +package application + +import ( + context "context" + + domain "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" + mock "github.com/stretchr/testify/mock" +) + +// mockApplyDogusUseCase is an autogenerated mock type for the applyDogusUseCase type +type mockApplyDogusUseCase struct { + mock.Mock +} + +type mockApplyDogusUseCase_Expecter struct { + mock *mock.Mock +} + +func (_m *mockApplyDogusUseCase) EXPECT() *mockApplyDogusUseCase_Expecter { + return &mockApplyDogusUseCase_Expecter{mock: &_m.Mock} +} + +// ApplyDogus provides a mock function with given fields: ctx, blueprint +func (_m *mockApplyDogusUseCase) ApplyDogus(ctx context.Context, blueprint *domain.BlueprintSpec) (bool, error) { + ret := _m.Called(ctx, blueprint) + + if len(ret) == 0 { + panic("no return value specified for ApplyDogus") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *domain.BlueprintSpec) (bool, error)); ok { + return rf(ctx, blueprint) + } + if rf, ok := ret.Get(0).(func(context.Context, *domain.BlueprintSpec) bool); ok { + r0 = rf(ctx, blueprint) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(context.Context, *domain.BlueprintSpec) error); ok { + r1 = rf(ctx, blueprint) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// mockApplyDogusUseCase_ApplyDogus_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ApplyDogus' +type mockApplyDogusUseCase_ApplyDogus_Call struct { + *mock.Call +} + +// ApplyDogus is a helper method to define mock.On call +// - ctx context.Context +// - blueprint *domain.BlueprintSpec +func (_e *mockApplyDogusUseCase_Expecter) ApplyDogus(ctx interface{}, blueprint interface{}) *mockApplyDogusUseCase_ApplyDogus_Call { + return &mockApplyDogusUseCase_ApplyDogus_Call{Call: _e.mock.On("ApplyDogus", ctx, blueprint)} +} + +func (_c *mockApplyDogusUseCase_ApplyDogus_Call) Run(run func(ctx context.Context, blueprint *domain.BlueprintSpec)) *mockApplyDogusUseCase_ApplyDogus_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*domain.BlueprintSpec)) + }) + return _c +} + +func (_c *mockApplyDogusUseCase_ApplyDogus_Call) Return(_a0 bool, _a1 error) *mockApplyDogusUseCase_ApplyDogus_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *mockApplyDogusUseCase_ApplyDogus_Call) RunAndReturn(run func(context.Context, *domain.BlueprintSpec) (bool, error)) *mockApplyDogusUseCase_ApplyDogus_Call { + _c.Call.Return(run) + return _c +} + +// newMockApplyDogusUseCase creates a new instance of mockApplyDogusUseCase. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func newMockApplyDogusUseCase(t interface { + mock.TestingT + Cleanup(func()) +}) *mockApplyDogusUseCase { + mock := &mockApplyDogusUseCase{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/application/mock_blueprintSpecRepository_test.go b/pkg/application/mock_blueprintSpecRepository_test.go index b6788b02..a92af831 100644 --- a/pkg/application/mock_blueprintSpecRepository_test.go +++ b/pkg/application/mock_blueprintSpecRepository_test.go @@ -22,6 +22,52 @@ func (_m *mockBlueprintSpecRepository) EXPECT() *mockBlueprintSpecRepository_Exp return &mockBlueprintSpecRepository_Expecter{mock: &_m.Mock} } +// CheckSingleton provides a mock function with given fields: ctx +func (_m *mockBlueprintSpecRepository) CheckSingleton(ctx context.Context) error { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for CheckSingleton") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(ctx) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// mockBlueprintSpecRepository_CheckSingleton_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CheckSingleton' +type mockBlueprintSpecRepository_CheckSingleton_Call struct { + *mock.Call +} + +// CheckSingleton is a helper method to define mock.On call +// - ctx context.Context +func (_e *mockBlueprintSpecRepository_Expecter) CheckSingleton(ctx interface{}) *mockBlueprintSpecRepository_CheckSingleton_Call { + return &mockBlueprintSpecRepository_CheckSingleton_Call{Call: _e.mock.On("CheckSingleton", ctx)} +} + +func (_c *mockBlueprintSpecRepository_CheckSingleton_Call) Run(run func(ctx context.Context)) *mockBlueprintSpecRepository_CheckSingleton_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *mockBlueprintSpecRepository_CheckSingleton_Call) Return(_a0 error) *mockBlueprintSpecRepository_CheckSingleton_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *mockBlueprintSpecRepository_CheckSingleton_Call) RunAndReturn(run func(context.Context) error) *mockBlueprintSpecRepository_CheckSingleton_Call { + _c.Call.Return(run) + return _c +} + // GetById provides a mock function with given fields: ctx, blueprintId func (_m *mockBlueprintSpecRepository) GetById(ctx context.Context, blueprintId string) (*domain.BlueprintSpec, error) { ret := _m.Called(ctx, blueprintId) diff --git a/pkg/application/mock_blueprintSpecValidationUseCase_test.go b/pkg/application/mock_blueprintSpecValidationUseCase_test.go index 9f317f9d..e8f12294 100644 --- a/pkg/application/mock_blueprintSpecValidationUseCase_test.go +++ b/pkg/application/mock_blueprintSpecValidationUseCase_test.go @@ -5,6 +5,7 @@ package application import ( context "context" + domain "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" mock "github.com/stretchr/testify/mock" ) @@ -21,17 +22,17 @@ func (_m *mockBlueprintSpecValidationUseCase) EXPECT() *mockBlueprintSpecValidat return &mockBlueprintSpecValidationUseCase_Expecter{mock: &_m.Mock} } -// ValidateBlueprintSpecDynamically provides a mock function with given fields: ctx, blueprintId -func (_m *mockBlueprintSpecValidationUseCase) ValidateBlueprintSpecDynamically(ctx context.Context, blueprintId string) error { - ret := _m.Called(ctx, blueprintId) +// ValidateBlueprintSpecDynamically provides a mock function with given fields: ctx, blueprint +func (_m *mockBlueprintSpecValidationUseCase) ValidateBlueprintSpecDynamically(ctx context.Context, blueprint *domain.BlueprintSpec) error { + ret := _m.Called(ctx, blueprint) if len(ret) == 0 { panic("no return value specified for ValidateBlueprintSpecDynamically") } var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { - r0 = rf(ctx, blueprintId) + if rf, ok := ret.Get(0).(func(context.Context, *domain.BlueprintSpec) error); ok { + r0 = rf(ctx, blueprint) } else { r0 = ret.Error(0) } @@ -46,14 +47,14 @@ type mockBlueprintSpecValidationUseCase_ValidateBlueprintSpecDynamically_Call st // ValidateBlueprintSpecDynamically is a helper method to define mock.On call // - ctx context.Context -// - blueprintId string -func (_e *mockBlueprintSpecValidationUseCase_Expecter) ValidateBlueprintSpecDynamically(ctx interface{}, blueprintId interface{}) *mockBlueprintSpecValidationUseCase_ValidateBlueprintSpecDynamically_Call { - return &mockBlueprintSpecValidationUseCase_ValidateBlueprintSpecDynamically_Call{Call: _e.mock.On("ValidateBlueprintSpecDynamically", ctx, blueprintId)} +// - blueprint *domain.BlueprintSpec +func (_e *mockBlueprintSpecValidationUseCase_Expecter) ValidateBlueprintSpecDynamically(ctx interface{}, blueprint interface{}) *mockBlueprintSpecValidationUseCase_ValidateBlueprintSpecDynamically_Call { + return &mockBlueprintSpecValidationUseCase_ValidateBlueprintSpecDynamically_Call{Call: _e.mock.On("ValidateBlueprintSpecDynamically", ctx, blueprint)} } -func (_c *mockBlueprintSpecValidationUseCase_ValidateBlueprintSpecDynamically_Call) Run(run func(ctx context.Context, blueprintId string)) *mockBlueprintSpecValidationUseCase_ValidateBlueprintSpecDynamically_Call { +func (_c *mockBlueprintSpecValidationUseCase_ValidateBlueprintSpecDynamically_Call) Run(run func(ctx context.Context, blueprint *domain.BlueprintSpec)) *mockBlueprintSpecValidationUseCase_ValidateBlueprintSpecDynamically_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string)) + run(args[0].(context.Context), args[1].(*domain.BlueprintSpec)) }) return _c } @@ -63,22 +64,22 @@ func (_c *mockBlueprintSpecValidationUseCase_ValidateBlueprintSpecDynamically_Ca return _c } -func (_c *mockBlueprintSpecValidationUseCase_ValidateBlueprintSpecDynamically_Call) RunAndReturn(run func(context.Context, string) error) *mockBlueprintSpecValidationUseCase_ValidateBlueprintSpecDynamically_Call { +func (_c *mockBlueprintSpecValidationUseCase_ValidateBlueprintSpecDynamically_Call) RunAndReturn(run func(context.Context, *domain.BlueprintSpec) error) *mockBlueprintSpecValidationUseCase_ValidateBlueprintSpecDynamically_Call { _c.Call.Return(run) return _c } -// ValidateBlueprintSpecStatically provides a mock function with given fields: ctx, blueprintId -func (_m *mockBlueprintSpecValidationUseCase) ValidateBlueprintSpecStatically(ctx context.Context, blueprintId string) error { - ret := _m.Called(ctx, blueprintId) +// ValidateBlueprintSpecStatically provides a mock function with given fields: ctx, blueprint +func (_m *mockBlueprintSpecValidationUseCase) ValidateBlueprintSpecStatically(ctx context.Context, blueprint *domain.BlueprintSpec) error { + ret := _m.Called(ctx, blueprint) if len(ret) == 0 { panic("no return value specified for ValidateBlueprintSpecStatically") } var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { - r0 = rf(ctx, blueprintId) + if rf, ok := ret.Get(0).(func(context.Context, *domain.BlueprintSpec) error); ok { + r0 = rf(ctx, blueprint) } else { r0 = ret.Error(0) } @@ -93,14 +94,14 @@ type mockBlueprintSpecValidationUseCase_ValidateBlueprintSpecStatically_Call str // ValidateBlueprintSpecStatically is a helper method to define mock.On call // - ctx context.Context -// - blueprintId string -func (_e *mockBlueprintSpecValidationUseCase_Expecter) ValidateBlueprintSpecStatically(ctx interface{}, blueprintId interface{}) *mockBlueprintSpecValidationUseCase_ValidateBlueprintSpecStatically_Call { - return &mockBlueprintSpecValidationUseCase_ValidateBlueprintSpecStatically_Call{Call: _e.mock.On("ValidateBlueprintSpecStatically", ctx, blueprintId)} +// - blueprint *domain.BlueprintSpec +func (_e *mockBlueprintSpecValidationUseCase_Expecter) ValidateBlueprintSpecStatically(ctx interface{}, blueprint interface{}) *mockBlueprintSpecValidationUseCase_ValidateBlueprintSpecStatically_Call { + return &mockBlueprintSpecValidationUseCase_ValidateBlueprintSpecStatically_Call{Call: _e.mock.On("ValidateBlueprintSpecStatically", ctx, blueprint)} } -func (_c *mockBlueprintSpecValidationUseCase_ValidateBlueprintSpecStatically_Call) Run(run func(ctx context.Context, blueprintId string)) *mockBlueprintSpecValidationUseCase_ValidateBlueprintSpecStatically_Call { +func (_c *mockBlueprintSpecValidationUseCase_ValidateBlueprintSpecStatically_Call) Run(run func(ctx context.Context, blueprint *domain.BlueprintSpec)) *mockBlueprintSpecValidationUseCase_ValidateBlueprintSpecStatically_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string)) + run(args[0].(context.Context), args[1].(*domain.BlueprintSpec)) }) return _c } @@ -110,7 +111,7 @@ func (_c *mockBlueprintSpecValidationUseCase_ValidateBlueprintSpecStatically_Cal return _c } -func (_c *mockBlueprintSpecValidationUseCase_ValidateBlueprintSpecStatically_Call) RunAndReturn(run func(context.Context, string) error) *mockBlueprintSpecValidationUseCase_ValidateBlueprintSpecStatically_Call { +func (_c *mockBlueprintSpecValidationUseCase_ValidateBlueprintSpecStatically_Call) RunAndReturn(run func(context.Context, *domain.BlueprintSpec) error) *mockBlueprintSpecValidationUseCase_ValidateBlueprintSpecStatically_Call { _c.Call.Return(run) return _c } diff --git a/pkg/application/mock_completeBlueprintUseCase_test.go b/pkg/application/mock_completeBlueprintUseCase_test.go new file mode 100644 index 00000000..f52c4837 --- /dev/null +++ b/pkg/application/mock_completeBlueprintUseCase_test.go @@ -0,0 +1,84 @@ +// Code generated by mockery v2.53.3. DO NOT EDIT. + +package application + +import ( + context "context" + + domain "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" + mock "github.com/stretchr/testify/mock" +) + +// mockCompleteBlueprintUseCase is an autogenerated mock type for the completeBlueprintUseCase type +type mockCompleteBlueprintUseCase struct { + mock.Mock +} + +type mockCompleteBlueprintUseCase_Expecter struct { + mock *mock.Mock +} + +func (_m *mockCompleteBlueprintUseCase) EXPECT() *mockCompleteBlueprintUseCase_Expecter { + return &mockCompleteBlueprintUseCase_Expecter{mock: &_m.Mock} +} + +// CompleteBlueprint provides a mock function with given fields: ctx, blueprint +func (_m *mockCompleteBlueprintUseCase) CompleteBlueprint(ctx context.Context, blueprint *domain.BlueprintSpec) error { + ret := _m.Called(ctx, blueprint) + + if len(ret) == 0 { + panic("no return value specified for CompleteBlueprint") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *domain.BlueprintSpec) error); ok { + r0 = rf(ctx, blueprint) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// mockCompleteBlueprintUseCase_CompleteBlueprint_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CompleteBlueprint' +type mockCompleteBlueprintUseCase_CompleteBlueprint_Call struct { + *mock.Call +} + +// CompleteBlueprint is a helper method to define mock.On call +// - ctx context.Context +// - blueprint *domain.BlueprintSpec +func (_e *mockCompleteBlueprintUseCase_Expecter) CompleteBlueprint(ctx interface{}, blueprint interface{}) *mockCompleteBlueprintUseCase_CompleteBlueprint_Call { + return &mockCompleteBlueprintUseCase_CompleteBlueprint_Call{Call: _e.mock.On("CompleteBlueprint", ctx, blueprint)} +} + +func (_c *mockCompleteBlueprintUseCase_CompleteBlueprint_Call) Run(run func(ctx context.Context, blueprint *domain.BlueprintSpec)) *mockCompleteBlueprintUseCase_CompleteBlueprint_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*domain.BlueprintSpec)) + }) + return _c +} + +func (_c *mockCompleteBlueprintUseCase_CompleteBlueprint_Call) Return(_a0 error) *mockCompleteBlueprintUseCase_CompleteBlueprint_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *mockCompleteBlueprintUseCase_CompleteBlueprint_Call) RunAndReturn(run func(context.Context, *domain.BlueprintSpec) error) *mockCompleteBlueprintUseCase_CompleteBlueprint_Call { + _c.Call.Return(run) + return _c +} + +// newMockCompleteBlueprintUseCase creates a new instance of mockCompleteBlueprintUseCase. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func newMockCompleteBlueprintUseCase(t interface { + mock.TestingT + Cleanup(func()) +}) *mockCompleteBlueprintUseCase { + mock := &mockCompleteBlueprintUseCase{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/application/mock_componentInstallationRepository_test.go b/pkg/application/mock_componentInstallationRepository_test.go deleted file mode 100644 index d5be824a..00000000 --- a/pkg/application/mock_componentInstallationRepository_test.go +++ /dev/null @@ -1,298 +0,0 @@ -// Code generated by mockery v2.53.3. DO NOT EDIT. - -package application - -import ( - context "context" - - common "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/common" - - ecosystem "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" - - mock "github.com/stretchr/testify/mock" -) - -// mockComponentInstallationRepository is an autogenerated mock type for the componentInstallationRepository type -type mockComponentInstallationRepository struct { - mock.Mock -} - -type mockComponentInstallationRepository_Expecter struct { - mock *mock.Mock -} - -func (_m *mockComponentInstallationRepository) EXPECT() *mockComponentInstallationRepository_Expecter { - return &mockComponentInstallationRepository_Expecter{mock: &_m.Mock} -} - -// Create provides a mock function with given fields: ctx, component -func (_m *mockComponentInstallationRepository) Create(ctx context.Context, component *ecosystem.ComponentInstallation) error { - ret := _m.Called(ctx, component) - - if len(ret) == 0 { - panic("no return value specified for Create") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *ecosystem.ComponentInstallation) error); ok { - r0 = rf(ctx, component) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// mockComponentInstallationRepository_Create_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Create' -type mockComponentInstallationRepository_Create_Call struct { - *mock.Call -} - -// Create is a helper method to define mock.On call -// - ctx context.Context -// - component *ecosystem.ComponentInstallation -func (_e *mockComponentInstallationRepository_Expecter) Create(ctx interface{}, component interface{}) *mockComponentInstallationRepository_Create_Call { - return &mockComponentInstallationRepository_Create_Call{Call: _e.mock.On("Create", ctx, component)} -} - -func (_c *mockComponentInstallationRepository_Create_Call) Run(run func(ctx context.Context, component *ecosystem.ComponentInstallation)) *mockComponentInstallationRepository_Create_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(*ecosystem.ComponentInstallation)) - }) - return _c -} - -func (_c *mockComponentInstallationRepository_Create_Call) Return(_a0 error) *mockComponentInstallationRepository_Create_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *mockComponentInstallationRepository_Create_Call) RunAndReturn(run func(context.Context, *ecosystem.ComponentInstallation) error) *mockComponentInstallationRepository_Create_Call { - _c.Call.Return(run) - return _c -} - -// Delete provides a mock function with given fields: ctx, componentName -func (_m *mockComponentInstallationRepository) Delete(ctx context.Context, componentName common.SimpleComponentName) error { - ret := _m.Called(ctx, componentName) - - if len(ret) == 0 { - panic("no return value specified for Delete") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, common.SimpleComponentName) error); ok { - r0 = rf(ctx, componentName) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// mockComponentInstallationRepository_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete' -type mockComponentInstallationRepository_Delete_Call struct { - *mock.Call -} - -// Delete is a helper method to define mock.On call -// - ctx context.Context -// - componentName common.SimpleComponentName -func (_e *mockComponentInstallationRepository_Expecter) Delete(ctx interface{}, componentName interface{}) *mockComponentInstallationRepository_Delete_Call { - return &mockComponentInstallationRepository_Delete_Call{Call: _e.mock.On("Delete", ctx, componentName)} -} - -func (_c *mockComponentInstallationRepository_Delete_Call) Run(run func(ctx context.Context, componentName common.SimpleComponentName)) *mockComponentInstallationRepository_Delete_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(common.SimpleComponentName)) - }) - return _c -} - -func (_c *mockComponentInstallationRepository_Delete_Call) Return(_a0 error) *mockComponentInstallationRepository_Delete_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *mockComponentInstallationRepository_Delete_Call) RunAndReturn(run func(context.Context, common.SimpleComponentName) error) *mockComponentInstallationRepository_Delete_Call { - _c.Call.Return(run) - return _c -} - -// GetAll provides a mock function with given fields: ctx -func (_m *mockComponentInstallationRepository) GetAll(ctx context.Context) (map[common.SimpleComponentName]*ecosystem.ComponentInstallation, error) { - ret := _m.Called(ctx) - - if len(ret) == 0 { - panic("no return value specified for GetAll") - } - - var r0 map[common.SimpleComponentName]*ecosystem.ComponentInstallation - var r1 error - if rf, ok := ret.Get(0).(func(context.Context) (map[common.SimpleComponentName]*ecosystem.ComponentInstallation, error)); ok { - return rf(ctx) - } - if rf, ok := ret.Get(0).(func(context.Context) map[common.SimpleComponentName]*ecosystem.ComponentInstallation); ok { - r0 = rf(ctx) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(map[common.SimpleComponentName]*ecosystem.ComponentInstallation) - } - } - - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(ctx) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// mockComponentInstallationRepository_GetAll_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAll' -type mockComponentInstallationRepository_GetAll_Call struct { - *mock.Call -} - -// GetAll is a helper method to define mock.On call -// - ctx context.Context -func (_e *mockComponentInstallationRepository_Expecter) GetAll(ctx interface{}) *mockComponentInstallationRepository_GetAll_Call { - return &mockComponentInstallationRepository_GetAll_Call{Call: _e.mock.On("GetAll", ctx)} -} - -func (_c *mockComponentInstallationRepository_GetAll_Call) Run(run func(ctx context.Context)) *mockComponentInstallationRepository_GetAll_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context)) - }) - return _c -} - -func (_c *mockComponentInstallationRepository_GetAll_Call) Return(_a0 map[common.SimpleComponentName]*ecosystem.ComponentInstallation, _a1 error) *mockComponentInstallationRepository_GetAll_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *mockComponentInstallationRepository_GetAll_Call) RunAndReturn(run func(context.Context) (map[common.SimpleComponentName]*ecosystem.ComponentInstallation, error)) *mockComponentInstallationRepository_GetAll_Call { - _c.Call.Return(run) - return _c -} - -// GetByName provides a mock function with given fields: ctx, componentName -func (_m *mockComponentInstallationRepository) GetByName(ctx context.Context, componentName common.SimpleComponentName) (*ecosystem.ComponentInstallation, error) { - ret := _m.Called(ctx, componentName) - - if len(ret) == 0 { - panic("no return value specified for GetByName") - } - - var r0 *ecosystem.ComponentInstallation - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, common.SimpleComponentName) (*ecosystem.ComponentInstallation, error)); ok { - return rf(ctx, componentName) - } - if rf, ok := ret.Get(0).(func(context.Context, common.SimpleComponentName) *ecosystem.ComponentInstallation); ok { - r0 = rf(ctx, componentName) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*ecosystem.ComponentInstallation) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, common.SimpleComponentName) error); ok { - r1 = rf(ctx, componentName) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// mockComponentInstallationRepository_GetByName_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetByName' -type mockComponentInstallationRepository_GetByName_Call struct { - *mock.Call -} - -// GetByName is a helper method to define mock.On call -// - ctx context.Context -// - componentName common.SimpleComponentName -func (_e *mockComponentInstallationRepository_Expecter) GetByName(ctx interface{}, componentName interface{}) *mockComponentInstallationRepository_GetByName_Call { - return &mockComponentInstallationRepository_GetByName_Call{Call: _e.mock.On("GetByName", ctx, componentName)} -} - -func (_c *mockComponentInstallationRepository_GetByName_Call) Run(run func(ctx context.Context, componentName common.SimpleComponentName)) *mockComponentInstallationRepository_GetByName_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(common.SimpleComponentName)) - }) - return _c -} - -func (_c *mockComponentInstallationRepository_GetByName_Call) Return(_a0 *ecosystem.ComponentInstallation, _a1 error) *mockComponentInstallationRepository_GetByName_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *mockComponentInstallationRepository_GetByName_Call) RunAndReturn(run func(context.Context, common.SimpleComponentName) (*ecosystem.ComponentInstallation, error)) *mockComponentInstallationRepository_GetByName_Call { - _c.Call.Return(run) - return _c -} - -// Update provides a mock function with given fields: ctx, component -func (_m *mockComponentInstallationRepository) Update(ctx context.Context, component *ecosystem.ComponentInstallation) error { - ret := _m.Called(ctx, component) - - if len(ret) == 0 { - panic("no return value specified for Update") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *ecosystem.ComponentInstallation) error); ok { - r0 = rf(ctx, component) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// mockComponentInstallationRepository_Update_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Update' -type mockComponentInstallationRepository_Update_Call struct { - *mock.Call -} - -// Update is a helper method to define mock.On call -// - ctx context.Context -// - component *ecosystem.ComponentInstallation -func (_e *mockComponentInstallationRepository_Expecter) Update(ctx interface{}, component interface{}) *mockComponentInstallationRepository_Update_Call { - return &mockComponentInstallationRepository_Update_Call{Call: _e.mock.On("Update", ctx, component)} -} - -func (_c *mockComponentInstallationRepository_Update_Call) Run(run func(ctx context.Context, component *ecosystem.ComponentInstallation)) *mockComponentInstallationRepository_Update_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(*ecosystem.ComponentInstallation)) - }) - return _c -} - -func (_c *mockComponentInstallationRepository_Update_Call) Return(_a0 error) *mockComponentInstallationRepository_Update_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *mockComponentInstallationRepository_Update_Call) RunAndReturn(run func(context.Context, *ecosystem.ComponentInstallation) error) *mockComponentInstallationRepository_Update_Call { - _c.Call.Return(run) - return _c -} - -// newMockComponentInstallationRepository creates a new instance of mockComponentInstallationRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func newMockComponentInstallationRepository(t interface { - mock.TestingT - Cleanup(func()) -}) *mockComponentInstallationRepository { - mock := &mockComponentInstallationRepository{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/application/mock_componentInstallationUseCase_test.go b/pkg/application/mock_componentInstallationUseCase_test.go deleted file mode 100644 index 36452ead..00000000 --- a/pkg/application/mock_componentInstallationUseCase_test.go +++ /dev/null @@ -1,246 +0,0 @@ -// Code generated by mockery v2.53.3. DO NOT EDIT. - -package application - -import ( - context "context" - - domain "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" - ecosystem "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" - - mock "github.com/stretchr/testify/mock" -) - -// mockComponentInstallationUseCase is an autogenerated mock type for the componentInstallationUseCase type -type mockComponentInstallationUseCase struct { - mock.Mock -} - -type mockComponentInstallationUseCase_Expecter struct { - mock *mock.Mock -} - -func (_m *mockComponentInstallationUseCase) EXPECT() *mockComponentInstallationUseCase_Expecter { - return &mockComponentInstallationUseCase_Expecter{mock: &_m.Mock} -} - -// ApplyComponentStates provides a mock function with given fields: ctx, blueprintId -func (_m *mockComponentInstallationUseCase) ApplyComponentStates(ctx context.Context, blueprintId string) error { - ret := _m.Called(ctx, blueprintId) - - if len(ret) == 0 { - panic("no return value specified for ApplyComponentStates") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { - r0 = rf(ctx, blueprintId) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// mockComponentInstallationUseCase_ApplyComponentStates_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ApplyComponentStates' -type mockComponentInstallationUseCase_ApplyComponentStates_Call struct { - *mock.Call -} - -// ApplyComponentStates is a helper method to define mock.On call -// - ctx context.Context -// - blueprintId string -func (_e *mockComponentInstallationUseCase_Expecter) ApplyComponentStates(ctx interface{}, blueprintId interface{}) *mockComponentInstallationUseCase_ApplyComponentStates_Call { - return &mockComponentInstallationUseCase_ApplyComponentStates_Call{Call: _e.mock.On("ApplyComponentStates", ctx, blueprintId)} -} - -func (_c *mockComponentInstallationUseCase_ApplyComponentStates_Call) Run(run func(ctx context.Context, blueprintId string)) *mockComponentInstallationUseCase_ApplyComponentStates_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string)) - }) - return _c -} - -func (_c *mockComponentInstallationUseCase_ApplyComponentStates_Call) Return(_a0 error) *mockComponentInstallationUseCase_ApplyComponentStates_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *mockComponentInstallationUseCase_ApplyComponentStates_Call) RunAndReturn(run func(context.Context, string) error) *mockComponentInstallationUseCase_ApplyComponentStates_Call { - _c.Call.Return(run) - return _c -} - -// CheckComponentHealth provides a mock function with given fields: ctx -func (_m *mockComponentInstallationUseCase) CheckComponentHealth(ctx context.Context) (ecosystem.ComponentHealthResult, error) { - ret := _m.Called(ctx) - - if len(ret) == 0 { - panic("no return value specified for CheckComponentHealth") - } - - var r0 ecosystem.ComponentHealthResult - var r1 error - if rf, ok := ret.Get(0).(func(context.Context) (ecosystem.ComponentHealthResult, error)); ok { - return rf(ctx) - } - if rf, ok := ret.Get(0).(func(context.Context) ecosystem.ComponentHealthResult); ok { - r0 = rf(ctx) - } else { - r0 = ret.Get(0).(ecosystem.ComponentHealthResult) - } - - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(ctx) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// mockComponentInstallationUseCase_CheckComponentHealth_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CheckComponentHealth' -type mockComponentInstallationUseCase_CheckComponentHealth_Call struct { - *mock.Call -} - -// CheckComponentHealth is a helper method to define mock.On call -// - ctx context.Context -func (_e *mockComponentInstallationUseCase_Expecter) CheckComponentHealth(ctx interface{}) *mockComponentInstallationUseCase_CheckComponentHealth_Call { - return &mockComponentInstallationUseCase_CheckComponentHealth_Call{Call: _e.mock.On("CheckComponentHealth", ctx)} -} - -func (_c *mockComponentInstallationUseCase_CheckComponentHealth_Call) Run(run func(ctx context.Context)) *mockComponentInstallationUseCase_CheckComponentHealth_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context)) - }) - return _c -} - -func (_c *mockComponentInstallationUseCase_CheckComponentHealth_Call) Return(_a0 ecosystem.ComponentHealthResult, _a1 error) *mockComponentInstallationUseCase_CheckComponentHealth_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *mockComponentInstallationUseCase_CheckComponentHealth_Call) RunAndReturn(run func(context.Context) (ecosystem.ComponentHealthResult, error)) *mockComponentInstallationUseCase_CheckComponentHealth_Call { - _c.Call.Return(run) - return _c -} - -// WaitForHealthyComponents provides a mock function with given fields: ctx -func (_m *mockComponentInstallationUseCase) WaitForHealthyComponents(ctx context.Context) (ecosystem.ComponentHealthResult, error) { - ret := _m.Called(ctx) - - if len(ret) == 0 { - panic("no return value specified for WaitForHealthyComponents") - } - - var r0 ecosystem.ComponentHealthResult - var r1 error - if rf, ok := ret.Get(0).(func(context.Context) (ecosystem.ComponentHealthResult, error)); ok { - return rf(ctx) - } - if rf, ok := ret.Get(0).(func(context.Context) ecosystem.ComponentHealthResult); ok { - r0 = rf(ctx) - } else { - r0 = ret.Get(0).(ecosystem.ComponentHealthResult) - } - - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(ctx) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// mockComponentInstallationUseCase_WaitForHealthyComponents_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WaitForHealthyComponents' -type mockComponentInstallationUseCase_WaitForHealthyComponents_Call struct { - *mock.Call -} - -// WaitForHealthyComponents is a helper method to define mock.On call -// - ctx context.Context -func (_e *mockComponentInstallationUseCase_Expecter) WaitForHealthyComponents(ctx interface{}) *mockComponentInstallationUseCase_WaitForHealthyComponents_Call { - return &mockComponentInstallationUseCase_WaitForHealthyComponents_Call{Call: _e.mock.On("WaitForHealthyComponents", ctx)} -} - -func (_c *mockComponentInstallationUseCase_WaitForHealthyComponents_Call) Run(run func(ctx context.Context)) *mockComponentInstallationUseCase_WaitForHealthyComponents_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context)) - }) - return _c -} - -func (_c *mockComponentInstallationUseCase_WaitForHealthyComponents_Call) Return(_a0 ecosystem.ComponentHealthResult, _a1 error) *mockComponentInstallationUseCase_WaitForHealthyComponents_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *mockComponentInstallationUseCase_WaitForHealthyComponents_Call) RunAndReturn(run func(context.Context) (ecosystem.ComponentHealthResult, error)) *mockComponentInstallationUseCase_WaitForHealthyComponents_Call { - _c.Call.Return(run) - return _c -} - -// applyComponentState provides a mock function with given fields: _a0, _a1, _a2 -func (_m *mockComponentInstallationUseCase) applyComponentState(_a0 context.Context, _a1 domain.ComponentDiff, _a2 *ecosystem.ComponentInstallation) error { - ret := _m.Called(_a0, _a1, _a2) - - if len(ret) == 0 { - panic("no return value specified for applyComponentState") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, domain.ComponentDiff, *ecosystem.ComponentInstallation) error); ok { - r0 = rf(_a0, _a1, _a2) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// mockComponentInstallationUseCase_applyComponentState_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'applyComponentState' -type mockComponentInstallationUseCase_applyComponentState_Call struct { - *mock.Call -} - -// applyComponentState is a helper method to define mock.On call -// - _a0 context.Context -// - _a1 domain.ComponentDiff -// - _a2 *ecosystem.ComponentInstallation -func (_e *mockComponentInstallationUseCase_Expecter) applyComponentState(_a0 interface{}, _a1 interface{}, _a2 interface{}) *mockComponentInstallationUseCase_applyComponentState_Call { - return &mockComponentInstallationUseCase_applyComponentState_Call{Call: _e.mock.On("applyComponentState", _a0, _a1, _a2)} -} - -func (_c *mockComponentInstallationUseCase_applyComponentState_Call) Run(run func(_a0 context.Context, _a1 domain.ComponentDiff, _a2 *ecosystem.ComponentInstallation)) *mockComponentInstallationUseCase_applyComponentState_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(domain.ComponentDiff), args[2].(*ecosystem.ComponentInstallation)) - }) - return _c -} - -func (_c *mockComponentInstallationUseCase_applyComponentState_Call) Return(_a0 error) *mockComponentInstallationUseCase_applyComponentState_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *mockComponentInstallationUseCase_applyComponentState_Call) RunAndReturn(run func(context.Context, domain.ComponentDiff, *ecosystem.ComponentInstallation) error) *mockComponentInstallationUseCase_applyComponentState_Call { - _c.Call.Return(run) - return _c -} - -// newMockComponentInstallationUseCase creates a new instance of mockComponentInstallationUseCase. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func newMockComponentInstallationUseCase(t interface { - mock.TestingT - Cleanup(func()) -}) *mockComponentInstallationUseCase { - mock := &mockComponentInstallationUseCase{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/application/mock_doguInstallationUseCase_test.go b/pkg/application/mock_doguInstallationUseCase_test.go index 2ef1213e..b7bf07c0 100644 --- a/pkg/application/mock_doguInstallationUseCase_test.go +++ b/pkg/application/mock_doguInstallationUseCase_test.go @@ -5,7 +5,11 @@ package application import ( context "context" + dogu "github.com/cloudogu/ces-commons-lib/dogu" + domain "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" + ecosystem "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" + mock "github.com/stretchr/testify/mock" ) @@ -22,17 +26,17 @@ func (_m *mockDoguInstallationUseCase) EXPECT() *mockDoguInstallationUseCase_Exp return &mockDoguInstallationUseCase_Expecter{mock: &_m.Mock} } -// ApplyDoguStates provides a mock function with given fields: ctx, blueprintId -func (_m *mockDoguInstallationUseCase) ApplyDoguStates(ctx context.Context, blueprintId string) error { - ret := _m.Called(ctx, blueprintId) +// ApplyDoguStates provides a mock function with given fields: ctx, blueprint +func (_m *mockDoguInstallationUseCase) ApplyDoguStates(ctx context.Context, blueprint *domain.BlueprintSpec) error { + ret := _m.Called(ctx, blueprint) if len(ret) == 0 { panic("no return value specified for ApplyDoguStates") } var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { - r0 = rf(ctx, blueprintId) + if rf, ok := ret.Get(0).(func(context.Context, *domain.BlueprintSpec) error); ok { + r0 = rf(ctx, blueprint) } else { r0 = ret.Error(0) } @@ -47,14 +51,14 @@ type mockDoguInstallationUseCase_ApplyDoguStates_Call struct { // ApplyDoguStates is a helper method to define mock.On call // - ctx context.Context -// - blueprintId string -func (_e *mockDoguInstallationUseCase_Expecter) ApplyDoguStates(ctx interface{}, blueprintId interface{}) *mockDoguInstallationUseCase_ApplyDoguStates_Call { - return &mockDoguInstallationUseCase_ApplyDoguStates_Call{Call: _e.mock.On("ApplyDoguStates", ctx, blueprintId)} +// - blueprint *domain.BlueprintSpec +func (_e *mockDoguInstallationUseCase_Expecter) ApplyDoguStates(ctx interface{}, blueprint interface{}) *mockDoguInstallationUseCase_ApplyDoguStates_Call { + return &mockDoguInstallationUseCase_ApplyDoguStates_Call{Call: _e.mock.On("ApplyDoguStates", ctx, blueprint)} } -func (_c *mockDoguInstallationUseCase_ApplyDoguStates_Call) Run(run func(ctx context.Context, blueprintId string)) *mockDoguInstallationUseCase_ApplyDoguStates_Call { +func (_c *mockDoguInstallationUseCase_ApplyDoguStates_Call) Run(run func(ctx context.Context, blueprint *domain.BlueprintSpec)) *mockDoguInstallationUseCase_ApplyDoguStates_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string)) + run(args[0].(context.Context), args[1].(*domain.BlueprintSpec)) }) return _c } @@ -64,7 +68,7 @@ func (_c *mockDoguInstallationUseCase_ApplyDoguStates_Call) Return(_a0 error) *m return _c } -func (_c *mockDoguInstallationUseCase_ApplyDoguStates_Call) RunAndReturn(run func(context.Context, string) error) *mockDoguInstallationUseCase_ApplyDoguStates_Call { +func (_c *mockDoguInstallationUseCase_ApplyDoguStates_Call) RunAndReturn(run func(context.Context, *domain.BlueprintSpec) error) *mockDoguInstallationUseCase_ApplyDoguStates_Call { _c.Call.Return(run) return _c } @@ -125,23 +129,25 @@ func (_c *mockDoguInstallationUseCase_CheckDoguHealth_Call) RunAndReturn(run fun return _c } -// WaitForHealthyDogus provides a mock function with given fields: ctx -func (_m *mockDoguInstallationUseCase) WaitForHealthyDogus(ctx context.Context) (ecosystem.DoguHealthResult, error) { +// CheckDogusUpToDate provides a mock function with given fields: ctx +func (_m *mockDoguInstallationUseCase) CheckDogusUpToDate(ctx context.Context) ([]dogu.SimpleName, error) { ret := _m.Called(ctx) if len(ret) == 0 { - panic("no return value specified for WaitForHealthyDogus") + panic("no return value specified for CheckDogusUpToDate") } - var r0 ecosystem.DoguHealthResult + var r0 []dogu.SimpleName var r1 error - if rf, ok := ret.Get(0).(func(context.Context) (ecosystem.DoguHealthResult, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context) ([]dogu.SimpleName, error)); ok { return rf(ctx) } - if rf, ok := ret.Get(0).(func(context.Context) ecosystem.DoguHealthResult); ok { + if rf, ok := ret.Get(0).(func(context.Context) []dogu.SimpleName); ok { r0 = rf(ctx) } else { - r0 = ret.Get(0).(ecosystem.DoguHealthResult) + if ret.Get(0) != nil { + r0 = ret.Get(0).([]dogu.SimpleName) + } } if rf, ok := ret.Get(1).(func(context.Context) error); ok { @@ -153,30 +159,30 @@ func (_m *mockDoguInstallationUseCase) WaitForHealthyDogus(ctx context.Context) return r0, r1 } -// mockDoguInstallationUseCase_WaitForHealthyDogus_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WaitForHealthyDogus' -type mockDoguInstallationUseCase_WaitForHealthyDogus_Call struct { +// mockDoguInstallationUseCase_CheckDogusUpToDate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CheckDogusUpToDate' +type mockDoguInstallationUseCase_CheckDogusUpToDate_Call struct { *mock.Call } -// WaitForHealthyDogus is a helper method to define mock.On call +// CheckDogusUpToDate is a helper method to define mock.On call // - ctx context.Context -func (_e *mockDoguInstallationUseCase_Expecter) WaitForHealthyDogus(ctx interface{}) *mockDoguInstallationUseCase_WaitForHealthyDogus_Call { - return &mockDoguInstallationUseCase_WaitForHealthyDogus_Call{Call: _e.mock.On("WaitForHealthyDogus", ctx)} +func (_e *mockDoguInstallationUseCase_Expecter) CheckDogusUpToDate(ctx interface{}) *mockDoguInstallationUseCase_CheckDogusUpToDate_Call { + return &mockDoguInstallationUseCase_CheckDogusUpToDate_Call{Call: _e.mock.On("CheckDogusUpToDate", ctx)} } -func (_c *mockDoguInstallationUseCase_WaitForHealthyDogus_Call) Run(run func(ctx context.Context)) *mockDoguInstallationUseCase_WaitForHealthyDogus_Call { +func (_c *mockDoguInstallationUseCase_CheckDogusUpToDate_Call) Run(run func(ctx context.Context)) *mockDoguInstallationUseCase_CheckDogusUpToDate_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context)) }) return _c } -func (_c *mockDoguInstallationUseCase_WaitForHealthyDogus_Call) Return(_a0 ecosystem.DoguHealthResult, _a1 error) *mockDoguInstallationUseCase_WaitForHealthyDogus_Call { +func (_c *mockDoguInstallationUseCase_CheckDogusUpToDate_Call) Return(_a0 []dogu.SimpleName, _a1 error) *mockDoguInstallationUseCase_CheckDogusUpToDate_Call { _c.Call.Return(_a0, _a1) return _c } -func (_c *mockDoguInstallationUseCase_WaitForHealthyDogus_Call) RunAndReturn(run func(context.Context) (ecosystem.DoguHealthResult, error)) *mockDoguInstallationUseCase_WaitForHealthyDogus_Call { +func (_c *mockDoguInstallationUseCase_CheckDogusUpToDate_Call) RunAndReturn(run func(context.Context) ([]dogu.SimpleName, error)) *mockDoguInstallationUseCase_CheckDogusUpToDate_Call { _c.Call.Return(run) return _c } diff --git a/pkg/application/mock_doguRestartRepository_test.go b/pkg/application/mock_doguRestartRepository_test.go deleted file mode 100644 index 46a256fb..00000000 --- a/pkg/application/mock_doguRestartRepository_test.go +++ /dev/null @@ -1,84 +0,0 @@ -// Code generated by mockery v2.53.3. DO NOT EDIT. - -package application - -import ( - context "context" - - dogu "github.com/cloudogu/ces-commons-lib/dogu" - mock "github.com/stretchr/testify/mock" -) - -// mockDoguRestartRepository is an autogenerated mock type for the doguRestartRepository type -type mockDoguRestartRepository struct { - mock.Mock -} - -type mockDoguRestartRepository_Expecter struct { - mock *mock.Mock -} - -func (_m *mockDoguRestartRepository) EXPECT() *mockDoguRestartRepository_Expecter { - return &mockDoguRestartRepository_Expecter{mock: &_m.Mock} -} - -// RestartAll provides a mock function with given fields: _a0, _a1 -func (_m *mockDoguRestartRepository) RestartAll(_a0 context.Context, _a1 []dogu.SimpleName) error { - ret := _m.Called(_a0, _a1) - - if len(ret) == 0 { - panic("no return value specified for RestartAll") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, []dogu.SimpleName) error); ok { - r0 = rf(_a0, _a1) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// mockDoguRestartRepository_RestartAll_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RestartAll' -type mockDoguRestartRepository_RestartAll_Call struct { - *mock.Call -} - -// RestartAll is a helper method to define mock.On call -// - _a0 context.Context -// - _a1 []dogu.SimpleName -func (_e *mockDoguRestartRepository_Expecter) RestartAll(_a0 interface{}, _a1 interface{}) *mockDoguRestartRepository_RestartAll_Call { - return &mockDoguRestartRepository_RestartAll_Call{Call: _e.mock.On("RestartAll", _a0, _a1)} -} - -func (_c *mockDoguRestartRepository_RestartAll_Call) Run(run func(_a0 context.Context, _a1 []dogu.SimpleName)) *mockDoguRestartRepository_RestartAll_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].([]dogu.SimpleName)) - }) - return _c -} - -func (_c *mockDoguRestartRepository_RestartAll_Call) Return(_a0 error) *mockDoguRestartRepository_RestartAll_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *mockDoguRestartRepository_RestartAll_Call) RunAndReturn(run func(context.Context, []dogu.SimpleName) error) *mockDoguRestartRepository_RestartAll_Call { - _c.Call.Return(run) - return _c -} - -// newMockDoguRestartRepository creates a new instance of mockDoguRestartRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func newMockDoguRestartRepository(t interface { - mock.TestingT - Cleanup(func()) -}) *mockDoguRestartRepository { - mock := &mockDoguRestartRepository{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/application/mock_doguRestartUseCase_test.go b/pkg/application/mock_doguRestartUseCase_test.go deleted file mode 100644 index acdc6cd0..00000000 --- a/pkg/application/mock_doguRestartUseCase_test.go +++ /dev/null @@ -1,83 +0,0 @@ -// Code generated by mockery v2.53.3. DO NOT EDIT. - -package application - -import ( - context "context" - - mock "github.com/stretchr/testify/mock" -) - -// mockDoguRestartUseCase is an autogenerated mock type for the doguRestartUseCase type -type mockDoguRestartUseCase struct { - mock.Mock -} - -type mockDoguRestartUseCase_Expecter struct { - mock *mock.Mock -} - -func (_m *mockDoguRestartUseCase) EXPECT() *mockDoguRestartUseCase_Expecter { - return &mockDoguRestartUseCase_Expecter{mock: &_m.Mock} -} - -// TriggerDoguRestarts provides a mock function with given fields: ctx, blueprintid -func (_m *mockDoguRestartUseCase) TriggerDoguRestarts(ctx context.Context, blueprintid string) error { - ret := _m.Called(ctx, blueprintid) - - if len(ret) == 0 { - panic("no return value specified for TriggerDoguRestarts") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { - r0 = rf(ctx, blueprintid) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// mockDoguRestartUseCase_TriggerDoguRestarts_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TriggerDoguRestarts' -type mockDoguRestartUseCase_TriggerDoguRestarts_Call struct { - *mock.Call -} - -// TriggerDoguRestarts is a helper method to define mock.On call -// - ctx context.Context -// - blueprintid string -func (_e *mockDoguRestartUseCase_Expecter) TriggerDoguRestarts(ctx interface{}, blueprintid interface{}) *mockDoguRestartUseCase_TriggerDoguRestarts_Call { - return &mockDoguRestartUseCase_TriggerDoguRestarts_Call{Call: _e.mock.On("TriggerDoguRestarts", ctx, blueprintid)} -} - -func (_c *mockDoguRestartUseCase_TriggerDoguRestarts_Call) Run(run func(ctx context.Context, blueprintid string)) *mockDoguRestartUseCase_TriggerDoguRestarts_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string)) - }) - return _c -} - -func (_c *mockDoguRestartUseCase_TriggerDoguRestarts_Call) Return(_a0 error) *mockDoguRestartUseCase_TriggerDoguRestarts_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *mockDoguRestartUseCase_TriggerDoguRestarts_Call) RunAndReturn(run func(context.Context, string) error) *mockDoguRestartUseCase_TriggerDoguRestarts_Call { - _c.Call.Return(run) - return _c -} - -// newMockDoguRestartUseCase creates a new instance of mockDoguRestartUseCase. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func newMockDoguRestartUseCase(t interface { - mock.TestingT - Cleanup(func()) -}) *mockDoguRestartUseCase { - mock := &mockDoguRestartUseCase{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/application/mock_dogusUpToDateUseCase_test.go b/pkg/application/mock_dogusUpToDateUseCase_test.go new file mode 100644 index 00000000..4990227f --- /dev/null +++ b/pkg/application/mock_dogusUpToDateUseCase_test.go @@ -0,0 +1,84 @@ +// Code generated by mockery v2.53.3. DO NOT EDIT. + +package application + +import ( + context "context" + + domain "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" + mock "github.com/stretchr/testify/mock" +) + +// mockDogusUpToDateUseCase is an autogenerated mock type for the dogusUpToDateUseCase type +type mockDogusUpToDateUseCase struct { + mock.Mock +} + +type mockDogusUpToDateUseCase_Expecter struct { + mock *mock.Mock +} + +func (_m *mockDogusUpToDateUseCase) EXPECT() *mockDogusUpToDateUseCase_Expecter { + return &mockDogusUpToDateUseCase_Expecter{mock: &_m.Mock} +} + +// CheckDogus provides a mock function with given fields: ctx, blueprint +func (_m *mockDogusUpToDateUseCase) CheckDogus(ctx context.Context, blueprint *domain.BlueprintSpec) error { + ret := _m.Called(ctx, blueprint) + + if len(ret) == 0 { + panic("no return value specified for CheckDogus") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *domain.BlueprintSpec) error); ok { + r0 = rf(ctx, blueprint) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// mockDogusUpToDateUseCase_CheckDogus_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CheckDogus' +type mockDogusUpToDateUseCase_CheckDogus_Call struct { + *mock.Call +} + +// CheckDogus is a helper method to define mock.On call +// - ctx context.Context +// - blueprint *domain.BlueprintSpec +func (_e *mockDogusUpToDateUseCase_Expecter) CheckDogus(ctx interface{}, blueprint interface{}) *mockDogusUpToDateUseCase_CheckDogus_Call { + return &mockDogusUpToDateUseCase_CheckDogus_Call{Call: _e.mock.On("CheckDogus", ctx, blueprint)} +} + +func (_c *mockDogusUpToDateUseCase_CheckDogus_Call) Run(run func(ctx context.Context, blueprint *domain.BlueprintSpec)) *mockDogusUpToDateUseCase_CheckDogus_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*domain.BlueprintSpec)) + }) + return _c +} + +func (_c *mockDogusUpToDateUseCase_CheckDogus_Call) Return(_a0 error) *mockDogusUpToDateUseCase_CheckDogus_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *mockDogusUpToDateUseCase_CheckDogus_Call) RunAndReturn(run func(context.Context, *domain.BlueprintSpec) error) *mockDogusUpToDateUseCase_CheckDogus_Call { + _c.Call.Return(run) + return _c +} + +// newMockDogusUpToDateUseCase creates a new instance of mockDogusUpToDateUseCase. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func newMockDogusUpToDateUseCase(t interface { + mock.TestingT + Cleanup(func()) +}) *mockDogusUpToDateUseCase { + mock := &mockDogusUpToDateUseCase{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/application/mock_ecosystemConfigUseCase_test.go b/pkg/application/mock_ecosystemConfigUseCase_test.go index b36d1cb0..93e678ce 100644 --- a/pkg/application/mock_ecosystemConfigUseCase_test.go +++ b/pkg/application/mock_ecosystemConfigUseCase_test.go @@ -5,6 +5,7 @@ package application import ( context "context" + domain "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" mock "github.com/stretchr/testify/mock" ) @@ -21,17 +22,17 @@ func (_m *mockEcosystemConfigUseCase) EXPECT() *mockEcosystemConfigUseCase_Expec return &mockEcosystemConfigUseCase_Expecter{mock: &_m.Mock} } -// ApplyConfig provides a mock function with given fields: ctx, blueprintId -func (_m *mockEcosystemConfigUseCase) ApplyConfig(ctx context.Context, blueprintId string) error { - ret := _m.Called(ctx, blueprintId) +// ApplyConfig provides a mock function with given fields: ctx, blueprint +func (_m *mockEcosystemConfigUseCase) ApplyConfig(ctx context.Context, blueprint *domain.BlueprintSpec) error { + ret := _m.Called(ctx, blueprint) if len(ret) == 0 { panic("no return value specified for ApplyConfig") } var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { - r0 = rf(ctx, blueprintId) + if rf, ok := ret.Get(0).(func(context.Context, *domain.BlueprintSpec) error); ok { + r0 = rf(ctx, blueprint) } else { r0 = ret.Error(0) } @@ -46,14 +47,14 @@ type mockEcosystemConfigUseCase_ApplyConfig_Call struct { // ApplyConfig is a helper method to define mock.On call // - ctx context.Context -// - blueprintId string -func (_e *mockEcosystemConfigUseCase_Expecter) ApplyConfig(ctx interface{}, blueprintId interface{}) *mockEcosystemConfigUseCase_ApplyConfig_Call { - return &mockEcosystemConfigUseCase_ApplyConfig_Call{Call: _e.mock.On("ApplyConfig", ctx, blueprintId)} +// - blueprint *domain.BlueprintSpec +func (_e *mockEcosystemConfigUseCase_Expecter) ApplyConfig(ctx interface{}, blueprint interface{}) *mockEcosystemConfigUseCase_ApplyConfig_Call { + return &mockEcosystemConfigUseCase_ApplyConfig_Call{Call: _e.mock.On("ApplyConfig", ctx, blueprint)} } -func (_c *mockEcosystemConfigUseCase_ApplyConfig_Call) Run(run func(ctx context.Context, blueprintId string)) *mockEcosystemConfigUseCase_ApplyConfig_Call { +func (_c *mockEcosystemConfigUseCase_ApplyConfig_Call) Run(run func(ctx context.Context, blueprint *domain.BlueprintSpec)) *mockEcosystemConfigUseCase_ApplyConfig_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string)) + run(args[0].(context.Context), args[1].(*domain.BlueprintSpec)) }) return _c } @@ -63,7 +64,7 @@ func (_c *mockEcosystemConfigUseCase_ApplyConfig_Call) Return(_a0 error) *mockEc return _c } -func (_c *mockEcosystemConfigUseCase_ApplyConfig_Call) RunAndReturn(run func(context.Context, string) error) *mockEcosystemConfigUseCase_ApplyConfig_Call { +func (_c *mockEcosystemConfigUseCase_ApplyConfig_Call) RunAndReturn(run func(context.Context, *domain.BlueprintSpec) error) *mockEcosystemConfigUseCase_ApplyConfig_Call { _c.Call.Return(run) return _c } diff --git a/pkg/application/mock_ecosystemHealthUseCase_test.go b/pkg/application/mock_ecosystemHealthUseCase_test.go index a0b5e2b1..29a31aa3 100644 --- a/pkg/application/mock_ecosystemHealthUseCase_test.go +++ b/pkg/application/mock_ecosystemHealthUseCase_test.go @@ -5,7 +5,9 @@ package application import ( context "context" + domain "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" ecosystem "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" + mock "github.com/stretchr/testify/mock" ) @@ -22,9 +24,9 @@ func (_m *mockEcosystemHealthUseCase) EXPECT() *mockEcosystemHealthUseCase_Expec return &mockEcosystemHealthUseCase_Expecter{mock: &_m.Mock} } -// CheckEcosystemHealth provides a mock function with given fields: ctx, ignoreDoguHealth, ignoreComponentHealth -func (_m *mockEcosystemHealthUseCase) CheckEcosystemHealth(ctx context.Context, ignoreDoguHealth bool, ignoreComponentHealth bool) (ecosystem.HealthResult, error) { - ret := _m.Called(ctx, ignoreDoguHealth, ignoreComponentHealth) +// CheckEcosystemHealth provides a mock function with given fields: _a0, _a1 +func (_m *mockEcosystemHealthUseCase) CheckEcosystemHealth(_a0 context.Context, _a1 *domain.BlueprintSpec) (ecosystem.HealthResult, error) { + ret := _m.Called(_a0, _a1) if len(ret) == 0 { panic("no return value specified for CheckEcosystemHealth") @@ -32,17 +34,17 @@ func (_m *mockEcosystemHealthUseCase) CheckEcosystemHealth(ctx context.Context, var r0 ecosystem.HealthResult var r1 error - if rf, ok := ret.Get(0).(func(context.Context, bool, bool) (ecosystem.HealthResult, error)); ok { - return rf(ctx, ignoreDoguHealth, ignoreComponentHealth) + if rf, ok := ret.Get(0).(func(context.Context, *domain.BlueprintSpec) (ecosystem.HealthResult, error)); ok { + return rf(_a0, _a1) } - if rf, ok := ret.Get(0).(func(context.Context, bool, bool) ecosystem.HealthResult); ok { - r0 = rf(ctx, ignoreDoguHealth, ignoreComponentHealth) + if rf, ok := ret.Get(0).(func(context.Context, *domain.BlueprintSpec) ecosystem.HealthResult); ok { + r0 = rf(_a0, _a1) } else { r0 = ret.Get(0).(ecosystem.HealthResult) } - if rf, ok := ret.Get(1).(func(context.Context, bool, bool) error); ok { - r1 = rf(ctx, ignoreDoguHealth, ignoreComponentHealth) + if rf, ok := ret.Get(1).(func(context.Context, *domain.BlueprintSpec) error); ok { + r1 = rf(_a0, _a1) } else { r1 = ret.Error(1) } @@ -56,16 +58,15 @@ type mockEcosystemHealthUseCase_CheckEcosystemHealth_Call struct { } // CheckEcosystemHealth is a helper method to define mock.On call -// - ctx context.Context -// - ignoreDoguHealth bool -// - ignoreComponentHealth bool -func (_e *mockEcosystemHealthUseCase_Expecter) CheckEcosystemHealth(ctx interface{}, ignoreDoguHealth interface{}, ignoreComponentHealth interface{}) *mockEcosystemHealthUseCase_CheckEcosystemHealth_Call { - return &mockEcosystemHealthUseCase_CheckEcosystemHealth_Call{Call: _e.mock.On("CheckEcosystemHealth", ctx, ignoreDoguHealth, ignoreComponentHealth)} +// - _a0 context.Context +// - _a1 *domain.BlueprintSpec +func (_e *mockEcosystemHealthUseCase_Expecter) CheckEcosystemHealth(_a0 interface{}, _a1 interface{}) *mockEcosystemHealthUseCase_CheckEcosystemHealth_Call { + return &mockEcosystemHealthUseCase_CheckEcosystemHealth_Call{Call: _e.mock.On("CheckEcosystemHealth", _a0, _a1)} } -func (_c *mockEcosystemHealthUseCase_CheckEcosystemHealth_Call) Run(run func(ctx context.Context, ignoreDoguHealth bool, ignoreComponentHealth bool)) *mockEcosystemHealthUseCase_CheckEcosystemHealth_Call { +func (_c *mockEcosystemHealthUseCase_CheckEcosystemHealth_Call) Run(run func(_a0 context.Context, _a1 *domain.BlueprintSpec)) *mockEcosystemHealthUseCase_CheckEcosystemHealth_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(bool), args[2].(bool)) + run(args[0].(context.Context), args[1].(*domain.BlueprintSpec)) }) return _c } @@ -75,7 +76,7 @@ func (_c *mockEcosystemHealthUseCase_CheckEcosystemHealth_Call) Return(_a0 ecosy return _c } -func (_c *mockEcosystemHealthUseCase_CheckEcosystemHealth_Call) RunAndReturn(run func(context.Context, bool, bool) (ecosystem.HealthResult, error)) *mockEcosystemHealthUseCase_CheckEcosystemHealth_Call { +func (_c *mockEcosystemHealthUseCase_CheckEcosystemHealth_Call) RunAndReturn(run func(context.Context, *domain.BlueprintSpec) (ecosystem.HealthResult, error)) *mockEcosystemHealthUseCase_CheckEcosystemHealth_Call { _c.Call.Return(run) return _c } diff --git a/pkg/application/mock_effectiveBlueprintUseCase_test.go b/pkg/application/mock_effectiveBlueprintUseCase_test.go index 84762fca..73554c4a 100644 --- a/pkg/application/mock_effectiveBlueprintUseCase_test.go +++ b/pkg/application/mock_effectiveBlueprintUseCase_test.go @@ -5,6 +5,7 @@ package application import ( context "context" + domain "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" mock "github.com/stretchr/testify/mock" ) @@ -21,17 +22,17 @@ func (_m *mockEffectiveBlueprintUseCase) EXPECT() *mockEffectiveBlueprintUseCase return &mockEffectiveBlueprintUseCase_Expecter{mock: &_m.Mock} } -// CalculateEffectiveBlueprint provides a mock function with given fields: ctx, blueprintId -func (_m *mockEffectiveBlueprintUseCase) CalculateEffectiveBlueprint(ctx context.Context, blueprintId string) error { - ret := _m.Called(ctx, blueprintId) +// CalculateEffectiveBlueprint provides a mock function with given fields: ctx, blueprint +func (_m *mockEffectiveBlueprintUseCase) CalculateEffectiveBlueprint(ctx context.Context, blueprint *domain.BlueprintSpec) error { + ret := _m.Called(ctx, blueprint) if len(ret) == 0 { panic("no return value specified for CalculateEffectiveBlueprint") } var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { - r0 = rf(ctx, blueprintId) + if rf, ok := ret.Get(0).(func(context.Context, *domain.BlueprintSpec) error); ok { + r0 = rf(ctx, blueprint) } else { r0 = ret.Error(0) } @@ -46,14 +47,14 @@ type mockEffectiveBlueprintUseCase_CalculateEffectiveBlueprint_Call struct { // CalculateEffectiveBlueprint is a helper method to define mock.On call // - ctx context.Context -// - blueprintId string -func (_e *mockEffectiveBlueprintUseCase_Expecter) CalculateEffectiveBlueprint(ctx interface{}, blueprintId interface{}) *mockEffectiveBlueprintUseCase_CalculateEffectiveBlueprint_Call { - return &mockEffectiveBlueprintUseCase_CalculateEffectiveBlueprint_Call{Call: _e.mock.On("CalculateEffectiveBlueprint", ctx, blueprintId)} +// - blueprint *domain.BlueprintSpec +func (_e *mockEffectiveBlueprintUseCase_Expecter) CalculateEffectiveBlueprint(ctx interface{}, blueprint interface{}) *mockEffectiveBlueprintUseCase_CalculateEffectiveBlueprint_Call { + return &mockEffectiveBlueprintUseCase_CalculateEffectiveBlueprint_Call{Call: _e.mock.On("CalculateEffectiveBlueprint", ctx, blueprint)} } -func (_c *mockEffectiveBlueprintUseCase_CalculateEffectiveBlueprint_Call) Run(run func(ctx context.Context, blueprintId string)) *mockEffectiveBlueprintUseCase_CalculateEffectiveBlueprint_Call { +func (_c *mockEffectiveBlueprintUseCase_CalculateEffectiveBlueprint_Call) Run(run func(ctx context.Context, blueprint *domain.BlueprintSpec)) *mockEffectiveBlueprintUseCase_CalculateEffectiveBlueprint_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string)) + run(args[0].(context.Context), args[1].(*domain.BlueprintSpec)) }) return _c } @@ -63,7 +64,7 @@ func (_c *mockEffectiveBlueprintUseCase_CalculateEffectiveBlueprint_Call) Return return _c } -func (_c *mockEffectiveBlueprintUseCase_CalculateEffectiveBlueprint_Call) RunAndReturn(run func(context.Context, string) error) *mockEffectiveBlueprintUseCase_CalculateEffectiveBlueprint_Call { +func (_c *mockEffectiveBlueprintUseCase_CalculateEffectiveBlueprint_Call) RunAndReturn(run func(context.Context, *domain.BlueprintSpec) error) *mockEffectiveBlueprintUseCase_CalculateEffectiveBlueprint_Call { _c.Call.Return(run) return _c } diff --git a/pkg/application/mock_healthConfigProvider_test.go b/pkg/application/mock_healthConfigProvider_test.go deleted file mode 100644 index 8891b9c8..00000000 --- a/pkg/application/mock_healthConfigProvider_test.go +++ /dev/null @@ -1,151 +0,0 @@ -// Code generated by mockery v2.53.3. DO NOT EDIT. - -package application - -import ( - context "context" - - ecosystem "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" - mock "github.com/stretchr/testify/mock" -) - -// mockHealthConfigProvider is an autogenerated mock type for the healthConfigProvider type -type mockHealthConfigProvider struct { - mock.Mock -} - -type mockHealthConfigProvider_Expecter struct { - mock *mock.Mock -} - -func (_m *mockHealthConfigProvider) EXPECT() *mockHealthConfigProvider_Expecter { - return &mockHealthConfigProvider_Expecter{mock: &_m.Mock} -} - -// GetRequiredComponents provides a mock function with given fields: ctx -func (_m *mockHealthConfigProvider) GetRequiredComponents(ctx context.Context) ([]ecosystem.RequiredComponent, error) { - ret := _m.Called(ctx) - - if len(ret) == 0 { - panic("no return value specified for GetRequiredComponents") - } - - var r0 []ecosystem.RequiredComponent - var r1 error - if rf, ok := ret.Get(0).(func(context.Context) ([]ecosystem.RequiredComponent, error)); ok { - return rf(ctx) - } - if rf, ok := ret.Get(0).(func(context.Context) []ecosystem.RequiredComponent); ok { - r0 = rf(ctx) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]ecosystem.RequiredComponent) - } - } - - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(ctx) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// mockHealthConfigProvider_GetRequiredComponents_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRequiredComponents' -type mockHealthConfigProvider_GetRequiredComponents_Call struct { - *mock.Call -} - -// GetRequiredComponents is a helper method to define mock.On call -// - ctx context.Context -func (_e *mockHealthConfigProvider_Expecter) GetRequiredComponents(ctx interface{}) *mockHealthConfigProvider_GetRequiredComponents_Call { - return &mockHealthConfigProvider_GetRequiredComponents_Call{Call: _e.mock.On("GetRequiredComponents", ctx)} -} - -func (_c *mockHealthConfigProvider_GetRequiredComponents_Call) Run(run func(ctx context.Context)) *mockHealthConfigProvider_GetRequiredComponents_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context)) - }) - return _c -} - -func (_c *mockHealthConfigProvider_GetRequiredComponents_Call) Return(_a0 []ecosystem.RequiredComponent, _a1 error) *mockHealthConfigProvider_GetRequiredComponents_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *mockHealthConfigProvider_GetRequiredComponents_Call) RunAndReturn(run func(context.Context) ([]ecosystem.RequiredComponent, error)) *mockHealthConfigProvider_GetRequiredComponents_Call { - _c.Call.Return(run) - return _c -} - -// GetWaitConfig provides a mock function with given fields: ctx -func (_m *mockHealthConfigProvider) GetWaitConfig(ctx context.Context) (ecosystem.WaitConfig, error) { - ret := _m.Called(ctx) - - if len(ret) == 0 { - panic("no return value specified for GetWaitConfig") - } - - var r0 ecosystem.WaitConfig - var r1 error - if rf, ok := ret.Get(0).(func(context.Context) (ecosystem.WaitConfig, error)); ok { - return rf(ctx) - } - if rf, ok := ret.Get(0).(func(context.Context) ecosystem.WaitConfig); ok { - r0 = rf(ctx) - } else { - r0 = ret.Get(0).(ecosystem.WaitConfig) - } - - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(ctx) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// mockHealthConfigProvider_GetWaitConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetWaitConfig' -type mockHealthConfigProvider_GetWaitConfig_Call struct { - *mock.Call -} - -// GetWaitConfig is a helper method to define mock.On call -// - ctx context.Context -func (_e *mockHealthConfigProvider_Expecter) GetWaitConfig(ctx interface{}) *mockHealthConfigProvider_GetWaitConfig_Call { - return &mockHealthConfigProvider_GetWaitConfig_Call{Call: _e.mock.On("GetWaitConfig", ctx)} -} - -func (_c *mockHealthConfigProvider_GetWaitConfig_Call) Run(run func(ctx context.Context)) *mockHealthConfigProvider_GetWaitConfig_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context)) - }) - return _c -} - -func (_c *mockHealthConfigProvider_GetWaitConfig_Call) Return(_a0 ecosystem.WaitConfig, _a1 error) *mockHealthConfigProvider_GetWaitConfig_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *mockHealthConfigProvider_GetWaitConfig_Call) RunAndReturn(run func(context.Context) (ecosystem.WaitConfig, error)) *mockHealthConfigProvider_GetWaitConfig_Call { - _c.Call.Return(run) - return _c -} - -// newMockHealthConfigProvider creates a new instance of mockHealthConfigProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func newMockHealthConfigProvider(t interface { - mock.TestingT - Cleanup(func()) -}) *mockHealthConfigProvider { - mock := &mockHealthConfigProvider{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/application/mock_healthWaitConfigProvider_test.go b/pkg/application/mock_healthWaitConfigProvider_test.go deleted file mode 100644 index 529218a4..00000000 --- a/pkg/application/mock_healthWaitConfigProvider_test.go +++ /dev/null @@ -1,93 +0,0 @@ -// Code generated by mockery v2.53.3. DO NOT EDIT. - -package application - -import ( - context "context" - - ecosystem "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" - mock "github.com/stretchr/testify/mock" -) - -// mockHealthWaitConfigProvider is an autogenerated mock type for the healthWaitConfigProvider type -type mockHealthWaitConfigProvider struct { - mock.Mock -} - -type mockHealthWaitConfigProvider_Expecter struct { - mock *mock.Mock -} - -func (_m *mockHealthWaitConfigProvider) EXPECT() *mockHealthWaitConfigProvider_Expecter { - return &mockHealthWaitConfigProvider_Expecter{mock: &_m.Mock} -} - -// GetWaitConfig provides a mock function with given fields: ctx -func (_m *mockHealthWaitConfigProvider) GetWaitConfig(ctx context.Context) (ecosystem.WaitConfig, error) { - ret := _m.Called(ctx) - - if len(ret) == 0 { - panic("no return value specified for GetWaitConfig") - } - - var r0 ecosystem.WaitConfig - var r1 error - if rf, ok := ret.Get(0).(func(context.Context) (ecosystem.WaitConfig, error)); ok { - return rf(ctx) - } - if rf, ok := ret.Get(0).(func(context.Context) ecosystem.WaitConfig); ok { - r0 = rf(ctx) - } else { - r0 = ret.Get(0).(ecosystem.WaitConfig) - } - - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(ctx) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// mockHealthWaitConfigProvider_GetWaitConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetWaitConfig' -type mockHealthWaitConfigProvider_GetWaitConfig_Call struct { - *mock.Call -} - -// GetWaitConfig is a helper method to define mock.On call -// - ctx context.Context -func (_e *mockHealthWaitConfigProvider_Expecter) GetWaitConfig(ctx interface{}) *mockHealthWaitConfigProvider_GetWaitConfig_Call { - return &mockHealthWaitConfigProvider_GetWaitConfig_Call{Call: _e.mock.On("GetWaitConfig", ctx)} -} - -func (_c *mockHealthWaitConfigProvider_GetWaitConfig_Call) Run(run func(ctx context.Context)) *mockHealthWaitConfigProvider_GetWaitConfig_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context)) - }) - return _c -} - -func (_c *mockHealthWaitConfigProvider_GetWaitConfig_Call) Return(_a0 ecosystem.WaitConfig, _a1 error) *mockHealthWaitConfigProvider_GetWaitConfig_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *mockHealthWaitConfigProvider_GetWaitConfig_Call) RunAndReturn(run func(context.Context) (ecosystem.WaitConfig, error)) *mockHealthWaitConfigProvider_GetWaitConfig_Call { - _c.Call.Return(run) - return _c -} - -// newMockHealthWaitConfigProvider creates a new instance of mockHealthWaitConfigProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func newMockHealthWaitConfigProvider(t interface { - mock.TestingT - Cleanup(func()) -}) *mockHealthWaitConfigProvider { - mock := &mockHealthWaitConfigProvider{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/application/mock_initialBlueprintStatusUseCase_test.go b/pkg/application/mock_initialBlueprintStatusUseCase_test.go new file mode 100644 index 00000000..34b060c6 --- /dev/null +++ b/pkg/application/mock_initialBlueprintStatusUseCase_test.go @@ -0,0 +1,84 @@ +// Code generated by mockery v2.53.3. DO NOT EDIT. + +package application + +import ( + context "context" + + domain "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" + mock "github.com/stretchr/testify/mock" +) + +// mockInitialBlueprintStatusUseCase is an autogenerated mock type for the initialBlueprintStatusUseCase type +type mockInitialBlueprintStatusUseCase struct { + mock.Mock +} + +type mockInitialBlueprintStatusUseCase_Expecter struct { + mock *mock.Mock +} + +func (_m *mockInitialBlueprintStatusUseCase) EXPECT() *mockInitialBlueprintStatusUseCase_Expecter { + return &mockInitialBlueprintStatusUseCase_Expecter{mock: &_m.Mock} +} + +// InitateConditions provides a mock function with given fields: ctx, blueprint +func (_m *mockInitialBlueprintStatusUseCase) InitateConditions(ctx context.Context, blueprint *domain.BlueprintSpec) error { + ret := _m.Called(ctx, blueprint) + + if len(ret) == 0 { + panic("no return value specified for InitateConditions") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *domain.BlueprintSpec) error); ok { + r0 = rf(ctx, blueprint) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// mockInitialBlueprintStatusUseCase_InitateConditions_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'InitateConditions' +type mockInitialBlueprintStatusUseCase_InitateConditions_Call struct { + *mock.Call +} + +// InitateConditions is a helper method to define mock.On call +// - ctx context.Context +// - blueprint *domain.BlueprintSpec +func (_e *mockInitialBlueprintStatusUseCase_Expecter) InitateConditions(ctx interface{}, blueprint interface{}) *mockInitialBlueprintStatusUseCase_InitateConditions_Call { + return &mockInitialBlueprintStatusUseCase_InitateConditions_Call{Call: _e.mock.On("InitateConditions", ctx, blueprint)} +} + +func (_c *mockInitialBlueprintStatusUseCase_InitateConditions_Call) Run(run func(ctx context.Context, blueprint *domain.BlueprintSpec)) *mockInitialBlueprintStatusUseCase_InitateConditions_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*domain.BlueprintSpec)) + }) + return _c +} + +func (_c *mockInitialBlueprintStatusUseCase_InitateConditions_Call) Return(_a0 error) *mockInitialBlueprintStatusUseCase_InitateConditions_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *mockInitialBlueprintStatusUseCase_InitateConditions_Call) RunAndReturn(run func(context.Context, *domain.BlueprintSpec) error) *mockInitialBlueprintStatusUseCase_InitateConditions_Call { + _c.Call.Return(run) + return _c +} + +// newMockInitialBlueprintStatusUseCase creates a new instance of mockInitialBlueprintStatusUseCase. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func newMockInitialBlueprintStatusUseCase(t interface { + mock.TestingT + Cleanup(func()) +}) *mockInitialBlueprintStatusUseCase { + mock := &mockInitialBlueprintStatusUseCase{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/application/mock_requiredComponentsProvider_test.go b/pkg/application/mock_requiredComponentsProvider_test.go deleted file mode 100644 index f7e874fc..00000000 --- a/pkg/application/mock_requiredComponentsProvider_test.go +++ /dev/null @@ -1,95 +0,0 @@ -// Code generated by mockery v2.53.3. DO NOT EDIT. - -package application - -import ( - context "context" - - ecosystem "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" - mock "github.com/stretchr/testify/mock" -) - -// mockRequiredComponentsProvider is an autogenerated mock type for the requiredComponentsProvider type -type mockRequiredComponentsProvider struct { - mock.Mock -} - -type mockRequiredComponentsProvider_Expecter struct { - mock *mock.Mock -} - -func (_m *mockRequiredComponentsProvider) EXPECT() *mockRequiredComponentsProvider_Expecter { - return &mockRequiredComponentsProvider_Expecter{mock: &_m.Mock} -} - -// GetRequiredComponents provides a mock function with given fields: ctx -func (_m *mockRequiredComponentsProvider) GetRequiredComponents(ctx context.Context) ([]ecosystem.RequiredComponent, error) { - ret := _m.Called(ctx) - - if len(ret) == 0 { - panic("no return value specified for GetRequiredComponents") - } - - var r0 []ecosystem.RequiredComponent - var r1 error - if rf, ok := ret.Get(0).(func(context.Context) ([]ecosystem.RequiredComponent, error)); ok { - return rf(ctx) - } - if rf, ok := ret.Get(0).(func(context.Context) []ecosystem.RequiredComponent); ok { - r0 = rf(ctx) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]ecosystem.RequiredComponent) - } - } - - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(ctx) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// mockRequiredComponentsProvider_GetRequiredComponents_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRequiredComponents' -type mockRequiredComponentsProvider_GetRequiredComponents_Call struct { - *mock.Call -} - -// GetRequiredComponents is a helper method to define mock.On call -// - ctx context.Context -func (_e *mockRequiredComponentsProvider_Expecter) GetRequiredComponents(ctx interface{}) *mockRequiredComponentsProvider_GetRequiredComponents_Call { - return &mockRequiredComponentsProvider_GetRequiredComponents_Call{Call: _e.mock.On("GetRequiredComponents", ctx)} -} - -func (_c *mockRequiredComponentsProvider_GetRequiredComponents_Call) Run(run func(ctx context.Context)) *mockRequiredComponentsProvider_GetRequiredComponents_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context)) - }) - return _c -} - -func (_c *mockRequiredComponentsProvider_GetRequiredComponents_Call) Return(_a0 []ecosystem.RequiredComponent, _a1 error) *mockRequiredComponentsProvider_GetRequiredComponents_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *mockRequiredComponentsProvider_GetRequiredComponents_Call) RunAndReturn(run func(context.Context) ([]ecosystem.RequiredComponent, error)) *mockRequiredComponentsProvider_GetRequiredComponents_Call { - _c.Call.Return(run) - return _c -} - -// newMockRequiredComponentsProvider creates a new instance of mockRequiredComponentsProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func newMockRequiredComponentsProvider(t interface { - mock.TestingT - Cleanup(func()) -}) *mockRequiredComponentsProvider { - mock := &mockRequiredComponentsProvider{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/application/mock_selfUpgradeUseCase_test.go b/pkg/application/mock_selfUpgradeUseCase_test.go deleted file mode 100644 index 04639aa1..00000000 --- a/pkg/application/mock_selfUpgradeUseCase_test.go +++ /dev/null @@ -1,83 +0,0 @@ -// Code generated by mockery v2.53.3. DO NOT EDIT. - -package application - -import ( - context "context" - - mock "github.com/stretchr/testify/mock" -) - -// mockSelfUpgradeUseCase is an autogenerated mock type for the selfUpgradeUseCase type -type mockSelfUpgradeUseCase struct { - mock.Mock -} - -type mockSelfUpgradeUseCase_Expecter struct { - mock *mock.Mock -} - -func (_m *mockSelfUpgradeUseCase) EXPECT() *mockSelfUpgradeUseCase_Expecter { - return &mockSelfUpgradeUseCase_Expecter{mock: &_m.Mock} -} - -// HandleSelfUpgrade provides a mock function with given fields: ctx, blueprintId -func (_m *mockSelfUpgradeUseCase) HandleSelfUpgrade(ctx context.Context, blueprintId string) error { - ret := _m.Called(ctx, blueprintId) - - if len(ret) == 0 { - panic("no return value specified for HandleSelfUpgrade") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { - r0 = rf(ctx, blueprintId) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// mockSelfUpgradeUseCase_HandleSelfUpgrade_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'HandleSelfUpgrade' -type mockSelfUpgradeUseCase_HandleSelfUpgrade_Call struct { - *mock.Call -} - -// HandleSelfUpgrade is a helper method to define mock.On call -// - ctx context.Context -// - blueprintId string -func (_e *mockSelfUpgradeUseCase_Expecter) HandleSelfUpgrade(ctx interface{}, blueprintId interface{}) *mockSelfUpgradeUseCase_HandleSelfUpgrade_Call { - return &mockSelfUpgradeUseCase_HandleSelfUpgrade_Call{Call: _e.mock.On("HandleSelfUpgrade", ctx, blueprintId)} -} - -func (_c *mockSelfUpgradeUseCase_HandleSelfUpgrade_Call) Run(run func(ctx context.Context, blueprintId string)) *mockSelfUpgradeUseCase_HandleSelfUpgrade_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string)) - }) - return _c -} - -func (_c *mockSelfUpgradeUseCase_HandleSelfUpgrade_Call) Return(_a0 error) *mockSelfUpgradeUseCase_HandleSelfUpgrade_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *mockSelfUpgradeUseCase_HandleSelfUpgrade_Call) RunAndReturn(run func(context.Context, string) error) *mockSelfUpgradeUseCase_HandleSelfUpgrade_Call { - _c.Call.Return(run) - return _c -} - -// newMockSelfUpgradeUseCase creates a new instance of mockSelfUpgradeUseCase. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func newMockSelfUpgradeUseCase(t interface { - mock.TestingT - Cleanup(func()) -}) *mockSelfUpgradeUseCase { - mock := &mockSelfUpgradeUseCase{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/application/mock_stateDiffUseCase_test.go b/pkg/application/mock_stateDiffUseCase_test.go index 3759daf6..2375a0eb 100644 --- a/pkg/application/mock_stateDiffUseCase_test.go +++ b/pkg/application/mock_stateDiffUseCase_test.go @@ -5,6 +5,7 @@ package application import ( context "context" + domain "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" mock "github.com/stretchr/testify/mock" ) @@ -21,17 +22,17 @@ func (_m *mockStateDiffUseCase) EXPECT() *mockStateDiffUseCase_Expecter { return &mockStateDiffUseCase_Expecter{mock: &_m.Mock} } -// DetermineStateDiff provides a mock function with given fields: ctx, blueprintId -func (_m *mockStateDiffUseCase) DetermineStateDiff(ctx context.Context, blueprintId string) error { - ret := _m.Called(ctx, blueprintId) +// DetermineStateDiff provides a mock function with given fields: ctx, blueprint +func (_m *mockStateDiffUseCase) DetermineStateDiff(ctx context.Context, blueprint *domain.BlueprintSpec) error { + ret := _m.Called(ctx, blueprint) if len(ret) == 0 { panic("no return value specified for DetermineStateDiff") } var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { - r0 = rf(ctx, blueprintId) + if rf, ok := ret.Get(0).(func(context.Context, *domain.BlueprintSpec) error); ok { + r0 = rf(ctx, blueprint) } else { r0 = ret.Error(0) } @@ -46,14 +47,14 @@ type mockStateDiffUseCase_DetermineStateDiff_Call struct { // DetermineStateDiff is a helper method to define mock.On call // - ctx context.Context -// - blueprintId string -func (_e *mockStateDiffUseCase_Expecter) DetermineStateDiff(ctx interface{}, blueprintId interface{}) *mockStateDiffUseCase_DetermineStateDiff_Call { - return &mockStateDiffUseCase_DetermineStateDiff_Call{Call: _e.mock.On("DetermineStateDiff", ctx, blueprintId)} +// - blueprint *domain.BlueprintSpec +func (_e *mockStateDiffUseCase_Expecter) DetermineStateDiff(ctx interface{}, blueprint interface{}) *mockStateDiffUseCase_DetermineStateDiff_Call { + return &mockStateDiffUseCase_DetermineStateDiff_Call{Call: _e.mock.On("DetermineStateDiff", ctx, blueprint)} } -func (_c *mockStateDiffUseCase_DetermineStateDiff_Call) Run(run func(ctx context.Context, blueprintId string)) *mockStateDiffUseCase_DetermineStateDiff_Call { +func (_c *mockStateDiffUseCase_DetermineStateDiff_Call) Run(run func(ctx context.Context, blueprint *domain.BlueprintSpec)) *mockStateDiffUseCase_DetermineStateDiff_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string)) + run(args[0].(context.Context), args[1].(*domain.BlueprintSpec)) }) return _c } @@ -63,7 +64,7 @@ func (_c *mockStateDiffUseCase_DetermineStateDiff_Call) Return(_a0 error) *mockS return _c } -func (_c *mockStateDiffUseCase_DetermineStateDiff_Call) RunAndReturn(run func(context.Context, string) error) *mockStateDiffUseCase_DetermineStateDiff_Call { +func (_c *mockStateDiffUseCase_DetermineStateDiff_Call) RunAndReturn(run func(context.Context, *domain.BlueprintSpec) error) *mockStateDiffUseCase_DetermineStateDiff_Call { _c.Call.Return(run) return _c } diff --git a/pkg/application/selfUpgradeUseCase.go b/pkg/application/selfUpgradeUseCase.go deleted file mode 100644 index fd637108..00000000 --- a/pkg/application/selfUpgradeUseCase.go +++ /dev/null @@ -1,146 +0,0 @@ -package application - -import ( - "context" - "errors" - "fmt" - "github.com/Masterminds/semver/v3" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/common" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domainservice" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/util" - "sigs.k8s.io/controller-runtime/pkg/log" -) - -type SelfUpgradeUseCase struct { - blueprintRepo domainservice.BlueprintSpecRepository - componentRepo componentInstallationRepository - componentUseCase componentInstallationUseCase - blueprintOperatorName common.SimpleComponentName - healthConfigProvider healthConfigProvider -} - -func NewSelfUpgradeUseCase( - blueprintRepo domainservice.BlueprintSpecRepository, - componentRepo componentInstallationRepository, - componentUseCase componentInstallationUseCase, - blueprintOperatorName common.SimpleComponentName, - healthConfigProvider healthConfigProvider, -) *SelfUpgradeUseCase { - return &SelfUpgradeUseCase{ - blueprintRepo: blueprintRepo, - componentRepo: componentRepo, - componentUseCase: componentUseCase, - blueprintOperatorName: blueprintOperatorName, - healthConfigProvider: healthConfigProvider, - } -} - -// HandleSelfUpgrade checks if a self upgrade is necessary, executes all needed steps and -// can check if the self upgrade was successful after a restart. -// It always sets the fitting status in the blueprint spec. -func (useCase *SelfUpgradeUseCase) HandleSelfUpgrade(ctx context.Context, blueprintId string) error { - logger := log.FromContext(ctx).WithName("ComponentInstallationUseCase.ApplySelfUpgrade") - - blueprintSpec, err := useCase.blueprintRepo.GetById(ctx, blueprintId) - if err != nil { - return fmt.Errorf("cannot load blueprint spec %q to possibly self upgrade the operator: %w", blueprintId, err) - } - - ownDiff := blueprintSpec.StateDiff.ComponentDiffs.GetComponentDiffByName(useCase.blueprintOperatorName) - - if !ownDiff.HasChanges() { - logger.Info("self upgrade not needed") - blueprintSpec.MarkSelfUpgradeCompleted() - err = useCase.blueprintRepo.Update(ctx, blueprintSpec) - if err != nil { - return fmt.Errorf("cannot save blueprint spec %q to skip self upgrade: %w", blueprintSpec.Id, err) - } - return nil - } - - ownComponent, err := useCase.componentRepo.GetByName(ctx, useCase.blueprintOperatorName) - - if err != nil && !domainservice.IsNotFoundError(err) { - // ignore not found errors as this is ok if the component was not installed via a component CR - // only return if other errors happen, e.g. InternalError - return fmt.Errorf("cannot load component installation for %q from ecosystem: %w", useCase.blueprintOperatorName, err) - } - // use extra vars to avoid nil pointer dereferences of the component - var expectedVersion, actualVersion *semver.Version - if ownComponent != nil { - expectedVersion = ownComponent.ExpectedVersion - actualVersion = ownComponent.ActualVersion - } - - if !ownDiff.IsExpectedVersion(expectedVersion) { - return useCase.doSelfUpgrade(ctx, blueprintSpec, ownDiff, ownComponent) - // the operator waits for termination, unless there was an error, so we can return here - } - - if !ownDiff.IsExpectedVersion(actualVersion) { - err = useCase.awaitInstallationConfirmation(ctx, blueprintSpec) - if err != nil { - return err - } - } - - blueprintSpec.MarkSelfUpgradeCompleted() - err = useCase.blueprintRepo.Update(ctx, blueprintSpec) - if err != nil { - return fmt.Errorf("cannot save blueprint spec %q after self upgrading the operator: %w", blueprintSpec.Id, err) - } - logger.Info("self upgrade successful") - - return nil -} - -func (useCase *SelfUpgradeUseCase) doSelfUpgrade(ctx context.Context, blueprintSpec *domain.BlueprintSpec, ownDiff domain.ComponentDiff, ownComponent *ecosystem.ComponentInstallation) error { - logger := log.FromContext(ctx).WithName("ComponentInstallationUseCase.doSelfUpgrade") - logger.Info("self upgrade needed, apply self upgrade") - blueprintSpec.MarkWaitingForSelfUpgrade() - err := useCase.blueprintRepo.Update(ctx, blueprintSpec) - if err != nil { - return fmt.Errorf("cannot persist blueprint spec %q to mark it waiting for self upgrade: %w", blueprintSpec.Id, err) - } - err = useCase.applySelfUpgrade(ctx, ownDiff, ownComponent) - if err != nil { - return err - } - logger.Info("await termination for self upgrade. Check the component-CR for the installation status") - useCase.waitForTermination(ctx) - return nil // this code is never reached as we wait for termination before -} - -func (useCase *SelfUpgradeUseCase) applySelfUpgrade(ctx context.Context, ownDiff domain.ComponentDiff, ownComponent *ecosystem.ComponentInstallation) error { - err := useCase.componentUseCase.applyComponentState(ctx, ownDiff, ownComponent) - if err != nil { - return fmt.Errorf("an error occurred while applying the self-upgrade to the ecosystem: %w", err) - } - return nil -} - -func (useCase *SelfUpgradeUseCase) waitForTermination(ctx context.Context) { - <-ctx.Done() -} - -func (useCase *SelfUpgradeUseCase) awaitInstallationConfirmation(ctx context.Context, blueprintSpec *domain.BlueprintSpec) error { - config, err := useCase.healthConfigProvider.GetWaitConfig(ctx) - if err != nil { - return fmt.Errorf("could not retrieve wait interval config for self upgrade: %w", err) - } - _, err = util.RetryUntilSuccessOrCancellation(ctx, config.Interval, func(ctx context.Context) (*interface{}, error, bool) { - ownComponent, err := useCase.componentRepo.GetByName(ctx, useCase.blueprintOperatorName) - if err != nil { - return nil, fmt.Errorf("could not reload component for version confirmation: %w", err), false - } - ownDiff := blueprintSpec.StateDiff.ComponentDiffs.GetComponentDiffByName(useCase.blueprintOperatorName) - return nil, nil, !ownDiff.IsExpectedVersion(ownComponent.ActualVersion) // retry if true - }) - if err != nil && !errors.Is(err, ctx.Err()) { - // ignore cancellation error as this can happen, if the operator is getting restarted more than once (e.g. maybe because of a cluster failure) - return fmt.Errorf("error while waiting for version confirmation: %w", err) - } - return nil -} diff --git a/pkg/application/selfUpgradeUseCase_test.go b/pkg/application/selfUpgradeUseCase_test.go deleted file mode 100644 index de103956..00000000 --- a/pkg/application/selfUpgradeUseCase_test.go +++ /dev/null @@ -1,402 +0,0 @@ -package application - -import ( - "context" - "github.com/Masterminds/semver/v3" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/common" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domainservice" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - "testing" - "time" -) - -func TestSelfUpgradeUseCase_HandleSelfUpgrade(t *testing.T) { - var blueprintOperatorName = common.SimpleComponentName("k8s-blueprint-operator") - blueprintId := "myBlueprint" - testCtx := context.TODO() - version1, _ := semver.NewVersion("1.0") - version2, _ := semver.NewVersion("2.0") - internalTestError := domainservice.NewInternalError(assert.AnError, "internal error") - waitConfig := ecosystem.WaitConfig{Interval: 5 * time.Second} - - UpgradeToV2ComponentDiff := domain.ComponentDiff{ - Name: blueprintOperatorName, - Actual: domain.ComponentDiffState{ - Version: version1, - }, - Expected: domain.ComponentDiffState{ - Version: version2, - }, - NeededActions: []domain.Action{domain.ActionUpgrade}, - } - upgradeToV2StateDiff := domain.StateDiff{ - ComponentDiffs: []domain.ComponentDiff{ - UpgradeToV2ComponentDiff, - }, - } - - t.Run("nothing to do", func(t *testing.T) { - blueprintRepo := newMockBlueprintSpecRepository(t) - componentRepo := newMockComponentInstallationRepository(t) - componentUseCase := newMockComponentInstallationUseCase(t) - configProvider := newMockHealthConfigProvider(t) - useCase := NewSelfUpgradeUseCase(blueprintRepo, componentRepo, componentUseCase, blueprintOperatorName, configProvider) - - blueprint := &domain.BlueprintSpec{ - StateDiff: domain.StateDiff{}, - Status: domain.StatusPhaseBlueprintApplicationPreProcessed, - } - - blueprintRepo.EXPECT().GetById(mock.Anything, blueprintId).Return(blueprint, nil) - blueprintRepo.EXPECT().Update(mock.Anything, blueprint).Return(nil).Run(func(ctx context.Context, blueprintSpec *domain.BlueprintSpec) { - require.Equal(t, domain.StatusPhaseSelfUpgradeCompleted, blueprint.Status) - }) - - err := useCase.HandleSelfUpgrade(testCtx, blueprintId) - - assert.NoError(t, err) - }) - - t.Run("apply upgrade until termination", func(t *testing.T) { - blueprintRepo := newMockBlueprintSpecRepository(t) - componentRepo := newMockComponentInstallationRepository(t) - componentUseCase := newMockComponentInstallationUseCase(t) - configProvider := newMockHealthConfigProvider(t) - useCase := NewSelfUpgradeUseCase(blueprintRepo, componentRepo, componentUseCase, blueprintOperatorName, configProvider) - - blueprint := &domain.BlueprintSpec{ - StateDiff: upgradeToV2StateDiff, - Status: domain.StatusPhaseBlueprintApplicationPreProcessed, - } - - component := &ecosystem.ComponentInstallation{ - ExpectedVersion: version1, - ActualVersion: version1, - } - - timeoutCtx, cancelCtx := context.WithTimeout(testCtx, time.Second) // but usually cancel - defer cancelCtx() - - blueprintRepo.EXPECT().GetById(mock.Anything, blueprintId).Return(blueprint, nil) - blueprintRepo.EXPECT().Update(mock.Anything, blueprint). - Return(nil). - Run(func(ctx context.Context, blueprintSpec *domain.BlueprintSpec) { - assert.Equal(t, domain.StatusPhaseAwaitSelfUpgrade, blueprint.Status) - }).Once() // only once as the operator will terminate and will set status completed later. - - componentRepo.EXPECT().GetByName(timeoutCtx, blueprintOperatorName).Return(component, nil).Once() - componentUseCase.EXPECT().applyComponentState(mock.Anything, UpgradeToV2ComponentDiff, component).Return(nil). - Run(func(_ context.Context, _ domain.ComponentDiff, _ *ecosystem.ComponentInstallation) { - // check that the status is set beforehand, as we cannot guarantee that we can set it afterward before termination - assert.Equal(t, domain.StatusPhaseAwaitSelfUpgrade, blueprint.Status) - cancelCtx() - }) - - err := useCase.HandleSelfUpgrade(timeoutCtx, blueprintId) - - assert.NoError(t, err) - }) - - t.Run("verify installation after termination", func(t *testing.T) { - blueprintRepo := newMockBlueprintSpecRepository(t) - componentRepo := newMockComponentInstallationRepository(t) - componentUseCase := newMockComponentInstallationUseCase(t) - configProvider := newMockHealthConfigProvider(t) - useCase := NewSelfUpgradeUseCase(blueprintRepo, componentRepo, componentUseCase, blueprintOperatorName, configProvider) - - blueprint := &domain.BlueprintSpec{ - StateDiff: upgradeToV2StateDiff, - Status: domain.StatusPhaseAwaitSelfUpgrade, - } - - component := &ecosystem.ComponentInstallation{ - ExpectedVersion: version2, - ActualVersion: version2, - } - - blueprintRepo.EXPECT().GetById(testCtx, blueprintId).Return(blueprint, nil) - componentRepo.EXPECT().GetByName(testCtx, blueprintOperatorName).Return(component, nil).Once() // no reload for actual version check - blueprintRepo.EXPECT().Update(testCtx, blueprint).Return(nil).Run(func(ctx context.Context, blueprintSpec *domain.BlueprintSpec) { - require.Equal(t, domain.StatusPhaseSelfUpgradeCompleted, blueprint.Status) - }) - - err := useCase.HandleSelfUpgrade(testCtx, blueprintId) - - assert.NoError(t, err) - }) - - t.Run("await installation confirmation after termination", func(t *testing.T) { - blueprintRepo := newMockBlueprintSpecRepository(t) - componentRepo := newMockComponentInstallationRepository(t) - componentUseCase := newMockComponentInstallationUseCase(t) - configProvider := newMockHealthConfigProvider(t) - useCase := NewSelfUpgradeUseCase(blueprintRepo, componentRepo, componentUseCase, blueprintOperatorName, configProvider) - - blueprint := &domain.BlueprintSpec{ - StateDiff: upgradeToV2StateDiff, - Status: domain.StatusPhaseAwaitSelfUpgrade, - } - - component1 := &ecosystem.ComponentInstallation{ - ExpectedVersion: version2, - ActualVersion: version1, - } - component2 := &ecosystem.ComponentInstallation{ - ExpectedVersion: version2, - ActualVersion: version2, - } - timeoutCtx, cancelCtx := context.WithTimeout(testCtx, time.Second) // no timeout should happen as - defer cancelCtx() - - blueprintRepo.EXPECT().GetById(timeoutCtx, blueprintId).Return(blueprint, nil) - componentRepo.EXPECT().GetByName(timeoutCtx, blueprintOperatorName).Return(component1, nil).Once() - componentRepo.EXPECT().GetByName(timeoutCtx, blueprintOperatorName).Return(component2, nil).Once() - configProvider.EXPECT().GetWaitConfig(timeoutCtx).Return(waitConfig, nil) - blueprintRepo.EXPECT().Update(timeoutCtx, blueprint).Return(nil).Run(func(ctx context.Context, blueprintSpec *domain.BlueprintSpec) { - require.Equal(t, domain.StatusPhaseSelfUpgradeCompleted, blueprint.Status) - }) - - err := useCase.HandleSelfUpgrade(timeoutCtx, blueprintId) - - assert.NoError(t, err) - }) - - t.Run("apply upgrade with missing component cr", func(t *testing.T) { - blueprintRepo := newMockBlueprintSpecRepository(t) - componentRepo := newMockComponentInstallationRepository(t) - componentUseCase := newMockComponentInstallationUseCase(t) - configProvider := newMockHealthConfigProvider(t) - useCase := NewSelfUpgradeUseCase(blueprintRepo, componentRepo, componentUseCase, blueprintOperatorName, configProvider) - - blueprint := &domain.BlueprintSpec{ - StateDiff: upgradeToV2StateDiff, - Status: domain.StatusPhaseBlueprintApplicationPreProcessed, - } - - timeoutCtx, cancelCtx := context.WithTimeout(testCtx, time.Second) // but usually cancel - defer cancelCtx() - - blueprintRepo.EXPECT().GetById(mock.Anything, blueprintId).Return(blueprint, nil) - componentRepo.EXPECT().GetByName(mock.Anything, blueprintOperatorName).Return(nil, domainservice.NewNotFoundError(assert.AnError, "test-error")) - blueprintRepo.EXPECT().Update(mock.Anything, blueprint).Return(nil) - var nilComponent *ecosystem.ComponentInstallation - componentUseCase.EXPECT().applyComponentState(mock.Anything, UpgradeToV2ComponentDiff, nilComponent).Return(nil).Run( - func(_ context.Context, _ domain.ComponentDiff, _ *ecosystem.ComponentInstallation) { - cancelCtx() - }, - ) - - err := useCase.HandleSelfUpgrade(timeoutCtx, blueprintId) - - assert.NoError(t, err) - }) - - t.Run("could not load blueprint", func(t *testing.T) { - blueprintRepo := newMockBlueprintSpecRepository(t) - componentRepo := newMockComponentInstallationRepository(t) - componentUseCase := newMockComponentInstallationUseCase(t) - configProvider := newMockHealthConfigProvider(t) - useCase := NewSelfUpgradeUseCase(blueprintRepo, componentRepo, componentUseCase, blueprintOperatorName, configProvider) - - blueprint := &domain.BlueprintSpec{ - StateDiff: upgradeToV2StateDiff, - Status: domain.StatusPhaseBlueprintApplicationPreProcessed, - } - - blueprintRepo.EXPECT().GetById(mock.Anything, blueprintId).Return(blueprint, assert.AnError) - - err := useCase.HandleSelfUpgrade(testCtx, blueprintId) - - assert.ErrorIs(t, err, assert.AnError) - assert.ErrorContains(t, err, "cannot load blueprint spec \""+blueprintId+"\" to possibly self upgrade the operator") - }) - - t.Run("cannot load component", func(t *testing.T) { - blueprintRepo := newMockBlueprintSpecRepository(t) - componentRepo := newMockComponentInstallationRepository(t) - componentUseCase := newMockComponentInstallationUseCase(t) - configProvider := newMockHealthConfigProvider(t) - useCase := NewSelfUpgradeUseCase(blueprintRepo, componentRepo, componentUseCase, blueprintOperatorName, configProvider) - - blueprint := &domain.BlueprintSpec{ - StateDiff: upgradeToV2StateDiff, - Status: domain.StatusPhaseBlueprintApplicationPreProcessed, - } - - blueprintRepo.EXPECT().GetById(mock.Anything, blueprintId).Return(blueprint, nil) - componentRepo.EXPECT().GetByName(mock.Anything, blueprintOperatorName).Return(nil, internalTestError) - - err := useCase.HandleSelfUpgrade(testCtx, blueprintId) - - assert.ErrorIs(t, err, internalTestError) - assert.ErrorContains(t, err, "cannot load component installation for \""+string(blueprintOperatorName)+"\" from ecosystem") - }) - - t.Run("cannot save blueprint in doSelfUpgrade", func(t *testing.T) { - blueprintRepo := newMockBlueprintSpecRepository(t) - componentRepo := newMockComponentInstallationRepository(t) - componentUseCase := newMockComponentInstallationUseCase(t) - configProvider := newMockHealthConfigProvider(t) - useCase := NewSelfUpgradeUseCase(blueprintRepo, componentRepo, componentUseCase, blueprintOperatorName, configProvider) - - blueprint := &domain.BlueprintSpec{ - Id: blueprintId, - StateDiff: upgradeToV2StateDiff, - Status: domain.StatusPhaseBlueprintApplicationPreProcessed, - } - - component := &ecosystem.ComponentInstallation{ - ExpectedVersion: version1, - } - - blueprintRepo.EXPECT().GetById(mock.Anything, blueprintId).Return(blueprint, nil) - componentRepo.EXPECT().GetByName(mock.Anything, blueprintOperatorName).Return(component, nil) - blueprintRepo.EXPECT().Update(mock.Anything, blueprint).Return(internalTestError) - - err := useCase.HandleSelfUpgrade(testCtx, blueprintId) - - assert.ErrorIs(t, err, internalTestError) - assert.ErrorContains(t, err, "cannot persist blueprint spec \""+blueprintId+"\" to mark it waiting for self upgrade") - }) - - t.Run("cannot apply self upgrade", func(t *testing.T) { - blueprintRepo := newMockBlueprintSpecRepository(t) - componentRepo := newMockComponentInstallationRepository(t) - componentUseCase := newMockComponentInstallationUseCase(t) - configProvider := newMockHealthConfigProvider(t) - useCase := NewSelfUpgradeUseCase(blueprintRepo, componentRepo, componentUseCase, blueprintOperatorName, configProvider) - - blueprint := &domain.BlueprintSpec{ - StateDiff: upgradeToV2StateDiff, - Status: domain.StatusPhaseBlueprintApplicationPreProcessed, - } - - component := &ecosystem.ComponentInstallation{ - ExpectedVersion: version1, - } - - blueprintRepo.EXPECT().GetById(mock.Anything, blueprintId).Return(blueprint, nil) - componentRepo.EXPECT().GetByName(mock.Anything, blueprintOperatorName).Return(component, nil) - blueprintRepo.EXPECT().Update(mock.Anything, blueprint).Return(nil) - componentUseCase.EXPECT().applyComponentState(mock.Anything, UpgradeToV2ComponentDiff, component).Return(internalTestError) - - err := useCase.HandleSelfUpgrade(testCtx, blueprintId) - - assert.ErrorIs(t, err, internalTestError) - assert.ErrorContains(t, err, "an error occurred while applying the self-upgrade to the ecosystem") - }) - - t.Run("cannot save blueprint to skip self upgrade", func(t *testing.T) { - blueprintRepo := newMockBlueprintSpecRepository(t) - componentRepo := newMockComponentInstallationRepository(t) - componentUseCase := newMockComponentInstallationUseCase(t) - configProvider := newMockHealthConfigProvider(t) - useCase := NewSelfUpgradeUseCase(blueprintRepo, componentRepo, componentUseCase, blueprintOperatorName, configProvider) - - blueprint := &domain.BlueprintSpec{ - Id: blueprintId, - StateDiff: domain.StateDiff{}, - Status: domain.StatusPhaseBlueprintApplicationPreProcessed, - } - - blueprintRepo.EXPECT().GetById(mock.Anything, blueprintId).Return(blueprint, nil) - blueprintRepo.EXPECT().Update(mock.Anything, blueprint).Return(assert.AnError) - - err := useCase.HandleSelfUpgrade(testCtx, blueprintId) - - assert.ErrorIs(t, err, assert.AnError) - assert.ErrorContains(t, err, "cannot save blueprint spec \""+blueprintId+"\" to skip self upgrade") - }) - - t.Run("error awaiting version confirmation, cannot load component", func(t *testing.T) { - blueprintRepo := newMockBlueprintSpecRepository(t) - componentRepo := newMockComponentInstallationRepository(t) - componentUseCase := newMockComponentInstallationUseCase(t) - configProvider := newMockHealthConfigProvider(t) - useCase := NewSelfUpgradeUseCase(blueprintRepo, componentRepo, componentUseCase, blueprintOperatorName, configProvider) - - blueprint := &domain.BlueprintSpec{ - StateDiff: upgradeToV2StateDiff, - Status: domain.StatusPhaseAwaitSelfUpgrade, - } - - component := &ecosystem.ComponentInstallation{ - ExpectedVersion: version2, - ActualVersion: version1, - } - timeoutCtx, cancelCtx := context.WithTimeout(testCtx, time.Second) // no timeout should happen as - defer cancelCtx() - - blueprintRepo.EXPECT().GetById(timeoutCtx, blueprintId).Return(blueprint, nil) - componentRepo.EXPECT().GetByName(timeoutCtx, blueprintOperatorName).Return(component, nil).Once() - componentRepo.EXPECT().GetByName(timeoutCtx, blueprintOperatorName).Return(nil, assert.AnError).Once() - configProvider.EXPECT().GetWaitConfig(timeoutCtx).Return(waitConfig, nil) - - err := useCase.HandleSelfUpgrade(timeoutCtx, blueprintId) - - assert.ErrorIs(t, err, assert.AnError) - assert.ErrorContains(t, err, "error while waiting for version confirmation") - assert.ErrorContains(t, err, "could not reload component for version confirmation") - }) - - t.Run("error awaiting version confirmation, cannot load wait config", func(t *testing.T) { - blueprintRepo := newMockBlueprintSpecRepository(t) - componentRepo := newMockComponentInstallationRepository(t) - componentUseCase := newMockComponentInstallationUseCase(t) - configProvider := newMockHealthConfigProvider(t) - useCase := NewSelfUpgradeUseCase(blueprintRepo, componentRepo, componentUseCase, blueprintOperatorName, configProvider) - - blueprint := &domain.BlueprintSpec{ - StateDiff: upgradeToV2StateDiff, - Status: domain.StatusPhaseAwaitSelfUpgrade, - } - - component := &ecosystem.ComponentInstallation{ - ExpectedVersion: version2, - ActualVersion: version1, - } - timeoutCtx, cancelCtx := context.WithTimeout(testCtx, time.Second) // no timeout should happen as - defer cancelCtx() - - blueprintRepo.EXPECT().GetById(timeoutCtx, blueprintId).Return(blueprint, nil) - componentRepo.EXPECT().GetByName(timeoutCtx, blueprintOperatorName).Return(component, nil).Once() - configProvider.EXPECT().GetWaitConfig(timeoutCtx).Return(waitConfig, assert.AnError) - - err := useCase.HandleSelfUpgrade(timeoutCtx, blueprintId) - - assert.ErrorIs(t, err, assert.AnError) - assert.ErrorContains(t, err, "could not retrieve wait interval config for self upgrade") - }) - - t.Run("error saving blueprint after awaiting version confirmation", func(t *testing.T) { - blueprintRepo := newMockBlueprintSpecRepository(t) - componentRepo := newMockComponentInstallationRepository(t) - componentUseCase := newMockComponentInstallationUseCase(t) - configProvider := newMockHealthConfigProvider(t) - useCase := NewSelfUpgradeUseCase(blueprintRepo, componentRepo, componentUseCase, blueprintOperatorName, configProvider) - - blueprint := &domain.BlueprintSpec{ - Id: blueprintId, - StateDiff: upgradeToV2StateDiff, - Status: domain.StatusPhaseAwaitSelfUpgrade, - } - - component := &ecosystem.ComponentInstallation{ - ExpectedVersion: version2, - ActualVersion: version2, - } - - blueprintRepo.EXPECT().GetById(testCtx, blueprintId).Return(blueprint, nil) - componentRepo.EXPECT().GetByName(testCtx, blueprintOperatorName).Return(component, nil).Once() // no reload for actual version check - blueprintRepo.EXPECT().Update(testCtx, blueprint).Return(assert.AnError) - - err := useCase.HandleSelfUpgrade(testCtx, blueprintId) - - assert.ErrorIs(t, err, assert.AnError) - assert.ErrorContains(t, err, "cannot save blueprint spec \"myBlueprint\" after self upgrading the operator") - }) -} diff --git a/pkg/application/stateDiffUseCase.go b/pkg/application/stateDiffUseCase.go index 7e705ce8..9f084174 100644 --- a/pkg/application/stateDiffUseCase.go +++ b/pkg/application/stateDiffUseCase.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domainservice" @@ -13,85 +14,68 @@ import ( const REFERENCED_CONFIG_NOT_FOUND = "could not load referenced sensitive config" type StateDiffUseCase struct { - blueprintSpecRepo blueprintSpecRepository - doguInstallationRepo doguInstallationRepository - componentInstallationRepo componentInstallationRepository - globalConfigRepo globalConfigRepository - doguConfigRepo doguConfigRepository - sensitiveDoguConfigRepo sensitiveDoguConfigRepository - sensitiveConfigRefReader sensitiveConfigRefReader + blueprintSpecRepo blueprintSpecRepository + doguInstallationRepo doguInstallationRepository + globalConfigRepo globalConfigRepository + doguConfigRepo doguConfigRepository + sensitiveDoguConfigRepo sensitiveDoguConfigRepository + sensitiveConfigRefReader sensitiveConfigRefReader } -func NewStateDiffUseCase( - blueprintSpecRepo domainservice.BlueprintSpecRepository, - doguInstallationRepo domainservice.DoguInstallationRepository, - componentInstallationRepo domainservice.ComponentInstallationRepository, - globalConfigRepo domainservice.GlobalConfigRepository, - doguConfigRepo domainservice.DoguConfigRepository, - sensitiveDoguConfigRepo domainservice.SensitiveDoguConfigRepository, - sensitiveConfigRefReader domainservice.SensitiveConfigRefReader, -) *StateDiffUseCase { +func NewStateDiffUseCase(blueprintSpecRepo domainservice.BlueprintSpecRepository, doguInstallationRepo domainservice.DoguInstallationRepository, globalConfigRepo domainservice.GlobalConfigRepository, doguConfigRepo domainservice.DoguConfigRepository, sensitiveDoguConfigRepo domainservice.SensitiveDoguConfigRepository, sensitiveConfigRefReader domainservice.SensitiveConfigRefReader) *StateDiffUseCase { return &StateDiffUseCase{ - blueprintSpecRepo: blueprintSpecRepo, - doguInstallationRepo: doguInstallationRepo, - componentInstallationRepo: componentInstallationRepo, - globalConfigRepo: globalConfigRepo, - doguConfigRepo: doguConfigRepo, - sensitiveDoguConfigRepo: sensitiveDoguConfigRepo, - sensitiveConfigRefReader: sensitiveConfigRefReader, + blueprintSpecRepo: blueprintSpecRepo, + doguInstallationRepo: doguInstallationRepo, + globalConfigRepo: globalConfigRepo, + doguConfigRepo: doguConfigRepo, + sensitiveDoguConfigRepo: sensitiveDoguConfigRepo, + sensitiveConfigRefReader: sensitiveConfigRefReader, } } // DetermineStateDiff loads the state of the ecosystem and compares it to the blueprint. It creates a declarative diff. // returns: -// - a domainservice.NotFoundError if the blueprint was not found or could not found dogu decryption keys or // - a domainservice.InternalError if there is any error while loading or persisting the blueprintSpec or while collecting the ecosystem state or // - a domainservice.ConflictError if there was a concurrent write to the blueprint or // - a domain.InvalidBlueprintError if there are any forbidden actions in the stateDiff. // - any error if there is any other error. -func (useCase *StateDiffUseCase) DetermineStateDiff(ctx context.Context, blueprintId string) error { - logger := log.FromContext(ctx).WithName("StateDiffUseCase.DetermineStateDiff"). - WithValues("blueprintId", blueprintId) +func (useCase *StateDiffUseCase) DetermineStateDiff(ctx context.Context, blueprint *domain.BlueprintSpec) error { + logger := log.FromContext(ctx).WithName("StateDiffUseCase.DetermineStateDiff") - logger.Info("getting blueprint spec for determining state diff") - blueprintSpec, err := useCase.blueprintSpecRepo.GetById(ctx, blueprintId) - if err != nil { - return fmt.Errorf("cannot load blueprint spec %q to determine state diff: %w", blueprintId, err) - } - - logger.Info("load referenced sensitive config") + logger.V(2).Info("load referenced sensitive config") // load referenced config before collecting ecosystem state // if an error happens here, we save a lot of heavy work referencedSensitiveConfig, err := useCase.sensitiveConfigRefReader.GetValues( - ctx, blueprintSpec.EffectiveBlueprint.Config.GetSensitiveConfigReferences(), + ctx, blueprint.EffectiveBlueprint.Config.GetSensitiveConfigReferences(), ) if err != nil { err = fmt.Errorf("%s: %w", REFERENCED_CONFIG_NOT_FOUND, err) - blueprintSpec.MissingConfigReferences(err) - updateError := useCase.blueprintSpecRepo.Update(ctx, blueprintSpec) + blueprint.MissingConfigReferences(err) + updateError := useCase.blueprintSpecRepo.Update(ctx, blueprint) if updateError != nil { return errors.Join(updateError, err) } return err } - logger.Info("collect ecosystem state for state diff") - ecosystemState, err := useCase.collectEcosystemState(ctx, blueprintSpec.EffectiveBlueprint) + logger.V(2).Info("collect ecosystem state for state diff") + ecosystemState, err := useCase.collectEcosystemState(ctx, blueprint.EffectiveBlueprint) if err != nil { return fmt.Errorf("could not determine state diff: %w", err) } - logger.Info("determine state diff to the cloudogu ecosystem", "blueprintStatus", blueprintSpec.Status) - stateDiffError := blueprintSpec.DetermineStateDiff(ecosystemState, referencedSensitiveConfig) + logger.V(2).Info("determine state diff to the cloudogu ecosystem") + stateDiffError := blueprint.DetermineStateDiff(ecosystemState, referencedSensitiveConfig) var invalidError *domain.InvalidBlueprintError if errors.As(stateDiffError, &invalidError) { // do not return here as with this error the blueprint status and events should be persisted as normal. } else if stateDiffError != nil { - return fmt.Errorf("failed to determine state diff for blueprint %q: %w", blueprintId, stateDiffError) + return fmt.Errorf("failed to determine state diff: %w", stateDiffError) } - err = useCase.blueprintSpecRepo.Update(ctx, blueprintSpec) + + err = useCase.blueprintSpecRepo.Update(ctx, blueprint) if err != nil { - return fmt.Errorf("cannot save blueprint spec %q after determining the state diff to the ecosystem: %w", blueprintId, err) + return fmt.Errorf("cannot save blueprint spec %q after determining the state diff to the ecosystem: %w", blueprint.Id, err) } // return this error back here to persist the blueprint status and events first. @@ -103,29 +87,26 @@ func (useCase *StateDiffUseCase) collectEcosystemState(ctx context.Context, effe logger := log.FromContext(ctx).WithName("StateDiffUseCase.collectEcosystemState") // TODO: collect ecosystem state in parallel (like for ecosystem health) if we have time - // load current dogus and components - logger.Info("collect installed dogus") + // load current dogus + logger.V(2).Info("collect installed dogus") installedDogus, doguErr := useCase.doguInstallationRepo.GetAll(ctx) - logger.Info("collect installed components") - installedComponents, componentErr := useCase.componentInstallationRepo.GetAll(ctx) // load current config - logger.Info("collect needed global config") + logger.V(2).Info("collect needed global config") globalConfig, globalConfigErr := useCase.globalConfigRepo.Get(ctx) - logger.Info("collect needed dogu config") + logger.V(2).Info("collect needed dogu config") configByDogu, doguConfigErr := useCase.doguConfigRepo.GetAllExisting(ctx, effectiveBlueprint.Config.GetDogusWithChangedConfig()) - logger.Info("collect needed sensitive dogu config") + logger.V(2).Info("collect needed sensitive dogu config") sensitiveConfigByDogu, sensitiveConfigErr := useCase.sensitiveDoguConfigRepo.GetAllExisting(ctx, effectiveBlueprint.Config.GetDogusWithChangedSensitiveConfig()) - joinedError := errors.Join(doguErr, componentErr, globalConfigErr, doguConfigErr, sensitiveConfigErr) + joinedError := errors.Join(doguErr, globalConfigErr, doguConfigErr, sensitiveConfigErr) if joinedError != nil { return ecosystem.EcosystemState{}, fmt.Errorf("could not collect ecosystem state: %w", joinedError) } return ecosystem.EcosystemState{ InstalledDogus: installedDogus, - InstalledComponents: installedComponents, GlobalConfig: globalConfig, ConfigByDogu: configByDogu, SensitiveConfigByDogu: sensitiveConfigByDogu, diff --git a/pkg/application/stateDiffUseCase_test.go b/pkg/application/stateDiffUseCase_test.go index 53c4cbaf..1a6b9b45 100644 --- a/pkg/application/stateDiffUseCase_test.go +++ b/pkg/application/stateDiffUseCase_test.go @@ -1,6 +1,8 @@ package application import ( + "testing" + cescommons "github.com/cloudogu/ces-commons-lib/dogu" "github.com/cloudogu/cesapp-lib/core" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" @@ -10,17 +12,14 @@ import ( "github.com/cloudogu/k8s-registry-lib/config" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "testing" + "k8s.io/apimachinery/pkg/api/resource" ) var ( nsOfficial = cescommons.Namespace("official") - nsK8s = cescommons.Namespace("k8s") - postfix = cescommons.SimpleName("postfix") - ldap = cescommons.SimpleName("ldap") - nginxIngress = cescommons.SimpleName("nginx-ingress") - nginxStatic = cescommons.SimpleName("nginx-static") + postfix = cescommons.SimpleName("postfix") + ldap = cescommons.SimpleName("ldap") postfixQualifiedDoguName = cescommons.QualifiedName{ Namespace: nsOfficial, @@ -30,98 +29,33 @@ var ( Namespace: nsOfficial, SimpleName: ldap, } - nginxIngressQualifiedDoguName = cescommons.QualifiedName{ - Namespace: nsK8s, - SimpleName: nginxIngress, - } - nginxStaticQualifiedDoguName = cescommons.QualifiedName{ - Namespace: nsK8s, - SimpleName: nginxStatic, - } nilDoguNameList []cescommons.SimpleName ) var ( - internalTestError = domainservice.NewInternalError(assert.AnError, "internal error") - nginxStaticConfigKeyNginxKey1 = common.DoguConfigKey{DoguName: "nginx-static", Key: "nginxKey1"} - nginxStaticConfigKeyNginxKey2 = common.DoguConfigKey{DoguName: "nginx-static", Key: "nginxKey2"} - nginxStaticSensitiveConfigKeyNginxKey1 = nginxStaticConfigKeyNginxKey1 - nginxStaticSensitiveConfigKeyNginxKey2 = nginxStaticConfigKeyNginxKey2 + internalTestError = domainservice.NewInternalError(assert.AnError, "internal error") + ldapConfigKeyNginxKey1 = common.DoguConfigKey{DoguName: "ldap", Key: "ldapKey1"} + ldapConfigKeyNginxKey2 = common.DoguConfigKey{DoguName: "ldap", Key: "ldapKey2"} + ldapSensitiveConfigKeyNginxKey1 = ldapConfigKeyNginxKey1 + ldapSensitiveConfigKeyNginxKey2 = ldapConfigKeyNginxKey2 + val1 = "val1" + val2 = "val2" + val3 = "val3" ) func TestStateDiffUseCase_DetermineStateDiff(t *testing.T) { - t.Run("should fail to load blueprint spec", func(t *testing.T) { - // given - blueprintRepoMock := newMockBlueprintSpecRepository(t) - blueprintRepoMock.EXPECT().GetById(testCtx, "testBlueprint1").Return(nil, assert.AnError) - - doguInstallRepoMock := newMockDoguInstallationRepository(t) - componentInstallRepoMock := newMockComponentInstallationRepository(t) - - sut := NewStateDiffUseCase(blueprintRepoMock, doguInstallRepoMock, componentInstallRepoMock, nil, nil, nil, nil) - - // when - err := sut.DetermineStateDiff(testCtx, "testBlueprint1") - - // then - require.Error(t, err) - assert.ErrorIs(t, err, assert.AnError) - assert.ErrorContains(t, err, "cannot load blueprint spec \"testBlueprint1\" to determine state diff") - }) t.Run("should fail to get installed dogus", func(t *testing.T) { // given blueprint := &domain.BlueprintSpec{Id: "testBlueprint1"} - blueprintRepoMock := newMockBlueprintSpecRepository(t) - blueprintRepoMock.EXPECT().GetById(testCtx, "testBlueprint1").Return(blueprint, nil) - doguInstallRepoMock := newMockDoguInstallationRepository(t) doguInstallRepoMock.EXPECT().GetAll(testCtx).Return(nil, internalTestError) - componentInstallRepoMock := newMockComponentInstallationRepository(t) - componentInstallRepoMock.EXPECT().GetAll(testCtx).Return(nil, nil) - - globalConfigRepoMock := newMockGlobalConfigRepository(t) - entries, _ := config.MapToEntries(map[string]any{}) - globalConfig := config.CreateGlobalConfig(entries) - globalConfigRepoMock.EXPECT().Get(testCtx).Return(globalConfig, nil) - doguConfigRepoMock := newMockDoguConfigRepository(t) - doguConfigRepoMock.EXPECT().GetAllExisting(testCtx, nilDoguNameList).Return(map[cescommons.SimpleName]config.DoguConfig{}, nil) - sensitiveDoguConfigRepoMock := newMockSensitiveDoguConfigRepository(t) - sensitiveDoguConfigRepoMock.EXPECT().GetAllExisting(testCtx, nilDoguNameList).Return(map[cescommons.SimpleName]config.DoguConfig{}, nil) - configRefReaderMock := newMockSensitiveConfigRefReader(t) - configRefReaderMock.EXPECT(). - GetValues(testCtx, map[common.DoguConfigKey]domain.SensitiveValueRef{}). - Return(map[common.DoguConfigKey]config.Value{}, nil) - - sut := NewStateDiffUseCase(blueprintRepoMock, doguInstallRepoMock, componentInstallRepoMock, globalConfigRepoMock, doguConfigRepoMock, sensitiveDoguConfigRepoMock, configRefReaderMock) - - // when - err := sut.DetermineStateDiff(testCtx, "testBlueprint1") - - // then - require.Error(t, err) - assert.ErrorIs(t, err, internalTestError) - assert.ErrorContains(t, err, "could not determine state diff") - assert.ErrorContains(t, err, "could not collect ecosystem state") - }) - t.Run("should fail to get installed components", func(t *testing.T) { - // given - blueprint := &domain.BlueprintSpec{Id: "testBlueprint1"} - - blueprintRepoMock := newMockBlueprintSpecRepository(t) - blueprintRepoMock.EXPECT().GetById(testCtx, "testBlueprint1").Return(blueprint, nil) - - doguInstallRepoMock := newMockDoguInstallationRepository(t) - doguInstallRepoMock.EXPECT().GetAll(testCtx).Return(map[cescommons.SimpleName]*ecosystem.DoguInstallation{}, nil) - componentInstallRepoMock := newMockComponentInstallationRepository(t) - componentInstallRepoMock.EXPECT().GetAll(testCtx).Return(nil, internalTestError) globalConfigRepoMock := newMockGlobalConfigRepository(t) entries, _ := config.MapToEntries(map[string]any{}) globalConfig := config.CreateGlobalConfig(entries) globalConfigRepoMock.EXPECT().Get(testCtx).Return(globalConfig, nil) - doguConfigRepoMock := newMockDoguConfigRepository(t) doguConfigRepoMock.EXPECT().GetAllExisting(testCtx, nilDoguNameList).Return(map[cescommons.SimpleName]config.DoguConfig{}, nil) sensitiveDoguConfigRepoMock := newMockSensitiveDoguConfigRepository(t) @@ -131,10 +65,10 @@ func TestStateDiffUseCase_DetermineStateDiff(t *testing.T) { GetValues(testCtx, map[common.DoguConfigKey]domain.SensitiveValueRef{}). Return(map[common.DoguConfigKey]config.Value{}, nil) - sut := NewStateDiffUseCase(blueprintRepoMock, doguInstallRepoMock, componentInstallRepoMock, globalConfigRepoMock, doguConfigRepoMock, sensitiveDoguConfigRepoMock, configRefReaderMock) + sut := NewStateDiffUseCase(nil, doguInstallRepoMock, globalConfigRepoMock, doguConfigRepoMock, sensitiveDoguConfigRepoMock, configRefReaderMock) // when - err := sut.DetermineStateDiff(testCtx, "testBlueprint1") + err := sut.DetermineStateDiff(testCtx, blueprint) // then require.Error(t, err) @@ -146,19 +80,13 @@ func TestStateDiffUseCase_DetermineStateDiff(t *testing.T) { // given blueprint := &domain.BlueprintSpec{Id: "testBlueprint1"} - blueprintRepoMock := newMockBlueprintSpecRepository(t) - blueprintRepoMock.EXPECT().GetById(testCtx, "testBlueprint1").Return(blueprint, nil) - doguInstallRepoMock := newMockDoguInstallationRepository(t) doguInstallRepoMock.EXPECT().GetAll(testCtx).Return(map[cescommons.SimpleName]*ecosystem.DoguInstallation{}, nil) - componentInstallRepoMock := newMockComponentInstallationRepository(t) - componentInstallRepoMock.EXPECT().GetAll(testCtx).Return(nil, nil) globalConfigRepoMock := newMockGlobalConfigRepository(t) entries, _ := config.MapToEntries(map[string]any{}) globalConfig := config.CreateGlobalConfig(entries) globalConfigRepoMock.EXPECT().Get(testCtx).Return(globalConfig, domainservice.NewInternalError(assert.AnError, "internal error")) - doguConfigRepoMock := newMockDoguConfigRepository(t) doguConfigRepoMock.EXPECT().GetAllExisting(testCtx, nilDoguNameList).Return(map[cescommons.SimpleName]config.DoguConfig{}, nil) sensitiveDoguConfigRepoMock := newMockSensitiveDoguConfigRepository(t) @@ -168,10 +96,10 @@ func TestStateDiffUseCase_DetermineStateDiff(t *testing.T) { GetValues(testCtx, map[common.DoguConfigKey]domain.SensitiveValueRef{}). Return(map[common.DoguConfigKey]config.Value{}, nil) - sut := NewStateDiffUseCase(blueprintRepoMock, doguInstallRepoMock, componentInstallRepoMock, globalConfigRepoMock, doguConfigRepoMock, sensitiveDoguConfigRepoMock, configRefReaderMock) + sut := NewStateDiffUseCase(nil, doguInstallRepoMock, globalConfigRepoMock, doguConfigRepoMock, sensitiveDoguConfigRepoMock, configRefReaderMock) // when - err := sut.DetermineStateDiff(testCtx, "testBlueprint1") + err := sut.DetermineStateDiff(testCtx, blueprint) // then require.Error(t, err) @@ -183,15 +111,15 @@ func TestStateDiffUseCase_DetermineStateDiff(t *testing.T) { }) t.Run("should fail to get dogu config", func(t *testing.T) { // given - blueprint := &domain.BlueprintSpec{Id: "testBlueprint1"} - - blueprintRepoMock := newMockBlueprintSpecRepository(t) - blueprintRepoMock.EXPECT().GetById(testCtx, "testBlueprint1").Return(blueprint, nil) + blueprint := &domain.BlueprintSpec{ + Id: "testBlueprint1", + EffectiveBlueprint: domain.EffectiveBlueprint{ + Config: domain.Config{}, + }, + } doguInstallRepoMock := newMockDoguInstallationRepository(t) doguInstallRepoMock.EXPECT().GetAll(testCtx).Return(map[cescommons.SimpleName]*ecosystem.DoguInstallation{}, nil) - componentInstallRepoMock := newMockComponentInstallationRepository(t) - componentInstallRepoMock.EXPECT().GetAll(testCtx).Return(nil, nil) globalConfigRepoMock := newMockGlobalConfigRepository(t) entries, _ := config.MapToEntries(map[string]any{}) @@ -199,20 +127,20 @@ func TestStateDiffUseCase_DetermineStateDiff(t *testing.T) { globalConfigRepoMock.EXPECT().Get(testCtx).Return(globalConfig, nil) doguConfigRepoMock := newMockDoguConfigRepository(t) - doguConfigRepoMock.EXPECT().GetAllExisting(testCtx, nilDoguNameList).Return(map[cescommons.SimpleName]config.DoguConfig{}, nil) + doguConfigRepoMock.EXPECT().GetAllExisting(testCtx, nilDoguNameList).Return(map[cescommons.SimpleName]config.DoguConfig{}, domainservice.NewInternalError(assert.AnError, "internal error")) sensitiveDoguConfigRepoMock := newMockSensitiveDoguConfigRepository(t) sensitiveDoguConfigRepoMock.EXPECT(). GetAllExisting(testCtx, nilDoguNameList). - Return(map[cescommons.SimpleName]config.DoguConfig{}, domainservice.NewInternalError(assert.AnError, "internal error")) + Return(map[cescommons.SimpleName]config.DoguConfig{}, nil) configRefReaderMock := newMockSensitiveConfigRefReader(t) configRefReaderMock.EXPECT(). GetValues(testCtx, map[common.DoguConfigKey]domain.SensitiveValueRef{}). Return(map[common.DoguConfigKey]config.Value{}, nil) - sut := NewStateDiffUseCase(blueprintRepoMock, doguInstallRepoMock, componentInstallRepoMock, globalConfigRepoMock, doguConfigRepoMock, sensitiveDoguConfigRepoMock, configRefReaderMock) + sut := NewStateDiffUseCase(nil, doguInstallRepoMock, globalConfigRepoMock, doguConfigRepoMock, sensitiveDoguConfigRepoMock, configRefReaderMock) // when - err := sut.DetermineStateDiff(testCtx, "testBlueprint1") + err := sut.DetermineStateDiff(testCtx, blueprint) // then require.Error(t, err) @@ -224,15 +152,10 @@ func TestStateDiffUseCase_DetermineStateDiff(t *testing.T) { }) t.Run("should fail to get sensitive dogu config", func(t *testing.T) { // given - blueprint := &domain.BlueprintSpec{Id: "testBlueprint1"} - - blueprintRepoMock := newMockBlueprintSpecRepository(t) - blueprintRepoMock.EXPECT().GetById(testCtx, "testBlueprint1").Return(blueprint, nil) + blueprint := &domain.BlueprintSpec{Id: "testBlueprint1", EffectiveBlueprint: domain.EffectiveBlueprint{Config: domain.Config{}}} doguInstallRepoMock := newMockDoguInstallationRepository(t) doguInstallRepoMock.EXPECT().GetAll(testCtx).Return(map[cescommons.SimpleName]*ecosystem.DoguInstallation{}, nil) - componentInstallRepoMock := newMockComponentInstallationRepository(t) - componentInstallRepoMock.EXPECT().GetAll(testCtx).Return(nil, nil) globalConfigRepoMock := newMockGlobalConfigRepository(t) entries, _ := config.MapToEntries(map[string]any{}) @@ -250,10 +173,10 @@ func TestStateDiffUseCase_DetermineStateDiff(t *testing.T) { GetValues(testCtx, map[common.DoguConfigKey]domain.SensitiveValueRef{}). Return(map[common.DoguConfigKey]config.Value{}, nil) - sut := NewStateDiffUseCase(blueprintRepoMock, doguInstallRepoMock, componentInstallRepoMock, globalConfigRepoMock, doguConfigRepoMock, sensitiveDoguConfigRepoMock, configRefReaderMock) + sut := NewStateDiffUseCase(nil, doguInstallRepoMock, globalConfigRepoMock, doguConfigRepoMock, sensitiveDoguConfigRepoMock, configRefReaderMock) // when - err := sut.DetermineStateDiff(testCtx, "testBlueprint1") + err := sut.DetermineStateDiff(testCtx, blueprint) // then require.Error(t, err) @@ -263,59 +186,23 @@ func TestStateDiffUseCase_DetermineStateDiff(t *testing.T) { assert.ErrorContains(t, err, "could not determine state diff") assert.ErrorContains(t, err, "could not collect ecosystem state") }) - t.Run("should fail to determine state diff for blueprint", func(t *testing.T) { - // given - blueprint := &domain.BlueprintSpec{Id: "testBlueprint1"} - - blueprintRepoMock := newMockBlueprintSpecRepository(t) - blueprintRepoMock.EXPECT().GetById(testCtx, "testBlueprint1").Return(blueprint, nil) - - doguInstallRepoMock := newMockDoguInstallationRepository(t) - doguInstallRepoMock.EXPECT().GetAll(testCtx).Return(nil, nil) - componentInstallRepoMock := newMockComponentInstallationRepository(t) - componentInstallRepoMock.EXPECT().GetAll(testCtx).Return(nil, nil) - - globalConfigRepoMock := newMockGlobalConfigRepository(t) - entries, _ := config.MapToEntries(map[string]any{}) - globalConfig := config.CreateGlobalConfig(entries) - globalConfigRepoMock.EXPECT().Get(testCtx).Return(globalConfig, nil) - - doguConfigRepoMock := newMockDoguConfigRepository(t) - doguConfigRepoMock.EXPECT().GetAllExisting(testCtx, nilDoguNameList).Return(map[cescommons.SimpleName]config.DoguConfig{}, nil) - sensitiveDoguConfigRepoMock := newMockSensitiveDoguConfigRepository(t) - sensitiveDoguConfigRepoMock.EXPECT().GetAllExisting(testCtx, nilDoguNameList).Return(map[cescommons.SimpleName]config.DoguConfig{}, nil) - configRefReaderMock := newMockSensitiveConfigRefReader(t) - configRefReaderMock.EXPECT(). - GetValues(testCtx, map[common.DoguConfigKey]domain.SensitiveValueRef{}). - Return(map[common.DoguConfigKey]config.Value{}, nil) - - sut := NewStateDiffUseCase(blueprintRepoMock, doguInstallRepoMock, componentInstallRepoMock, globalConfigRepoMock, doguConfigRepoMock, sensitiveDoguConfigRepoMock, configRefReaderMock) - - // when - err := sut.DetermineStateDiff(testCtx, "testBlueprint1") - - // then - require.Error(t, err) - assert.ErrorContains(t, err, "failed to determine state diff for blueprint \"testBlueprint1\"") - }) t.Run("should fail to update blueprint", func(t *testing.T) { // given - blueprint := &domain.BlueprintSpec{Id: "testBlueprint1", Status: domain.StatusPhaseValidated} + blueprint := &domain.BlueprintSpec{ + Id: "testBlueprint1", + Conditions: []domain.Condition{}, + } blueprintRepoMock := newMockBlueprintSpecRepository(t) - blueprintRepoMock.EXPECT().GetById(testCtx, "testBlueprint1").Return(blueprint, nil) blueprintRepoMock.EXPECT().Update(testCtx, blueprint).Return(assert.AnError) doguInstallRepoMock := newMockDoguInstallationRepository(t) doguInstallRepoMock.EXPECT().GetAll(testCtx).Return(map[cescommons.SimpleName]*ecosystem.DoguInstallation{}, nil) - componentInstallRepoMock := newMockComponentInstallationRepository(t) - componentInstallRepoMock.EXPECT().GetAll(testCtx).Return(nil, nil) globalConfigRepoMock := newMockGlobalConfigRepository(t) entries, _ := config.MapToEntries(map[string]any{}) globalConfig := config.CreateGlobalConfig(entries) globalConfigRepoMock.EXPECT().Get(testCtx).Return(globalConfig, nil) - doguConfigRepoMock := newMockDoguConfigRepository(t) doguConfigRepoMock.EXPECT().GetAllExisting(testCtx, nilDoguNameList).Return(map[cescommons.SimpleName]config.DoguConfig{}, nil) sensitiveDoguConfigRepoMock := newMockSensitiveDoguConfigRepository(t) @@ -325,10 +212,10 @@ func TestStateDiffUseCase_DetermineStateDiff(t *testing.T) { GetValues(testCtx, map[common.DoguConfigKey]domain.SensitiveValueRef{}). Return(map[common.DoguConfigKey]config.Value{}, nil) - sut := NewStateDiffUseCase(blueprintRepoMock, doguInstallRepoMock, componentInstallRepoMock, globalConfigRepoMock, doguConfigRepoMock, sensitiveDoguConfigRepoMock, configRefReaderMock) + sut := NewStateDiffUseCase(blueprintRepoMock, doguInstallRepoMock, globalConfigRepoMock, doguConfigRepoMock, sensitiveDoguConfigRepoMock, configRefReaderMock) // when - err := sut.DetermineStateDiff(testCtx, "testBlueprint1") + err := sut.DetermineStateDiff(testCtx, blueprint) // then require.Error(t, err) @@ -337,54 +224,54 @@ func TestStateDiffUseCase_DetermineStateDiff(t *testing.T) { }) t.Run("should succeed for dogu diff", func(t *testing.T) { // given + volumeSize := resource.MustParse("2Gi") + bodySize := resource.MustParse("2G") blueprint := &domain.BlueprintSpec{ - Id: "testBlueprint1", + Id: "testBlueprint1", + Conditions: []domain.Condition{}, EffectiveBlueprint: domain.EffectiveBlueprint{ Dogus: []domain.Dogu{ { - Name: postfixQualifiedDoguName, - Version: mustParseVersion(t, "2.9.0"), - TargetState: domain.TargetStatePresent, - }, - { - Name: ldapQualifiedDoguName, - Version: mustParseVersion(t, "1.2.3"), - TargetState: domain.TargetStatePresent, - }, - { - Name: nginxIngressQualifiedDoguName, - Version: mustParseVersion(t, "1.8.5"), - TargetState: domain.TargetStatePresent, + Name: postfixQualifiedDoguName, + Version: mustParseVersionToPtr(t, "2.9.0"), + Absent: false, + MinVolumeSize: &volumeSize, + ReverseProxyConfig: ecosystem.ReverseProxyConfig{ + MaxBodySize: &bodySize, + RewriteTarget: ecosystem.RewriteTarget(rewriteTarget), + AdditionalConfig: ecosystem.AdditionalConfig(additionalConfig), + }, + AdditionalMounts: []ecosystem.AdditionalMount{ + { + SourceType: ecosystem.DataSourceConfigMap, + Name: "configmap", + Volume: "volume", + Subfolder: subfolder, + }, + }, }, { - Name: nginxStaticQualifiedDoguName, - TargetState: domain.TargetStateAbsent, + Name: ldapQualifiedDoguName, + Version: mustParseVersionToPtr(t, "1.2.3"), + Absent: false, }, }, }, - Status: domain.StatusPhaseValidated, - // TODO: add config to test } blueprintRepoMock := newMockBlueprintSpecRepository(t) - blueprintRepoMock.EXPECT().GetById(testCtx, "testBlueprint1").Return(blueprint, nil) blueprintRepoMock.EXPECT().Update(testCtx, blueprint).Return(nil) doguInstallRepoMock := newMockDoguInstallationRepository(t) installedDogus := map[cescommons.SimpleName]*ecosystem.DoguInstallation{ - "ldap": {Name: ldapQualifiedDoguName, Version: mustParseVersion(t, "1.1.1")}, - "nginx-ingress": {Name: nginxIngressQualifiedDoguName, Version: mustParseVersion(t, "1.8.5")}, - "nginx-static": {Name: nginxStaticQualifiedDoguName, Version: mustParseVersion(t, "1.8.6")}, + "ldap": {Name: ldapQualifiedDoguName, Version: mustParseVersion(t, "1.1.1")}, } doguInstallRepoMock.EXPECT().GetAll(testCtx).Return(installedDogus, nil) - componentInstallRepoMock := newMockComponentInstallationRepository(t) - componentInstallRepoMock.EXPECT().GetAll(testCtx).Return(nil, nil) globalConfigRepoMock := newMockGlobalConfigRepository(t) entries, _ := config.MapToEntries(map[string]any{}) globalConfig := config.CreateGlobalConfig(entries) globalConfigRepoMock.EXPECT().Get(testCtx).Return(globalConfig, nil) - doguConfigRepoMock := newMockDoguConfigRepository(t) doguConfigRepoMock.EXPECT().GetAllExisting(testCtx, nilDoguNameList).Return(map[cescommons.SimpleName]config.DoguConfig{}, nil) sensitiveDoguConfigRepoMock := newMockSensitiveDoguConfigRepository(t) @@ -394,98 +281,82 @@ func TestStateDiffUseCase_DetermineStateDiff(t *testing.T) { GetValues(testCtx, map[common.DoguConfigKey]domain.SensitiveValueRef{}). Return(map[common.DoguConfigKey]config.Value{}, nil) - sut := NewStateDiffUseCase(blueprintRepoMock, doguInstallRepoMock, componentInstallRepoMock, globalConfigRepoMock, doguConfigRepoMock, sensitiveDoguConfigRepoMock, configRefReaderMock) + sut := NewStateDiffUseCase(blueprintRepoMock, doguInstallRepoMock, globalConfigRepoMock, doguConfigRepoMock, sensitiveDoguConfigRepoMock, configRefReaderMock) // when - err := sut.DetermineStateDiff(testCtx, "testBlueprint1") + err := sut.DetermineStateDiff(testCtx, blueprint) // then require.NoError(t, err) expectedDoguDiffs := []domain.DoguDiff{ { DoguName: "postfix", - Actual: domain.DoguDiffState{InstallationState: domain.TargetStateAbsent}, + Actual: domain.DoguDiffState{Absent: true}, Expected: domain.DoguDiffState{ - Namespace: "official", - Version: mustParseVersion(t, "2.9.0"), - InstallationState: domain.TargetStatePresent, + Namespace: "official", + Version: mustParseVersionToPtr(t, "2.9.0"), + Absent: false, + MinVolumeSize: &volumeSize, + ReverseProxyConfig: ecosystem.ReverseProxyConfig{ + MaxBodySize: &bodySize, + RewriteTarget: ecosystem.RewriteTarget(rewriteTarget), + AdditionalConfig: ecosystem.AdditionalConfig(additionalConfig), + }, + AdditionalMounts: []ecosystem.AdditionalMount{ + { + SourceType: ecosystem.DataSourceConfigMap, + Name: "configmap", + Volume: "volume", + Subfolder: subfolder, + }, + }, }, NeededActions: []domain.Action{domain.ActionInstall}, }, { DoguName: "ldap", Actual: domain.DoguDiffState{ - Namespace: "official", - Version: mustParseVersion(t, "1.1.1"), - InstallationState: domain.TargetStatePresent, + Namespace: "official", + Version: mustParseVersionToPtr(t, "1.1.1"), + Absent: false, }, Expected: domain.DoguDiffState{ - Namespace: "official", - Version: mustParseVersion(t, "1.2.3"), - InstallationState: domain.TargetStatePresent, + Namespace: "official", + Version: mustParseVersionToPtr(t, "1.2.3"), + Absent: false, }, NeededActions: []domain.Action{domain.ActionUpgrade}, }, - { - DoguName: "nginx-ingress", - Actual: domain.DoguDiffState{ - Namespace: "k8s", - Version: mustParseVersion(t, "1.8.5"), - InstallationState: domain.TargetStatePresent, - }, - Expected: domain.DoguDiffState{ - Namespace: "k8s", - Version: mustParseVersion(t, "1.8.5"), - InstallationState: domain.TargetStatePresent, - }, - NeededActions: nil, - }, - { - DoguName: "nginx-static", - Actual: domain.DoguDiffState{ - Namespace: "k8s", - Version: mustParseVersion(t, "1.8.6"), - InstallationState: domain.TargetStatePresent, - }, - Expected: domain.DoguDiffState{ - Namespace: "k8s", - InstallationState: domain.TargetStateAbsent, - }, - NeededActions: []domain.Action{domain.ActionUninstall}, - }, } assert.ElementsMatch(t, expectedDoguDiffs, blueprint.StateDiff.DoguDiffs) }) t.Run("should succeed for global config diff", func(t *testing.T) { // given blueprint := &domain.BlueprintSpec{ - Id: "testBlueprint1", + Id: "testBlueprint1", + Conditions: []domain.Condition{}, EffectiveBlueprint: domain.EffectiveBlueprint{ Config: domain.Config{ - Global: domain.GlobalConfig{ - Present: map[common.GlobalConfigKey]common.GlobalConfigValue{ - "globalKey1": "globalValue", + Global: domain.GlobalConfigEntries{ + { + Key: "globalKey1", + Value: (*config.Value)(&val1), }, - Absent: []common.GlobalConfigKey{ - "globalKey2", + { + Key: "globalKey2", + Absent: true, }, }, }, }, - Status: domain.StatusPhaseValidated, } blueprintRepoMock := newMockBlueprintSpecRepository(t) - blueprintRepoMock.EXPECT().GetById(testCtx, "testBlueprint1").Return(blueprint, nil) blueprintRepoMock.EXPECT().Update(testCtx, blueprint).Return(nil) doguInstallRepoMock := newMockDoguInstallationRepository(t) - installedDogus := map[cescommons.SimpleName]*ecosystem.DoguInstallation{ - "nginx-static": {Name: nginxStaticQualifiedDoguName, Version: mustParseVersion(t, "1.8.6")}, - } + installedDogus := map[cescommons.SimpleName]*ecosystem.DoguInstallation{} doguInstallRepoMock.EXPECT().GetAll(testCtx).Return(installedDogus, nil) - componentInstallRepoMock := newMockComponentInstallationRepository(t) - componentInstallRepoMock.EXPECT().GetAll(testCtx).Return(nil, nil) globalConfigRepoMock := newMockGlobalConfigRepository(t) entries, _ := config.MapToEntries(map[string]any{}) @@ -501,66 +372,56 @@ func TestStateDiffUseCase_DetermineStateDiff(t *testing.T) { GetValues(testCtx, map[common.DoguConfigKey]domain.SensitiveValueRef{}). Return(map[common.DoguConfigKey]config.Value{}, nil) - sut := NewStateDiffUseCase(blueprintRepoMock, doguInstallRepoMock, componentInstallRepoMock, globalConfigRepoMock, doguConfigRepoMock, sensitiveDoguConfigRepoMock, configRefReaderMock) + sut := NewStateDiffUseCase(blueprintRepoMock, doguInstallRepoMock, globalConfigRepoMock, doguConfigRepoMock, sensitiveDoguConfigRepoMock, configRefReaderMock) // when - err := sut.DetermineStateDiff(testCtx, "testBlueprint1") + err := sut.DetermineStateDiff(testCtx, blueprint) // then require.NoError(t, err) + // only changes are expected expectedConfigDiff := []domain.GlobalConfigEntryDiff{ { Key: "globalKey1", - Actual: domain.GlobalConfigValueState{Value: "", Exists: false}, - Expected: domain.GlobalConfigValueState{Value: "globalValue", Exists: true}, + Actual: domain.GlobalConfigValueState{Value: nil, Exists: false}, + Expected: domain.GlobalConfigValueState{Value: &val1, Exists: true}, NeededAction: domain.ConfigActionSet, }, - { - Key: "globalKey2", - Actual: domain.GlobalConfigValueState{Value: "", Exists: false}, - Expected: domain.GlobalConfigValueState{Value: "", Exists: false}, - NeededAction: domain.ConfigActionNone, - }, } assert.ElementsMatch(t, expectedConfigDiff, blueprint.StateDiff.GlobalConfigDiffs) }) t.Run("should succeed for dogu config diff", func(t *testing.T) { // given blueprint := &domain.BlueprintSpec{ - Id: "testBlueprint1", + Id: "testBlueprint1", + Conditions: []domain.Condition{}, EffectiveBlueprint: domain.EffectiveBlueprint{ Config: domain.Config{ - Dogus: map[cescommons.SimpleName]domain.CombinedDoguConfig{ - nginxStaticQualifiedDoguName.SimpleName: { - DoguName: nginxStaticQualifiedDoguName.SimpleName, - Config: domain.DoguConfig{ - Present: map[common.DoguConfigKey]common.DoguConfigValue{ - nginxStaticConfigKeyNginxKey1: "nginxVal1", - }, - Absent: []common.DoguConfigKey{ - nginxStaticConfigKeyNginxKey2, - }, + Dogus: map[cescommons.SimpleName]domain.DoguConfigEntries{ + ldapQualifiedDoguName.SimpleName: { + { + Key: ldapConfigKeyNginxKey1.Key, + Value: (*config.Value)(&val3), + }, + { + Key: ldapConfigKeyNginxKey2.Key, + Absent: true, }, - SensitiveConfig: domain.SensitiveDoguConfig{}, }, }, }, }, - Status: domain.StatusPhaseValidated, } blueprintRepoMock := newMockBlueprintSpecRepository(t) - blueprintRepoMock.EXPECT().GetById(testCtx, "testBlueprint1").Return(blueprint, nil) blueprintRepoMock.EXPECT().Update(testCtx, blueprint).Return(nil) doguInstallRepoMock := newMockDoguInstallationRepository(t) installedDogus := map[cescommons.SimpleName]*ecosystem.DoguInstallation{ - "nginx-static": {Name: nginxStaticQualifiedDoguName, Version: mustParseVersion(t, "1.8.6")}, + "ldap": {Name: ldapQualifiedDoguName, Version: mustParseVersion(t, "1.8.6")}, } doguInstallRepoMock.EXPECT().GetAll(testCtx).Return(installedDogus, nil) - componentInstallRepoMock := newMockComponentInstallationRepository(t) - componentInstallRepoMock.EXPECT().GetAll(testCtx).Return(nil, nil) globalConfigRepoMock := newMockGlobalConfigRepository(t) entries, _ := config.MapToEntries(map[string]any{}) @@ -569,16 +430,16 @@ func TestStateDiffUseCase_DetermineStateDiff(t *testing.T) { doguConfigRepoMock := newMockDoguConfigRepository(t) doguConfigEntries, entryErr := config.MapToEntries(map[string]any{ - "nginxKey1": "val1", - "nginxKey2": "val2", + "ldapKey1": val1, + "ldapKey2": val2, }) require.NoError(t, entryErr) doguConfigRepoMock.EXPECT(). GetAllExisting(testCtx, []cescommons.SimpleName{ - nginxStatic, + ldap, }). Return(map[cescommons.SimpleName]config.DoguConfig{ - nginxStatic: config.CreateDoguConfig(nginxStatic, doguConfigEntries), + ldap: config.CreateDoguConfig(ldap, doguConfigEntries), }, nil) sensitiveDoguConfigRepoMock := newMockSensitiveDoguConfigRepository(t) @@ -588,26 +449,26 @@ func TestStateDiffUseCase_DetermineStateDiff(t *testing.T) { GetValues(testCtx, map[common.DoguConfigKey]domain.SensitiveValueRef{}). Return(map[common.DoguConfigKey]config.Value{}, nil) - sut := NewStateDiffUseCase(blueprintRepoMock, doguInstallRepoMock, componentInstallRepoMock, globalConfigRepoMock, doguConfigRepoMock, sensitiveDoguConfigRepoMock, configRefReaderMock) + sut := NewStateDiffUseCase(blueprintRepoMock, doguInstallRepoMock, globalConfigRepoMock, doguConfigRepoMock, sensitiveDoguConfigRepoMock, configRefReaderMock) // when - err := sut.DetermineStateDiff(testCtx, "testBlueprint1") + err := sut.DetermineStateDiff(testCtx, blueprint) // then require.NoError(t, err) expectedConfigDiff := map[cescommons.SimpleName]domain.DoguConfigDiffs{ - nginxStatic: { + ldap: { domain.DoguConfigEntryDiff{ - Key: nginxStaticConfigKeyNginxKey1, - Actual: domain.DoguConfigValueState{Value: "val1", Exists: true}, - Expected: domain.DoguConfigValueState{Value: "nginxVal1", Exists: true}, + Key: ldapConfigKeyNginxKey1, + Actual: domain.DoguConfigValueState{Value: &val1, Exists: true}, + Expected: domain.DoguConfigValueState{Value: &val3, Exists: true}, NeededAction: domain.ConfigActionSet, }, domain.DoguConfigEntryDiff{ - Key: nginxStaticConfigKeyNginxKey2, - Actual: domain.DoguConfigValueState{Value: "val2", Exists: true}, - Expected: domain.DoguConfigValueState{Value: "", Exists: false}, + Key: ldapConfigKeyNginxKey2, + Actual: domain.DoguConfigValueState{Value: &val2, Exists: true}, + Expected: domain.DoguConfigValueState{Value: nil, Exists: false}, NeededAction: domain.ConfigActionRemove, }, }, @@ -617,41 +478,39 @@ func TestStateDiffUseCase_DetermineStateDiff(t *testing.T) { t.Run("should succeed for sensitive dogu config diff", func(t *testing.T) { // given blueprint := &domain.BlueprintSpec{ - Id: "testBlueprint1", + Id: "testBlueprint1", + Conditions: []domain.Condition{}, EffectiveBlueprint: domain.EffectiveBlueprint{ Config: domain.Config{ - Dogus: map[cescommons.SimpleName]domain.CombinedDoguConfig{ - nginxStatic: { - DoguName: nginxStatic, - SensitiveConfig: domain.SensitiveDoguConfig{ - Present: map[common.SensitiveDoguConfigKey]domain.SensitiveValueRef{ - nginxStaticSensitiveConfigKeyNginxKey1: { - SecretName: "nginx-conf", - SecretKey: "nginxKey1", - }, //"nginxVal1" - }, - Absent: []common.SensitiveDoguConfigKey{ - nginxStaticSensitiveConfigKeyNginxKey2, - }, + Dogus: map[cescommons.SimpleName]domain.DoguConfigEntries{ + ldap: { + { + Key: ldapSensitiveConfigKeyNginxKey1.Key, + Sensitive: true, + SecretRef: &domain.SensitiveValueRef{ + SecretName: "ldap-conf", + SecretKey: "ldapKey1", + }, // val3 + }, + { + Key: ldapSensitiveConfigKeyNginxKey2.Key, + Sensitive: true, + Absent: true, }, }, }, }, }, - Status: domain.StatusPhaseValidated, } blueprintRepoMock := newMockBlueprintSpecRepository(t) - blueprintRepoMock.EXPECT().GetById(testCtx, "testBlueprint1").Return(blueprint, nil) blueprintRepoMock.EXPECT().Update(testCtx, blueprint).Return(nil) doguInstallRepoMock := newMockDoguInstallationRepository(t) installedDogus := map[cescommons.SimpleName]*ecosystem.DoguInstallation{ - "nginx-static": {Name: nginxStaticQualifiedDoguName, Version: mustParseVersion(t, "1.8.6")}, + "ldap": {Name: ldapQualifiedDoguName, Version: mustParseVersion(t, "1.8.6")}, } doguInstallRepoMock.EXPECT().GetAll(testCtx).Return(installedDogus, nil) - componentInstallRepoMock := newMockComponentInstallationRepository(t) - componentInstallRepoMock.EXPECT().GetAll(testCtx).Return(nil, nil) globalConfigRepoMock := newMockGlobalConfigRepository(t) entries, _ := config.MapToEntries(map[string]any{}) @@ -663,47 +522,47 @@ func TestStateDiffUseCase_DetermineStateDiff(t *testing.T) { sensitiveDoguConfigRepoMock := newMockSensitiveDoguConfigRepository(t) doguConfigEntries, entryErr := config.MapToEntries(map[string]any{ - "nginxKey1": "val1", - "nginxKey2": "val2", + "ldapKey1": val1, + "ldapKey2": val2, }) require.NoError(t, entryErr) sensitiveDoguConfigRepoMock.EXPECT(). GetAllExisting(testCtx, []cescommons.SimpleName{ - nginxStatic, + ldap, }). Return(map[cescommons.SimpleName]config.DoguConfig{ - nginxStatic: config.CreateDoguConfig(nginxStatic, doguConfigEntries), + ldap: config.CreateDoguConfig(ldap, doguConfigEntries), }, nil) configRefReaderMock := newMockSensitiveConfigRefReader(t) configRefReaderMock.EXPECT(). GetValues( testCtx, - blueprint.EffectiveBlueprint.Config.Dogus[nginxStatic].SensitiveConfig.Present, + blueprint.EffectiveBlueprint.Config.GetSensitiveConfigReferences(), ). Return(map[common.DoguConfigKey]config.Value{ - nginxStaticConfigKeyNginxKey1: "nginxVal1", + ldapConfigKeyNginxKey1: config.Value(val3), }, nil) - sut := NewStateDiffUseCase(blueprintRepoMock, doguInstallRepoMock, componentInstallRepoMock, globalConfigRepoMock, doguConfigRepoMock, sensitiveDoguConfigRepoMock, configRefReaderMock) + sut := NewStateDiffUseCase(blueprintRepoMock, doguInstallRepoMock, globalConfigRepoMock, doguConfigRepoMock, sensitiveDoguConfigRepoMock, configRefReaderMock) // when - err := sut.DetermineStateDiff(testCtx, "testBlueprint1") + err := sut.DetermineStateDiff(testCtx, blueprint) // then require.NoError(t, err) expectedConfigDiff := map[cescommons.SimpleName]domain.DoguConfigDiffs{ - nginxStatic: { + ldap: { { - Key: nginxStaticSensitiveConfigKeyNginxKey1, - Actual: domain.DoguConfigValueState{Value: "val1", Exists: true}, - Expected: domain.DoguConfigValueState{Value: "nginxVal1", Exists: true}, + Key: ldapSensitiveConfigKeyNginxKey1, + Actual: domain.DoguConfigValueState{Value: &val1, Exists: true}, + Expected: domain.DoguConfigValueState{Value: &val3, Exists: true}, NeededAction: domain.ConfigActionSet, }, { - Key: nginxStaticSensitiveConfigKeyNginxKey2, - Actual: domain.DoguConfigValueState{Value: "val2", Exists: true}, - Expected: domain.DoguConfigValueState{Value: "", Exists: false}, + Key: ldapSensitiveConfigKeyNginxKey2, + Actual: domain.DoguConfigValueState{Value: &val2, Exists: true}, + Expected: domain.DoguConfigValueState{Value: nil, Exists: false}, NeededAction: domain.ConfigActionRemove, }, }, @@ -718,41 +577,49 @@ func mustParseVersion(t *testing.T, raw string) core.Version { return version } +func mustParseVersionToPtr(t *testing.T, raw string) *core.Version { + version := mustParseVersion(t, raw) + return &version +} + func TestStateDiffUseCase_collectEcosystemState(t *testing.T) { t.Run("all ok", func(t *testing.T) { // given effectiveBlueprint := domain.EffectiveBlueprint{ Config: domain.Config{ - Global: domain.GlobalConfig{ - Present: map[common.GlobalConfigKey]common.GlobalConfigValue{ - "globalKey1": "globalValue", + Global: domain.GlobalConfigEntries{ + { + Key: "globalKey1", + Value: (*config.Value)(&val1), }, - Absent: []common.GlobalConfigKey{ - "globalKey2", + { + Key: "globalKey2", + Absent: true, }, }, - Dogus: map[cescommons.SimpleName]domain.CombinedDoguConfig{ - nginxStaticQualifiedDoguName.SimpleName: { - DoguName: nginxStaticQualifiedDoguName.SimpleName, - Config: domain.DoguConfig{ - Present: map[common.DoguConfigKey]common.DoguConfigValue{ - nginxStaticConfigKeyNginxKey1: "nginxVal1", - }, - Absent: []common.DoguConfigKey{ - nginxStaticConfigKeyNginxKey2, - }, + Dogus: map[cescommons.SimpleName]domain.DoguConfigEntries{ + ldapQualifiedDoguName.SimpleName: { + { + Key: ldapConfigKeyNginxKey1.Key, + Value: (*config.Value)(&val1), }, - SensitiveConfig: domain.SensitiveDoguConfig{ - Present: map[common.SensitiveDoguConfigKey]domain.SensitiveValueRef{ - nginxStaticSensitiveConfigKeyNginxKey1: { - SecretName: "nginx-conf", - SecretKey: "nginxKey1", - }, //"nginxVal1" - }, - Absent: []common.SensitiveDoguConfigKey{ - nginxStaticSensitiveConfigKeyNginxKey2, + { + Key: ldapConfigKeyNginxKey2.Key, + Absent: true, + }, + { + Key: ldapSensitiveConfigKeyNginxKey1.Key, + Sensitive: true, + SecretRef: &domain.SensitiveValueRef{ + SecretName: "nginx-conf", + SecretKey: "nginxKey1", }, }, + { + Key: ldapSensitiveConfigKeyNginxKey2.Key, + Sensitive: true, + Absent: true, + }, }, }, }, @@ -760,8 +627,6 @@ func TestStateDiffUseCase_collectEcosystemState(t *testing.T) { doguInstallRepoMock := newMockDoguInstallationRepository(t) doguInstallRepoMock.EXPECT().GetAll(testCtx).Return(nil, nil) - componentInstallRepoMock := newMockComponentInstallationRepository(t) - componentInstallRepoMock.EXPECT().GetAll(testCtx).Return(nil, nil) globalConfigRepoMock := newMockGlobalConfigRepository(t) entries, _ := config.MapToEntries(map[string]any{}) @@ -774,17 +639,17 @@ func TestStateDiffUseCase_collectEcosystemState(t *testing.T) { Return(map[cescommons.SimpleName]config.DoguConfig{}, nil) sensitiveDoguConfigRepoMock := newMockSensitiveDoguConfigRepository(t) - nginxStaticConfig := config.CreateDoguConfig(nginxStatic, map[config.Key]config.Value{ - "nginxKey1": "val1", + ldapConfig := config.CreateDoguConfig(ldap, map[config.Key]config.Value{ + "ldapKey1": "val1", }) sensitiveDoguConfigRepoMock.EXPECT(). GetAllExisting(testCtx, effectiveBlueprint.Config.GetDogusWithChangedSensitiveConfig()). Return(map[cescommons.SimpleName]config.DoguConfig{ - nginxStatic: nginxStaticConfig, + ldap: ldapConfig, }, nil) configRefReaderMock := newMockSensitiveConfigRefReader(t) - sut := NewStateDiffUseCase(nil, doguInstallRepoMock, componentInstallRepoMock, globalConfigRepoMock, doguConfigRepoMock, sensitiveDoguConfigRepoMock, configRefReaderMock) + sut := NewStateDiffUseCase(nil, doguInstallRepoMock, globalConfigRepoMock, doguConfigRepoMock, sensitiveDoguConfigRepoMock, configRefReaderMock) // when ecosystemState, err := sut.collectEcosystemState(testCtx, effectiveBlueprint) @@ -796,7 +661,7 @@ func TestStateDiffUseCase_collectEcosystemState(t *testing.T) { GlobalConfig: globalConfig, ConfigByDogu: map[cescommons.SimpleName]config.DoguConfig{}, SensitiveConfigByDogu: map[cescommons.SimpleName]config.DoguConfig{ - nginxStatic: nginxStaticConfig, + ldap: ldapConfig, }, }, ecosystemState) }) @@ -804,36 +669,39 @@ func TestStateDiffUseCase_collectEcosystemState(t *testing.T) { // given effectiveBlueprint := domain.EffectiveBlueprint{ Config: domain.Config{ - Global: domain.GlobalConfig{ - Present: map[common.GlobalConfigKey]common.GlobalConfigValue{ - "globalKey1": "globalValue", + Global: domain.GlobalConfigEntries{ + { + Key: "globalKey1", + Value: (*config.Value)(&val1), }, - Absent: []common.GlobalConfigKey{ - "globalKey2", + { + Key: "globalKey2", + Absent: true, }, }, - Dogus: map[cescommons.SimpleName]domain.CombinedDoguConfig{ - nginxStaticQualifiedDoguName.SimpleName: { - DoguName: nginxStaticQualifiedDoguName.SimpleName, - Config: domain.DoguConfig{ - Present: map[common.DoguConfigKey]common.DoguConfigValue{ - nginxStaticConfigKeyNginxKey1: "nginxVal1", - }, - Absent: []common.DoguConfigKey{ - nginxStaticConfigKeyNginxKey2, - }, + Dogus: map[cescommons.SimpleName]domain.DoguConfigEntries{ + ldapQualifiedDoguName.SimpleName: { + { + Key: ldapConfigKeyNginxKey1.Key, + Value: (*config.Value)(&val1), }, - SensitiveConfig: domain.SensitiveDoguConfig{ - Present: map[common.SensitiveDoguConfigKey]domain.SensitiveValueRef{ - nginxStaticSensitiveConfigKeyNginxKey1: { - SecretName: "nginx-conf", - SecretKey: "nginxKey1", - }, //"nginxVal1" - }, - Absent: []common.SensitiveDoguConfigKey{ - nginxStaticSensitiveConfigKeyNginxKey2, + { + Key: ldapConfigKeyNginxKey2.Key, + Absent: true, + }, + { + Key: ldapSensitiveConfigKeyNginxKey1.Key, + Sensitive: true, + SecretRef: &domain.SensitiveValueRef{ + SecretName: "nginx-conf", + SecretKey: "nginxKey1", }, }, + { + Key: ldapSensitiveConfigKeyNginxKey2.Key, + Sensitive: true, + Absent: true, + }, }, }, }, @@ -843,8 +711,6 @@ func TestStateDiffUseCase_collectEcosystemState(t *testing.T) { doguInstallRepoMock := newMockDoguInstallationRepository(t) doguInstallRepoMock.EXPECT().GetAll(testCtx).Return(nil, nil) - componentInstallRepoMock := newMockComponentInstallationRepository(t) - componentInstallRepoMock.EXPECT().GetAll(testCtx).Return(nil, nil) globalConfigRepoMock := newMockGlobalConfigRepository(t) entries, _ := config.MapToEntries(map[string]any{}) @@ -861,7 +727,7 @@ func TestStateDiffUseCase_collectEcosystemState(t *testing.T) { Return(map[cescommons.SimpleName]config.DoguConfig{}, internalTestError) configRefReaderMock := newMockSensitiveConfigRefReader(t) - sut := NewStateDiffUseCase(nil, doguInstallRepoMock, componentInstallRepoMock, globalConfigRepoMock, doguConfigRepoMock, sensitiveDoguConfigRepoMock, configRefReaderMock) + sut := NewStateDiffUseCase(nil, doguInstallRepoMock, globalConfigRepoMock, doguConfigRepoMock, sensitiveDoguConfigRepoMock, configRefReaderMock) // when ecosystemState, err := sut.collectEcosystemState(testCtx, effectiveBlueprint) diff --git a/pkg/bootstrap.go b/pkg/bootstrap.go index bcbbd7e1..052551b7 100644 --- a/pkg/bootstrap.go +++ b/pkg/bootstrap.go @@ -2,29 +2,25 @@ package pkg import ( "fmt" + adapterconfigk8s "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/adapter/config/kubernetes" v2 "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/adapter/kubernetes/blueprintcr/v2" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/adapter/kubernetes/restartcr" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/adapter/kubernetes/sensitiveconfigref" + "github.com/cloudogu/k8s-registry-lib/dogu" "github.com/cloudogu/k8s-registry-lib/repository" remotedogudescriptor "github.com/cloudogu/remote-dogu-descriptor-lib/repository" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/common" - "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "k8s.io/client-go/tools/record" adapterk8s "github.com/cloudogu/k8s-blueprint-lib/v2/client" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/adapter/doguregistry" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/adapter/kubernetes/componentcr" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/adapter/kubernetes/dogucr" - adapterhealthconfig "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/adapter/kubernetes/healthConfig" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/adapter/reconciler" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/application" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/config" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domainservice" - componentEcoClient "github.com/cloudogu/k8s-component-operator/pkg/api/ecosystem" doguEcoClient "github.com/cloudogu/k8s-dogu-lib/v2/client" ) @@ -33,11 +29,6 @@ type ApplicationContext struct { Reconciler *reconciler.BlueprintReconciler } -var blueprintOperatorName = common.QualifiedComponentName{ - Namespace: "k8s", - SimpleName: "k8s-blueprint-operator", -} - // Bootstrap creates the ApplicationContext and does all dependency injection of the whole application. func Bootstrap(restConfig *rest.Config, eventRecorder record.EventRecorder, namespace string) (*ApplicationContext, error) { ecosystemClientSet, err := createEcosystemClientSet(restConfig) @@ -49,16 +40,12 @@ func Bootstrap(restConfig *rest.Config, eventRecorder record.EventRecorder, name if err != nil { return nil, fmt.Errorf("failed to create dogus interface: %w", err) } - componentsInterface, err := componentEcoClient.NewForConfig(restConfig) - if err != nil { - return nil, fmt.Errorf("failed to create components interface: %w", err) - } - blueprintSpecRepository := v2.NewBlueprintSpecRepository( + blueprintRepo := v2.NewBlueprintSpecRepository( ecosystemClientSet.EcosystemV1Alpha1().Blueprints(namespace), eventRecorder, ) - remoteDoguRegistry, err := createRemoteDoguRegistry() + remoteDoguRegistry, err := createRemoteDoguRegistry(ecosystemClientSet, namespace) if err != nil { return nil, err } @@ -71,33 +58,36 @@ func Bootstrap(restConfig *rest.Config, eventRecorder record.EventRecorder, name k8sGlobalConfigRepo := repository.NewGlobalConfigRepository(ecosystemClientSet.CoreV1().ConfigMaps(namespace)) globalConfigRepoAdapter := adapterconfigk8s.NewGlobalConfigRepository(*k8sGlobalConfigRepo) - doguInstallationRepo := dogucr.NewDoguInstallationRepo(dogusInterface.Dogus(namespace)) - componentInstallationRepo := componentcr.NewComponentInstallationRepo(componentsInterface.Components(namespace)) - healthConfigRepo := adapterhealthconfig.NewHealthConfigProvider(ecosystemClientSet.CoreV1().ConfigMaps(namespace)) - doguRestartAdapter := dogusInterface.DoguRestarts(namespace) - restartRepository := restartcr.NewDoguRestartRepository(doguRestartAdapter) + doguRepo := dogucr.NewDoguInstallationRepo(dogusInterface.Dogus(namespace)) + initialBlueprintStateUseCase := application.NewInitiateBlueprintStatusUseCase(blueprintRepo) validateDependenciesUseCase := domainservice.NewValidateDependenciesDomainUseCase(remoteDoguRegistry) validateMountsUseCase := domainservice.NewValidateAdditionalMountsDomainUseCase(remoteDoguRegistry) - blueprintValidationUseCase := application.NewBlueprintSpecValidationUseCase(blueprintSpecRepository, validateDependenciesUseCase, validateMountsUseCase) - effectiveBlueprintUseCase := application.NewEffectiveBlueprintUseCase(blueprintSpecRepository) - stateDiffUseCase := application.NewStateDiffUseCase(blueprintSpecRepository, doguInstallationRepo, componentInstallationRepo, globalConfigRepoAdapter, doguConfigRepo, sensitiveDoguConfigRepo, sensitiveConfigRefReader) - doguInstallationUseCase := application.NewDoguInstallationUseCase(blueprintSpecRepository, doguInstallationRepo, healthConfigRepo) - componentInstallationUseCase := application.NewComponentInstallationUseCase(blueprintSpecRepository, componentInstallationRepo, healthConfigRepo) - ecosystemHealthUseCase := application.NewEcosystemHealthUseCase(doguInstallationUseCase, componentInstallationUseCase) - applyBlueprintSpecUseCase := application.NewApplyBlueprintSpecUseCase(blueprintSpecRepository, doguInstallationUseCase, ecosystemHealthUseCase, componentInstallationUseCase) - ConfigUseCase := application.NewEcosystemConfigUseCase(blueprintSpecRepository, doguConfigRepo, sensitiveDoguConfigRepo, globalConfigRepoAdapter) - doguRestartUseCase := application.NewDoguRestartUseCase(doguInstallationRepo, blueprintSpecRepository, restartRepository) - - selfUpgradeUseCase := application.NewSelfUpgradeUseCase(blueprintSpecRepository, componentInstallationRepo, componentInstallationUseCase, blueprintOperatorName.SimpleName, healthConfigRepo) - - blueprintChangeUseCase := application.NewBlueprintSpecChangeUseCase( - blueprintSpecRepository, blueprintValidationUseCase, - effectiveBlueprintUseCase, stateDiffUseCase, - applyBlueprintSpecUseCase, ConfigUseCase, - doguRestartUseCase, - selfUpgradeUseCase, + blueprintValidationUseCase := application.NewBlueprintSpecValidationUseCase(blueprintRepo, validateDependenciesUseCase, validateMountsUseCase) + effectiveBlueprintUseCase := application.NewEffectiveBlueprintUseCase(blueprintRepo) + stateDiffUseCase := application.NewStateDiffUseCase(blueprintRepo, doguRepo, globalConfigRepoAdapter, doguConfigRepo, sensitiveDoguConfigRepo, sensitiveConfigRefReader) + doguInstallationUseCase := application.NewDoguInstallationUseCase(blueprintRepo, doguRepo, doguConfigRepo, globalConfigRepoAdapter) + ecosystemHealthUseCase := application.NewEcosystemHealthUseCase(doguInstallationUseCase, blueprintRepo) + completeBlueprintSpecUseCase := application.NewCompleteBlueprintUseCase(blueprintRepo) + applyDogusUseCase := application.NewApplyDogusUseCase(blueprintRepo, doguInstallationUseCase) + ConfigUseCase := application.NewEcosystemConfigUseCase(blueprintRepo, doguConfigRepo, sensitiveDoguConfigRepo, globalConfigRepoAdapter, doguRepo) + dogusUpToDateUseCase := application.NewDogusUpToDateUseCase(blueprintRepo, doguInstallationUseCase) + + preparationUseCases := application.NewBlueprintPreparationUseCase( + initialBlueprintStateUseCase, + blueprintValidationUseCase, + effectiveBlueprintUseCase, + stateDiffUseCase, + ecosystemHealthUseCase, ) + applyUseCases := application.NewBlueprintApplyUseCase( + completeBlueprintSpecUseCase, + ConfigUseCase, + applyDogusUseCase, + ecosystemHealthUseCase, + dogusUpToDateUseCase, + ) + blueprintChangeUseCase := application.NewBlueprintSpecChangeUseCase(blueprintRepo, preparationUseCases, applyUseCases) blueprintReconciler := reconciler.NewBlueprintReconciler(blueprintChangeUseCase) return &ApplicationContext{ @@ -105,7 +95,7 @@ func Bootstrap(restConfig *rest.Config, eventRecorder record.EventRecorder, name }, nil } -func createRemoteDoguRegistry() (*doguregistry.Remote, error) { +func createRemoteDoguRegistry(clientSet *adapterk8s.ClientSet, namespace string) (*doguregistry.DoguDescriptorRepository, error) { remoteConfig, err := config.GetRemoteConfiguration() if err != nil { return nil, fmt.Errorf("failed to get remote dogu registry config: %w", err) @@ -117,11 +107,11 @@ func createRemoteDoguRegistry() (*doguregistry.Remote, error) { } doguRemoteRepository, err := remotedogudescriptor.NewRemoteDoguDescriptorRepository(remoteConfig, remoteCreds) + doguLocalRepository := dogu.NewLocalDoguDescriptorRepository(clientSet.CoreV1().ConfigMaps(namespace)) if err != nil { return nil, fmt.Errorf("failed to create new remote dogu repository: %w", err) } - - return doguregistry.NewRemote(doguRemoteRepository), nil + return doguregistry.NewDoguDescriptorRepository(doguRemoteRepository, doguLocalRepository), nil } func createEcosystemClientSet(restConfig *rest.Config) (*adapterk8s.ClientSet, error) { diff --git a/pkg/domain/blueprint.go b/pkg/domain/blueprint.go index 1edf7b12..4b6cdcab 100644 --- a/pkg/domain/blueprint.go +++ b/pkg/domain/blueprint.go @@ -3,8 +3,8 @@ package domain import ( "errors" "fmt" + cescommons "github.com/cloudogu/ces-commons-lib/dogu" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/common" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/util" ) @@ -18,9 +18,6 @@ type Blueprint struct { // Dogus contains a set of exact dogu versions which should be present or absent in the CES instance after which this // blueprint was applied. Optional. Dogus []Dogu - // Components contains a set of exact components versions which should be present or absent in the CES instance after which - // this blueprint was applied. Optional. - Components []Component // Config contains all config entries to set via blueprint. Config Config } @@ -30,9 +27,7 @@ func (blueprint *Blueprint) Validate() error { errorList := []error{ blueprint.validateDogus(), blueprint.validateDoguUniqueness(), - blueprint.validateComponents(), - blueprint.validateComponentUniqueness(), - blueprint.Config.validate(), + blueprint.validateConfig(), } err := errors.Join(errorList...) @@ -57,17 +52,6 @@ func (blueprint *Blueprint) validateDoguUniqueness() error { return nil } -func (blueprint *Blueprint) validateComponents() error { - errorList := util.Map(blueprint.Components, func(component Component) error { return component.Validate() }) - return errors.Join(errorList...) -} - -// validateComponentUniqueness checks if components exist twice in the blueprint and returns an error if it's so. -func (blueprint *Blueprint) validateComponentUniqueness() error { - componentNames := util.Map(blueprint.Components, func(component Component) common.SimpleComponentName { return component.Name.SimpleName }) - duplicates := util.GetDuplicates(componentNames) - if len(duplicates) != 0 { - return fmt.Errorf("there are duplicate components: %v", duplicates) - } - return nil +func (blueprint *Blueprint) validateConfig() error { + return blueprint.Config.validate() } diff --git a/pkg/domain/blueprintMask_test.go b/pkg/domain/blueprintMask_test.go index a61f737d..3404522c 100644 --- a/pkg/domain/blueprintMask_test.go +++ b/pkg/domain/blueprintMask_test.go @@ -1,10 +1,11 @@ package domain import ( + "testing" + cescommons "github.com/cloudogu/ces-commons-lib/dogu" "github.com/cloudogu/cesapp-lib/core" "github.com/stretchr/testify/require" - "testing" ) var version3_2_1_0, _ = core.ParseVersion("3.2.1-0") @@ -12,7 +13,6 @@ var version3_2_1_0, _ = core.ParseVersion("3.2.1-0") var ( officialNamespace = cescommons.Namespace("official") k8sNamespace = cescommons.Namespace("k8s") - nginxStatic = cescommons.QualifiedName{Namespace: k8sNamespace, SimpleName: cescommons.SimpleName("nginx-static")} officialDogu1 = cescommons.QualifiedName{Namespace: officialNamespace, SimpleName: cescommons.SimpleName("dogu1")} officialDogu2 = cescommons.QualifiedName{Namespace: officialNamespace, SimpleName: cescommons.SimpleName("dogu2")} officialDogu3 = cescommons.QualifiedName{Namespace: officialNamespace, SimpleName: cescommons.SimpleName("dogu3")} @@ -20,9 +20,9 @@ var ( func Test_Validate(t *testing.T) { dogus := []MaskDogu{ - {Name: officialDogu1, Version: version3_2_1_0, TargetState: TargetStateAbsent}, - {Name: officialDogu2, TargetState: TargetStateAbsent}, - {Name: officialDogu3, Version: version3_2_1_0, TargetState: TargetStatePresent}, + {Name: officialDogu1, Version: version3_2_1_0, Absent: true}, + {Name: officialDogu2, Absent: true}, + {Name: officialDogu3, Version: version3_2_1_0, Absent: false}, } blueprintMask := BlueprintMask{ Dogus: dogus, @@ -35,8 +35,8 @@ func Test_Validate(t *testing.T) { func Test_ValidateWithDuplicatedDoguNames(t *testing.T) { dogus := []MaskDogu{ - {Name: officialDogu1, TargetState: TargetStatePresent}, - {Name: officialDogu1, TargetState: TargetStateAbsent}, + {Name: officialDogu1, Absent: false}, + {Name: officialDogu1, Absent: true}, } blueprintMask := BlueprintMask{Dogus: dogus} diff --git a/pkg/domain/blueprintSpec.go b/pkg/domain/blueprintSpec.go index c0512d1b..b0fb29a1 100644 --- a/pkg/domain/blueprintSpec.go +++ b/pkg/domain/blueprintSpec.go @@ -3,23 +3,27 @@ package domain import ( "errors" "fmt" + "maps" + "slices" + cescommons "github.com/cloudogu/ces-commons-lib/dogu" "github.com/cloudogu/cesapp-lib/core" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/common" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/util" - "maps" - "slices" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) type BlueprintSpec struct { Id string + DisplayName string Blueprint Blueprint BlueprintMask BlueprintMask EffectiveBlueprint EffectiveBlueprint StateDiff StateDiff Config BlueprintConfiguration - Status StatusPhase + Conditions []Condition // PersistenceContext can hold generic values needed for persistence with repositories, e.g. version counters or transaction contexts. // This field has a generic map type as the values within it highly depend on the used type of repository. // This field should be ignored in the whole domain. @@ -27,69 +31,33 @@ type BlueprintSpec struct { Events []Event } -type StatusPhase string +type Condition = metav1.Condition const ( - // StatusPhaseNew marks a newly created blueprint-CR. - StatusPhaseNew StatusPhase = "" - // StatusPhaseStaticallyValidated marks the given blueprint spec as validated. - StatusPhaseStaticallyValidated StatusPhase = "staticallyValidated" - // StatusPhaseValidated marks the given blueprint spec as validated. - StatusPhaseValidated StatusPhase = "validated" - // StatusPhaseEffectiveBlueprintGenerated marks that the effective blueprint was generated out of the blueprint and the mask. - StatusPhaseEffectiveBlueprintGenerated StatusPhase = "effectiveBlueprintGenerated" - // StatusPhaseStateDiffDetermined marks that the diff to the ecosystem state was successfully determined. - StatusPhaseStateDiffDetermined StatusPhase = "stateDiffDetermined" - // StatusPhaseInvalid marks the given blueprint spec is semantically incorrect. - StatusPhaseInvalid StatusPhase = "invalid" - // StatusPhaseEcosystemHealthyUpfront marks that all currently installed dogus are healthy. - StatusPhaseEcosystemHealthyUpfront StatusPhase = "ecosystemHealthyUpfront" - // StatusPhaseEcosystemUnhealthyUpfront marks that some currently installed dogus are unhealthy. - StatusPhaseEcosystemUnhealthyUpfront StatusPhase = "ecosystemUnhealthyUpfront" - // StatusPhaseBlueprintApplicationPreProcessed shows that all pre-processing steps for the blueprint application - // were successful. - StatusPhaseBlueprintApplicationPreProcessed StatusPhase = "blueprintApplicationPreProcessed" - // StatusPhaseAwaitSelfUpgrade marks that the blueprint operator waits for termination for a self upgrade. - StatusPhaseAwaitSelfUpgrade StatusPhase = "awaitSelfUpgrade" - // StatusPhaseSelfUpgradeCompleted marks that the blueprint operator itself got successfully upgraded. - StatusPhaseSelfUpgradeCompleted StatusPhase = "selfUpgradeCompleted" - // StatusPhaseInProgress marks that the blueprint is currently being processed. - StatusPhaseInProgress StatusPhase = "inProgress" - // StatusPhaseBlueprintApplicationFailed shows that the blueprint application failed. - StatusPhaseBlueprintApplicationFailed StatusPhase = "blueprintApplicationFailed" - // StatusPhaseBlueprintApplied indicates that the blueprint was applied but the ecosystem is not healthy yet. - StatusPhaseBlueprintApplied StatusPhase = "blueprintApplied" - // StatusPhaseEcosystemHealthyAfterwards shows that the ecosystem got healthy again after applying the blueprint. - StatusPhaseEcosystemHealthyAfterwards StatusPhase = "ecosystemHealthyAfterwards" - // StatusPhaseEcosystemUnhealthyAfterwards shows that the ecosystem got not healthy again after applying the blueprint. - StatusPhaseEcosystemUnhealthyAfterwards StatusPhase = "ecosystemUnhealthyAfterwards" - // StatusPhaseFailed marks that an error occurred during processing of the blueprint. - StatusPhaseFailed StatusPhase = "failed" - // StatusPhaseCompleted marks the blueprint as successfully applied. - StatusPhaseCompleted StatusPhase = "completed" - // StatusPhaseApplyEcosystemConfig indicates that the apply ecosystem config phase is active. - StatusPhaseApplyEcosystemConfig StatusPhase = "applyEcosystemConfig" - // StatusPhaseApplyEcosystemConfigFailed indicates that the phase to apply ecosystem config failed. - StatusPhaseApplyEcosystemConfigFailed StatusPhase = "applyEcosystemConfigFailed" - // StatusPhaseEcosystemConfigApplied indicates that the phase to apply ecosystem config succeeded. - StatusPhaseEcosystemConfigApplied StatusPhase = "ecosystemConfigApplied" - // StatusPhaseRestartsTriggered indicates that a restart has been triggered for all Dogus that needed a restart. - // Restarts are needed when the Dogu config changes. - StatusPhaseRestartsTriggered StatusPhase = "restartsTriggered" + ConditionValid = "Valid" + ConditionExecutable = "Executable" + ConditionEcosystemHealthy = "EcosystemHealthy" + ConditionCompleted = "Completed" + ConditionLastApplySucceeded = "LastApplySucceeded" + + ReasonLastApplyErrorAtDogus = "DoguApplyFailure" + ReasonLastApplyErrorAtConfig = "ConfigApplyFailure" ) -// censorValue is the value for censoring sensitive blueprint configuration data. -const censorValue = "*****" +var ( + BlueprintConditions = []string{ConditionValid, ConditionExecutable, ConditionEcosystemHealthy, ConditionCompleted, ConditionLastApplySucceeded} + + // ActionSwitchDoguNamespace is an exception and should be handled with the blueprint config. + notAllowedDoguActions = []Action{ActionDowngrade, ActionSwitchDoguNamespace} +) type BlueprintConfiguration struct { // IgnoreDoguHealth forces blueprint upgrades even if dogus are unhealthy IgnoreDoguHealth bool - // IgnoreComponentHealth forces blueprint upgrades even if components are unhealthy - IgnoreComponentHealth bool // AllowDoguNamespaceSwitch allows the blueprint upgrade to switch a dogus namespace AllowDoguNamespaceSwitch bool - // DryRun lets the user test a blueprint run to check if all attributes of the blueprint are correct and avoid a result with a failure state. - DryRun bool + // Stopped lets the user test a blueprint run to check if all attributes of the blueprint are correct and avoid a result with a failure state. + Stopped bool } // ValidateStatically checks the blueprintSpec for semantic errors and sets the status to the result. @@ -97,13 +65,6 @@ type BlueprintConfiguration struct { // returns a domain.InvalidBlueprintError if blueprint is invalid // or nil otherwise. func (spec *BlueprintSpec) ValidateStatically() error { - switch spec.Status { - case StatusPhaseNew: // continue - case StatusPhaseInvalid: // do not validate again - return &InvalidBlueprintError{Message: "blueprint spec was marked invalid before: do not revalidate"} - default: // do not validate again. for all other status it must be either status validated or a status beyond that - return nil - } var errorList []error if spec.Id == "" { @@ -118,12 +79,17 @@ func (spec *BlueprintSpec) ValidateStatically() error { WrappedError: err, Message: "blueprint spec is invalid", } - spec.Status = StatusPhaseInvalid spec.Events = append(spec.Events, BlueprintSpecInvalidEvent{ValidationError: err}) - } else { - spec.Status = StatusPhaseStaticallyValidated - spec.Events = append(spec.Events, BlueprintSpecStaticallyValidatedEvent{}) + meta.SetStatusCondition(&spec.Conditions, metav1.Condition{ + Type: ConditionValid, + Status: metav1.ConditionFalse, + Reason: "Invalid", + Message: err.Error(), + }) } + // Do not set condition to true here. + // We reuse the condition for the dynamic validation. + // If the blueprint is completely consistent and valid can only be decided there return err } @@ -134,7 +100,7 @@ func (spec *BlueprintSpec) validateMaskAgainstBlueprint() error { if !found { errorList = append(errorList, fmt.Errorf("dogu %q is missing in the blueprint", doguMask.Name)) } - if doguMask.TargetState == TargetStatePresent && dogu.TargetState == TargetStateAbsent { + if !doguMask.Absent && dogu.Absent { errorList = append(errorList, fmt.Errorf("absent dogu %q cannot be present in blueprint mask", dogu.Name.SimpleName)) } if !spec.Config.AllowDoguNamespaceSwitch && dogu.Name.Namespace != doguMask.Name.Namespace { @@ -151,35 +117,34 @@ func (spec *BlueprintSpec) validateMaskAgainstBlueprint() error { return err } -// ValidateDynamically sets the Status either to StatusPhaseInvalid or StatusPhaseValidated +// ValidateDynamically sets the ConditionValid // depending on if the dependencies or versions of the elements in the blueprint are invalid. -// returns a domain.InvalidBlueprintError if blueprint is invalid -// or nil otherwise. +// This function decides completely on the given error, therefore no error will be returned explicitly again. func (spec *BlueprintSpec) ValidateDynamically(possibleInvalidDependenciesError error) { if possibleInvalidDependenciesError != nil { err := &InvalidBlueprintError{ WrappedError: possibleInvalidDependenciesError, Message: "blueprint spec is invalid", } - spec.Status = StatusPhaseInvalid - spec.Events = append(spec.Events, BlueprintSpecInvalidEvent{ValidationError: err}) + conditionChanged := meta.SetStatusCondition(&spec.Conditions, metav1.Condition{ + Type: ConditionValid, + Status: metav1.ConditionFalse, + Reason: "Inconsistent", + Message: err.Error(), + }) + if conditionChanged { + spec.Events = append(spec.Events, BlueprintSpecInvalidEvent{ValidationError: err}) + } } else { - spec.Status = StatusPhaseValidated - spec.Events = append(spec.Events, BlueprintSpecValidatedEvent{}) + meta.SetStatusCondition(&spec.Conditions, metav1.Condition{ + Type: ConditionValid, + Status: metav1.ConditionTrue, + Reason: "Valid", + }) } } func (spec *BlueprintSpec) CalculateEffectiveBlueprint() error { - switch spec.Status { - case StatusPhaseEffectiveBlueprintGenerated: - return nil // do not regenerate effective blueprint - case StatusPhaseNew: // stop - return fmt.Errorf("cannot calculate effective blueprint before the blueprint spec is validated") - case StatusPhaseInvalid: // stop - return fmt.Errorf("cannot calculate effective blueprint on invalid blueprint spec") - default: // continue: StatusPhaseValidated, StatusPhaseInProgress, StatusPhaseFailed, StatusPhaseCompleted - } - effectiveDogus, err := spec.calculateEffectiveDogus() if err != nil { return err @@ -188,18 +153,22 @@ func (spec *BlueprintSpec) CalculateEffectiveBlueprint() error { effectiveConfig := spec.removeConfigForMaskedDogus() spec.EffectiveBlueprint = EffectiveBlueprint{ - Dogus: effectiveDogus, - Components: spec.Blueprint.Components, - Config: effectiveConfig, + Dogus: effectiveDogus, + Config: effectiveConfig, } validationError := spec.EffectiveBlueprint.validateOnlyConfigForDogusInBlueprint() if validationError != nil { - spec.Status = StatusPhaseInvalid - spec.Events = append(spec.Events, BlueprintSpecInvalidEvent{ValidationError: validationError}) + conditionChanged := meta.SetStatusCondition(&spec.Conditions, metav1.Condition{ + Type: ConditionValid, + Status: metav1.ConditionFalse, + Reason: "Inconsistent", + Message: validationError.Error(), + }) + if conditionChanged { + spec.Events = append(spec.Events, BlueprintSpecInvalidEvent{ValidationError: validationError}) + } return validationError } - spec.Status = StatusPhaseEffectiveBlueprintGenerated - spec.Events = append(spec.Events, EffectiveBlueprintCalculatedEvent{}) return nil } @@ -219,7 +188,7 @@ func (spec *BlueprintSpec) calculateEffectiveDogu(dogu Dogu) (Dogu, error) { effectiveDogu := Dogu{ Name: dogu.Name, Version: dogu.Version, - TargetState: dogu.TargetState, + Absent: dogu.Absent, MinVolumeSize: dogu.MinVolumeSize, ReverseProxyConfig: dogu.ReverseProxyConfig, AdditionalMounts: dogu.AdditionalMounts, @@ -228,17 +197,17 @@ func (spec *BlueprintSpec) calculateEffectiveDogu(dogu Dogu) (Dogu, error) { if noMaskDoguErr == nil { emptyVersion := core.Version{} if maskDogu.Version != emptyVersion { - effectiveDogu.Version = maskDogu.Version + effectiveDogu.Version = &maskDogu.Version } if maskDogu.Name.Namespace != dogu.Name.Namespace { if spec.Config.AllowDoguNamespaceSwitch { - effectiveDogu.Name.Namespace = cescommons.Namespace(maskDogu.Name.Namespace) + effectiveDogu.Name.Namespace = maskDogu.Name.Namespace } else { return Dogu{}, fmt.Errorf( "changing the dogu namespace is forbidden by default and can be allowed by a flag: %q -> %q", dogu.Name, maskDogu.Name) } } - effectiveDogu.TargetState = maskDogu.TargetState + effectiveDogu.Absent = maskDogu.Absent } return effectiveDogu, nil @@ -249,7 +218,7 @@ func (spec *BlueprintSpec) removeConfigForMaskedDogus() Config { effectiveDoguConfig := maps.Clone(spec.Blueprint.Config.Dogus) for _, dogu := range spec.BlueprintMask.Dogus { - if dogu.TargetState == TargetStateAbsent { + if dogu.Absent { delete(effectiveDoguConfig, dogu.Name.SimpleName) } } @@ -260,15 +229,17 @@ func (spec *BlueprintSpec) removeConfigForMaskedDogus() Config { } } -// MarkInvalid is used to mark the blueprint as invalid after dynamically validating it. -func (spec *BlueprintSpec) MarkInvalid(err error) { - spec.Status = StatusPhaseInvalid - spec.Events = append(spec.Events, BlueprintSpecInvalidEvent{ValidationError: err}) -} - // MissingConfigReferences adds a given error, which was caused during preparations for determining the state diff func (spec *BlueprintSpec) MissingConfigReferences(error error) { - spec.Events = append(spec.Events, NewMissingConfigReferencesEvent(error)) + conditionChanged := meta.SetStatusCondition(&spec.Conditions, metav1.Condition{ + Type: ConditionExecutable, + Status: metav1.ConditionFalse, + Reason: "MissingConfigReferences", + Message: error.Error(), + }) + if conditionChanged { + spec.Events = append(spec.Events, NewMissingConfigReferencesEvent(error)) + } } // DetermineStateDiff creates the StateDiff between the blueprint and the actual state of the ecosystem. @@ -279,27 +250,9 @@ func (spec *BlueprintSpec) MissingConfigReferences(error error) { // returns an error if the BlueprintSpec is not in the necessary state to determine the stateDiff. func (spec *BlueprintSpec) DetermineStateDiff( ecosystemState ecosystem.EcosystemState, - referencedSensitiveConfig map[common.SensitiveDoguConfigKey]common.SensitiveDoguConfigValue, + referencedSensitiveConfig map[common.DoguConfigKey]common.SensitiveDoguConfigValue, ) error { - switch spec.Status { - case StatusPhaseNew: - fallthrough - case StatusPhaseStaticallyValidated: - fallthrough - case StatusPhaseEffectiveBlueprintGenerated: - return fmt.Errorf("cannot determine state diff in status phase %q", spec.Status) - case StatusPhaseValidated: // this is the state, the blueprint spec should be - default: - return nil // do not re-determine the state diff from status StatusPhaseStateDiffDetermined and above - } - doguDiffs := determineDoguDiffs(spec.EffectiveBlueprint.Dogus, ecosystemState.InstalledDogus) - compDiffs, err := determineComponentDiffs(spec.EffectiveBlueprint.Components, ecosystemState.InstalledComponents) - if err != nil { - // FIXME: a proper state and event should be set, so that this error don't lead to an endless retry. - // we need to analyze first, what kind of error this is. Why do we need one? - return err - } doguConfigDiffs, sensitiveDoguConfigDiffs, globalConfigDiffs := determineConfigDiffs( spec.EffectiveBlueprint.Config, ecosystemState.GlobalConfig, @@ -310,145 +263,109 @@ func (spec *BlueprintSpec) DetermineStateDiff( spec.StateDiff = StateDiff{ DoguDiffs: doguDiffs, - ComponentDiffs: compDiffs, DoguConfigDiffs: doguConfigDiffs, SensitiveDoguConfigDiffs: sensitiveDoguConfigDiffs, GlobalConfigDiffs: globalConfigDiffs, } - spec.Events = append(spec.Events, newStateDiffDoguEvent(spec.StateDiff.DoguDiffs)) - spec.Events = append(spec.Events, newStateDiffComponentEvent(spec.StateDiff.ComponentDiffs)) - spec.Events = append(spec.Events, GlobalConfigDiffDeterminedEvent{GlobalConfigDiffs: spec.StateDiff.GlobalConfigDiffs}) - spec.Events = append(spec.Events, NewDoguConfigDiffDeterminedEvent(spec.StateDiff.DoguConfigDiffs)) - spec.Events = append(spec.Events, NewSensitiveDoguConfigDiffDeterminedEvent(spec.StateDiff.SensitiveDoguConfigDiffs)) + spec.resetCompletedConditionAfterStateDiff() + if spec.StateDiff.DoguDiffs.HasChanges() { + spec.Events = append(spec.Events, newStateDiffEvent(spec.StateDiff)) + } invalidBlueprintError := spec.validateStateDiff() if invalidBlueprintError != nil { - spec.Status = StatusPhaseInvalid - spec.Events = append(spec.Events, BlueprintSpecInvalidEvent{ValidationError: invalidBlueprintError}) + conditionChanged := meta.SetStatusCondition(&spec.Conditions, metav1.Condition{ + Type: ConditionExecutable, + Status: metav1.ConditionFalse, + Reason: "ForbiddenOperations", + Message: invalidBlueprintError.Error(), + }) + if conditionChanged { + spec.Events = append(spec.Events, BlueprintSpecInvalidEvent{ValidationError: invalidBlueprintError}) + } return invalidBlueprintError } - spec.Status = StatusPhaseStateDiffDetermined - + meta.SetStatusCondition(&spec.Conditions, metav1.Condition{ + Type: ConditionExecutable, + Status: metav1.ConditionTrue, + Reason: "Executable", + }) return nil } -// CheckEcosystemHealthUpfront checks if the ecosystem is healthy with the given health result and sets the next status phase depending on that. -func (spec *BlueprintSpec) CheckEcosystemHealthUpfront(healthResult ecosystem.HealthResult) error { - // healthResult does not contain dogu info if IgnoreDoguHealth flag is set. (no need to load all doguInstallations then) - // Therefore we don't need to exclude dogus while checking with AllHealthy() - if healthResult.AllHealthy() { - spec.Status = StatusPhaseEcosystemHealthyUpfront - spec.Events = append(spec.Events, EcosystemHealthyUpfrontEvent{doguHealthIgnored: spec.Config.IgnoreDoguHealth, - componentHealthIgnored: spec.Config.IgnoreComponentHealth}) - return nil - } else { - //TODO: set health condition here in the future - spec.Events = append(spec.Events, EcosystemUnhealthyUpfrontEvent{HealthResult: healthResult}) - return NewUnhealthyEcosystemError(nil, "ecosystem is unhealthy before applying the blueprint", healthResult) +// HandleHealthResult sets the healthCondition accordingly to the healthResult and a possible error. +// if an error is given, the condition will be set to unknown. +// The function returns true if the condition changed, otherwise false. +func (spec *BlueprintSpec) HandleHealthResult(healthResult ecosystem.HealthResult, err error) bool { + if err != nil { + conditionChanged := meta.SetStatusCondition(&spec.Conditions, metav1.Condition{ + Type: ConditionEcosystemHealthy, + Status: metav1.ConditionUnknown, + Reason: "CannotCheckHealth", + Message: err.Error(), + }) + return conditionChanged } -} - -// ShouldBeApplied returns true if the blueprint should be applied or an early-exit should happen, e.g. while dry run. -func (spec *BlueprintSpec) ShouldBeApplied() bool { - // TODO: also check if an early-exit is possible if no changes need to be applied, see PR #29 - return !spec.Config.DryRun -} - -// CompletePreProcessing decides if the blueprint is ready to be applied or not by setting the fitting next status phase. -func (spec *BlueprintSpec) CompletePreProcessing() { - if spec.Config.DryRun { - spec.Events = append(spec.Events, BlueprintDryRunEvent{}) - } else { - spec.Status = StatusPhaseBlueprintApplicationPreProcessed - spec.Events = append(spec.Events, BlueprintApplicationPreProcessedEvent{}) + if healthResult.AllHealthy() { + event := EcosystemHealthyEvent{ + doguHealthIgnored: spec.Config.IgnoreDoguHealth, + } + conditionChanged := meta.SetStatusCondition(&spec.Conditions, metav1.Condition{ + Type: ConditionEcosystemHealthy, + Status: metav1.ConditionTrue, + Reason: "Healthy", + Message: event.Message(), + }) + if conditionChanged { + spec.Events = append(spec.Events, event) + } + return conditionChanged } -} -func (spec *BlueprintSpec) MarkWaitingForSelfUpgrade() { - if spec.Status != StatusPhaseAwaitSelfUpgrade { - spec.Status = StatusPhaseAwaitSelfUpgrade - spec.Events = append(spec.Events, AwaitSelfUpgradeEvent{}) + event := EcosystemUnhealthyEvent{ + HealthResult: healthResult, } -} - -func (spec *BlueprintSpec) MarkSelfUpgradeCompleted() { - if spec.Status != StatusPhaseSelfUpgradeCompleted { - spec.Status = StatusPhaseSelfUpgradeCompleted - spec.Events = append(spec.Events, SelfUpgradeCompletedEvent{}) + oldHealthyCondition := meta.FindStatusCondition(spec.Conditions, ConditionEcosystemHealthy) + // determine here, because the condition is a pointer and will change with the SetStatusCondition call below + isConditionStatusChanged := oldHealthyCondition == nil || oldHealthyCondition.Status == metav1.ConditionTrue + conditionChanged := meta.SetStatusCondition(&spec.Conditions, metav1.Condition{ + Type: ConditionEcosystemHealthy, + Status: metav1.ConditionFalse, + Reason: "Unhealthy", + Message: "ecosystem health:\n " + healthResult.String(), + }) + // only throw an event the first unhealthy time to avoid having too many events + if isConditionStatusChanged { + spec.Events = append(spec.Events, event) } + return conditionChanged } -// CheckEcosystemHealthAfterwards checks with the given health result if the ecosystem is healthy and the blueprint was therefore successful. -func (spec *BlueprintSpec) CheckEcosystemHealthAfterwards(healthResult ecosystem.HealthResult) error { - if healthResult.AllHealthy() { - spec.Status = StatusPhaseEcosystemHealthyAfterwards - spec.Events = append(spec.Events, EcosystemHealthyAfterwardsEvent{}) - return nil - } else { - //TODO write condition here in the future - spec.Status = StatusPhaseEcosystemUnhealthyAfterwards - spec.Events = append(spec.Events, EcosystemUnhealthyAfterwardsEvent{HealthResult: healthResult}) - return NewUnhealthyEcosystemError(nil, "ecosystem is unhealthy after applying the blueprint", healthResult) +// ShouldBeApplied returns true if the blueprint should be applied or an early-exit should happen, e.g. while being stopped. +func (spec *BlueprintSpec) ShouldBeApplied() bool { + if spec.Config.Stopped { + return false } + // not true does not equal IsStatusConditionFalse here, because not true includes status "unknown" + return !meta.IsStatusConditionTrue(spec.Conditions, ConditionCompleted) || spec.StateDiff.HasChanges() } -// StartApplying marks the blueprint as in progress, which indicates, that the system started applying the blueprint. -// This state is used to detect complete failures as this state will only stay persisted if the process failed before setting the state to blueprint applied. -func (spec *BlueprintSpec) StartApplying() { - spec.Status = StatusPhaseInProgress - spec.Events = append(spec.Events, InProgressEvent{}) -} - -// MarkBlueprintApplicationFailed sets the blueprint state to application failed, which indicates that the blueprint could not be applied completely. -// In reaction to this, further post-processing will happen. -func (spec *BlueprintSpec) MarkBlueprintApplicationFailed(err error) { - spec.Status = StatusPhaseBlueprintApplicationFailed - spec.Events = append(spec.Events, ExecutionFailedEvent{err: err}) -} - -// MarkBlueprintApplied sets the blueprint state to blueprint applied, which indicates that the blueprint was applied successful and further steps can happen then. -func (spec *BlueprintSpec) MarkBlueprintApplied() { - spec.Status = StatusPhaseBlueprintApplied - spec.Events = append(spec.Events, BlueprintAppliedEvent{}) -} - -// CensorSensitiveData censors all sensitive configuration data of the blueprint, effective blueprint and the statediff, -// to make the values unrecognisable. -func (spec *BlueprintSpec) CensorSensitiveData() { - spec.StateDiff.SensitiveDoguConfigDiffs = censorValues(spec.StateDiff.SensitiveDoguConfigDiffs) - - spec.Events = append(spec.Events, SensitiveConfigDataCensoredEvent{}) -} - -// CompletePostProcessing is used to mark the blueprint as completed or failed , depending on the blueprint application result. -func (spec *BlueprintSpec) CompletePostProcessing() { - switch spec.Status { - case StatusPhaseEcosystemHealthyAfterwards: - spec.Status = StatusPhaseCompleted - spec.Events = append(spec.Events, CompletedEvent{}) - case StatusPhaseApplyEcosystemConfigFailed: - fallthrough - case StatusPhaseEcosystemUnhealthyAfterwards: - spec.Status = StatusPhaseFailed - spec.Events = append(spec.Events, ExecutionFailedEvent{err: errors.New("ecosystem is unhealthy")}) - case StatusPhaseInProgress: - spec.Status = StatusPhaseFailed - err := errors.New(handleInProgressMsg) - spec.Events = append(spec.Events, ExecutionFailedEvent{err: err}) - - case StatusPhaseBlueprintApplicationFailed: - spec.Status = StatusPhaseFailed - spec.Events = append(spec.Events, ExecutionFailedEvent{err: errors.New("could not apply blueprint")}) +func (spec *BlueprintSpec) resetCompletedConditionAfterStateDiff() bool { + if spec.StateDiff.HasChanges() { + conditionChanged := meta.SetStatusCondition(&spec.Conditions, metav1.Condition{ + Type: ConditionCompleted, + Status: metav1.ConditionFalse, + Reason: "StateDiffHasChanges", + Message: "Blueprint is being applied.", + }) + return conditionChanged } -} - -var notAllowedComponentActions = []Action{ActionDowngrade, ActionSwitchComponentNamespace} -// ActionSwitchDoguNamespace is an exception and should be handled with the blueprint config. -var notAllowedDoguActions = []Action{ActionDowngrade, ActionSwitchDoguNamespace} + return false +} func (spec *BlueprintSpec) validateStateDiff() error { var invalidBlueprintErrors []error @@ -457,10 +374,6 @@ func (spec *BlueprintSpec) validateStateDiff() error { invalidBlueprintErrors = append(invalidBlueprintErrors, spec.validateDoguDiffActions(diff)...) } - for _, diff := range spec.StateDiff.ComponentDiffs { - invalidBlueprintErrors = append(invalidBlueprintErrors, spec.validateComponentDiffActions(diff)...) - } - return errors.Join(invalidBlueprintErrors...) } @@ -478,50 +391,62 @@ func (spec *BlueprintSpec) validateDoguDiffActions(diff DoguDiff) []error { }) } -func (spec *BlueprintSpec) validateComponentDiffActions(diff ComponentDiff) []error { - return util.Map(diff.NeededActions, func(action Action) error { - if slices.Contains(notAllowedComponentActions, action) { - return getActionNotAllowedError(action) - } - - return nil - }) -} - func getActionNotAllowedError(action Action) *InvalidBlueprintError { return &InvalidBlueprintError{ Message: fmt.Sprintf("action %q is not allowed", action), } } -func (spec *BlueprintSpec) GetDogusThatNeedARestart() []cescommons.SimpleName { - var dogusThatNeedRestart []cescommons.SimpleName - dogusInEffectiveBlueprint := spec.EffectiveBlueprint.Dogus - for _, dogu := range dogusInEffectiveBlueprint { - //TODO: test this - if spec.StateDiff.DoguConfigDiffs[dogu.Name.SimpleName].HasChanges() || - spec.StateDiff.SensitiveDoguConfigDiffs[dogu.Name.SimpleName].HasChanges() { - dogusThatNeedRestart = append(dogusThatNeedRestart, dogu.Name.SimpleName) - } +// Complete is used to mark the blueprint as completed and to inform the user. +// Returns true if the condition changed, false otherwise. +func (spec *BlueprintSpec) Complete() bool { + conditionChanged := meta.SetStatusCondition(&spec.Conditions, metav1.Condition{ + Type: ConditionCompleted, + Status: metav1.ConditionTrue, + Reason: "Completed", + }) + meta.SetStatusCondition(&spec.Conditions, metav1.Condition{ + Type: ConditionLastApplySucceeded, + Status: metav1.ConditionTrue, + Reason: "ApplySucceeded", + }) + + if conditionChanged { + spec.Events = append(spec.Events, CompletedEvent{}) } - return dogusThatNeedRestart + return conditionChanged } -func (spec *BlueprintSpec) StartApplyEcosystemConfig() { - spec.Status = StatusPhaseApplyEcosystemConfig - spec.Events = append(spec.Events, ApplyEcosystemConfigEvent{}) -} +func (spec *BlueprintSpec) SetLastApplySucceededConditionOnError(reason string, err error) bool { + if err != nil { + conditionChanged := meta.SetStatusCondition(&spec.Conditions, metav1.Condition{ + Type: ConditionLastApplySucceeded, + Status: metav1.ConditionFalse, + Reason: reason, + Message: err.Error(), + }) + if conditionChanged { + spec.Events = append(spec.Events, ExecutionFailedEvent{err: err}) + } -func (spec *BlueprintSpec) MarkApplyEcosystemConfigFailed(err error) { - spec.Status = StatusPhaseApplyEcosystemConfigFailed - spec.Events = append(spec.Events, ApplyEcosystemConfigFailedEvent{err: err}) + return conditionChanged + } + + return false } func (spec *BlueprintSpec) MarkEcosystemConfigApplied() { - spec.Status = StatusPhaseEcosystemConfigApplied spec.Events = append(spec.Events, EcosystemConfigAppliedEvent{}) } -const handleInProgressMsg = "cannot handle blueprint in state " + string(StatusPhaseInProgress) + - " as this state shows that the appliance of the blueprint was interrupted before it could update the state " + - "to either " + string(StatusPhaseFailed) + " or " + string(StatusPhaseCompleted) +func (spec *BlueprintSpec) MarkBlueprintStopped() { + spec.Events = append(spec.Events, BlueprintStoppedEvent{}) +} + +func (spec *BlueprintSpec) MarkDogusApplied(isDogusApplied bool, err error) bool { + if isDogusApplied { + spec.Events = append(spec.Events, DogusAppliedEvent{Diffs: spec.StateDiff.DoguDiffs}) + } + conditionChanged := spec.SetLastApplySucceededConditionOnError(ReasonLastApplyErrorAtDogus, err) + return conditionChanged +} diff --git a/pkg/domain/blueprintSpec_test.go b/pkg/domain/blueprintSpec_test.go index 6328e5be..407174f1 100644 --- a/pkg/domain/blueprintSpec_test.go +++ b/pkg/domain/blueprintSpec_test.go @@ -1,29 +1,25 @@ package domain import ( - "errors" - "fmt" + "testing" + cescommons "github.com/cloudogu/ces-commons-lib/dogu" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/common" - "golang.org/x/exp/maps" - "testing" + "github.com/google/go-cmp/cmp" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/cloudogu/cesapp-lib/core" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/util" ) var version3211, _ = core.ParseVersion("3.2.1-1") var version3212, _ = core.ParseVersion("3.2.1-2") var version3213, _ = core.ParseVersion("3.2.1-3") - -const ( - testDistributionNamespace = "k8s" - testChangeDistributionNamespace = "k8s-testing" -) +var subfolder = "subfolder" var k8sNginxStatic = cescommons.QualifiedName{Namespace: "k8s", SimpleName: "nginx-static"} var officialNexus = cescommons.QualifiedName{ @@ -37,45 +33,11 @@ var premiumNexus = cescommons.QualifiedName{ func Test_BlueprintSpec_Validate_allOk(t *testing.T) { spec := BlueprintSpec{Id: "29.11.2023"} - require.Equal(t, StatusPhaseNew, spec.Status, "Status new should be the default") err := spec.ValidateStatically() require.Nil(t, err) - assert.Equal(t, StatusPhaseStaticallyValidated, spec.Status) - require.Equal(t, 1, len(spec.Events)) - assert.Equal(t, BlueprintSpecStaticallyValidatedEvent{}, spec.Events[0]) -} - -func Test_BlueprintSpec_Validate_inStatusValidated(t *testing.T) { - spec := BlueprintSpec{Id: "29.11.2023", Status: StatusPhaseStaticallyValidated} - - err := spec.ValidateStatically() - - require.Nil(t, err) - assert.Equal(t, StatusPhaseStaticallyValidated, spec.Status) - require.Equal(t, 0, len(spec.Events), "there should be no additional Events generated") -} - -func Test_BlueprintSpec_Validate_inStatusInProgress(t *testing.T) { - spec := BlueprintSpec{Id: "29.11.2023", Status: StatusPhaseInProgress} - - err := spec.ValidateStatically() - - require.Nil(t, err) - assert.Equal(t, StatusPhaseInProgress, spec.Status, "should stay in the old status") - require.Equal(t, 0, len(spec.Events), "there should be no additional Events generated") -} - -func Test_BlueprintSpec_Validate_inStatusInvalid(t *testing.T) { - spec := BlueprintSpec{Id: "29.11.2023", Status: StatusPhaseInvalid} - - err := spec.ValidateStatically() - - require.NotNil(t, err, "should not evaluate again and should stop with an error") - var invalidError *InvalidBlueprintError - assert.ErrorAs(t, err, &invalidError) - assert.ErrorContains(t, err, "blueprint spec was marked invalid before: do not revalidate") + require.Equal(t, 0, len(spec.Events)) } func Test_BlueprintSpec_Validate_emptyID(t *testing.T) { @@ -83,6 +45,7 @@ func Test_BlueprintSpec_Validate_emptyID(t *testing.T) { err := spec.ValidateStatically() + assert.True(t, meta.IsStatusConditionFalse(spec.Conditions, ConditionValid)) require.NotNil(t, err, "No ID definition should lead to an error") var invalidError *InvalidBlueprintError assert.ErrorAs(t, err, &invalidError) @@ -91,9 +54,10 @@ func Test_BlueprintSpec_Validate_emptyID(t *testing.T) { } func Test_BlueprintSpec_Validate_combineErrors(t *testing.T) { + name, _ := cescommons.QualifiedNameFromString("/noNamespace") spec := BlueprintSpec{ - Blueprint: Blueprint{Dogus: []Dogu{{Version: core.Version{}, TargetState: TargetStatePresent}}}, - BlueprintMask: BlueprintMask{Dogus: []MaskDogu{{TargetState: 666}}}, + Blueprint: Blueprint{Dogus: []Dogu{{Version: &core.Version{}, Absent: false}}}, + BlueprintMask: BlueprintMask{Dogus: []MaskDogu{{Name: name}}}, } err := spec.ValidateStatically() @@ -143,8 +107,8 @@ func Test_BlueprintSpec_validateMaskAgainstBlueprint(t *testing.T) { }) t.Run("absent dogus cannot be present in blueprint mask", func(t *testing.T) { spec := BlueprintSpec{ - Blueprint: Blueprint{Dogus: []Dogu{{Name: officialNexus, TargetState: TargetStateAbsent}}}, - BlueprintMask: BlueprintMask{Dogus: []MaskDogu{{Name: officialNexus, TargetState: TargetStatePresent}}}, + Blueprint: Blueprint{Dogus: []Dogu{{Name: officialNexus, Absent: true}}}, + BlueprintMask: BlueprintMask{Dogus: []MaskDogu{{Name: officialNexus, Absent: false}}}, } err := spec.validateMaskAgainstBlueprint() @@ -157,185 +121,146 @@ func Test_BlueprintSpec_validateMaskAgainstBlueprint(t *testing.T) { func Test_BlueprintSpec_CalculateEffectiveBlueprint(t *testing.T) { t.Run("no mask", func(t *testing.T) { dogus := []Dogu{ - {Name: officialDogu1, Version: version3211, TargetState: TargetStatePresent}, - {Name: officialDogu2, Version: version3212, TargetState: TargetStatePresent}, - {Name: officialDogu3, Version: version3213, TargetState: TargetStateAbsent}, + {Name: officialDogu1, Version: &version3211, Absent: false}, + {Name: officialDogu2, Version: &version3212, Absent: false}, + {Name: officialDogu3, Version: &version3213, Absent: true}, } spec := BlueprintSpec{ Blueprint: Blueprint{Dogus: dogus}, BlueprintMask: BlueprintMask{Dogus: []MaskDogu{}}, - Status: StatusPhaseValidated, } err := spec.CalculateEffectiveBlueprint() require.Nil(t, err) }) - t.Run("status new", func(t *testing.T) { - spec := BlueprintSpec{ - Blueprint: Blueprint{Dogus: []Dogu{}}, - BlueprintMask: BlueprintMask{Dogus: []MaskDogu{}}, - Status: StatusPhaseNew, - } - err := spec.CalculateEffectiveBlueprint() - - require.NotNil(t, err) - assert.ErrorContains(t, err, "cannot calculate effective blueprint before the blueprint spec is validated") - }) - t.Run("status effective blueprint generated", func(t *testing.T) { - spec := BlueprintSpec{ - Blueprint: Blueprint{Dogus: []Dogu{}}, - BlueprintMask: BlueprintMask{Dogus: []MaskDogu{}}, - Status: StatusPhaseEffectiveBlueprintGenerated, - } - expectedSpec := spec - - err := spec.CalculateEffectiveBlueprint() - - require.Nil(t, err) - assert.Equal(t, expectedSpec, spec) - }) - t.Run("status invalid", func(t *testing.T) { - spec := BlueprintSpec{ - Blueprint: Blueprint{Dogus: []Dogu{}}, - BlueprintMask: BlueprintMask{Dogus: []MaskDogu{}}, - Status: StatusPhaseInvalid, - } - - err := spec.CalculateEffectiveBlueprint() - - require.NotNil(t, err) - assert.ErrorContains(t, err, "cannot calculate effective blueprint on invalid blueprint spec") - }) t.Run("change version", func(t *testing.T) { dogus := []Dogu{ - {Name: officialDogu1, Version: version3211, TargetState: TargetStatePresent}, - {Name: officialDogu2, Version: version3212, TargetState: TargetStatePresent}, + {Name: officialDogu1, Version: &version3211, Absent: false}, + {Name: officialDogu2, Version: &version3212, Absent: false}, } maskedDogus := []MaskDogu{ - {Name: officialDogu1, Version: version3212, TargetState: TargetStatePresent}, - {Name: officialDogu2, Version: version3211, TargetState: TargetStatePresent}, + {Name: officialDogu1, Version: version3212, Absent: false}, + {Name: officialDogu2, Version: version3211, Absent: false}, } spec := BlueprintSpec{ Blueprint: Blueprint{Dogus: dogus}, BlueprintMask: BlueprintMask{Dogus: maskedDogus}, - Status: StatusPhaseValidated, } err := spec.CalculateEffectiveBlueprint() require.Nil(t, err) require.Equal(t, 2, len(spec.EffectiveBlueprint.Dogus), "effective blueprint should contain the elements from the mask") - assert.Equal(t, Dogu{Name: officialDogu1, Version: version3212, TargetState: TargetStatePresent}, spec.EffectiveBlueprint.Dogus[0]) - assert.Equal(t, Dogu{Name: officialDogu2, Version: version3211, TargetState: TargetStatePresent}, spec.EffectiveBlueprint.Dogus[1]) + assert.Equal(t, Dogu{Name: officialDogu1, Version: &version3212, Absent: false}, spec.EffectiveBlueprint.Dogus[0]) + assert.Equal(t, Dogu{Name: officialDogu2, Version: &version3211, Absent: false}, spec.EffectiveBlueprint.Dogus[1]) }) + t.Run("make dogu absent", func(t *testing.T) { dogus := []Dogu{ - {Name: officialDogu1, Version: version3211, TargetState: TargetStatePresent}, - {Name: officialDogu2, Version: version3212, TargetState: TargetStatePresent}, + {Name: officialDogu1, Version: &version3211, Absent: false}, + {Name: officialDogu2, Version: &version3212, Absent: false}, } maskedDogus := []MaskDogu{ - {Name: officialDogu1, Version: version3211, TargetState: TargetStateAbsent}, - {Name: officialDogu2, TargetState: TargetStateAbsent}, + {Name: officialDogu1, Version: version3211, Absent: true}, + {Name: officialDogu2, Absent: true}, } config := Config{ - Dogus: map[cescommons.SimpleName]CombinedDoguConfig{ - officialDogu1.SimpleName: {}, + Dogus: map[cescommons.SimpleName]DoguConfigEntries{ + officialDogu1.SimpleName: {ConfigEntry{Key: "test", Value: &val1}}, }, } spec := BlueprintSpec{ Blueprint: Blueprint{Dogus: dogus, Config: config}, BlueprintMask: BlueprintMask{Dogus: maskedDogus}, - Status: StatusPhaseValidated, } err := spec.CalculateEffectiveBlueprint() require.Nil(t, err) require.Equal(t, 2, len(spec.EffectiveBlueprint.Dogus), "effective blueprint should contain the elements from the mask") - assert.Equal(t, Dogu{Name: officialDogu1, Version: version3211, TargetState: TargetStateAbsent}, spec.EffectiveBlueprint.Dogus[0]) - assert.Equal(t, Dogu{Name: officialDogu2, Version: version3212, TargetState: TargetStateAbsent}, spec.EffectiveBlueprint.Dogus[1]) + assert.Equal(t, Dogu{Name: officialDogu1, Version: &version3211, Absent: true}, spec.EffectiveBlueprint.Dogus[0]) + assert.Equal(t, Dogu{Name: officialDogu2, Version: &version3212, Absent: true}, spec.EffectiveBlueprint.Dogus[1]) assert.NotContains(t, spec.EffectiveBlueprint.Config.Dogus, officialDogu1.SimpleName) }) + t.Run("change dogu namespace", func(t *testing.T) { dogus := []Dogu{ - {Name: officialNexus, Version: version3211, TargetState: TargetStatePresent}, + {Name: officialNexus, Version: &version3211, Absent: false}, } maskedDogus := []MaskDogu{ - {Name: premiumNexus, Version: version3211, TargetState: TargetStatePresent}, + {Name: premiumNexus, Version: version3211, Absent: false}, } spec := BlueprintSpec{ Blueprint: Blueprint{Dogus: dogus}, BlueprintMask: BlueprintMask{Dogus: maskedDogus}, Config: BlueprintConfiguration{AllowDoguNamespaceSwitch: false}, - Status: StatusPhaseValidated, } err := spec.CalculateEffectiveBlueprint() require.Error(t, err, "without the feature flag, namespace changes are not allowed") require.ErrorContains(t, err, "changing the dogu namespace is forbidden by default and can be allowed by a flag: \"official/nexus\" -> \"premium/nexus\"") }) + t.Run("change dogu namespace with flag", func(t *testing.T) { dogus := []Dogu{ - {Name: officialNexus, Version: version3211, TargetState: TargetStatePresent}, + {Name: officialNexus, Version: &version3211, Absent: false}, } maskedDogus := []MaskDogu{ - {Name: premiumNexus, Version: version3211, TargetState: TargetStatePresent}, + {Name: premiumNexus, Version: version3211, Absent: false}, } spec := BlueprintSpec{ Blueprint: Blueprint{Dogus: dogus}, BlueprintMask: BlueprintMask{Dogus: maskedDogus}, Config: BlueprintConfiguration{AllowDoguNamespaceSwitch: true}, - Status: StatusPhaseValidated, } err := spec.CalculateEffectiveBlueprint() require.NoError(t, err, "with the feature flag namespace changes should be allowed") require.Equal(t, 1, len(spec.EffectiveBlueprint.Dogus), "effective blueprint should contain the elements from the mask") - assert.Equal(t, Dogu{Name: premiumNexus, Version: version3211, TargetState: TargetStatePresent}, spec.EffectiveBlueprint.Dogus[0]) + assert.Equal(t, Dogu{Name: premiumNexus, Version: &version3211, Absent: false}, spec.EffectiveBlueprint.Dogus[0]) }) + t.Run("validate only config for dogus in blueprint", func(t *testing.T) { config := Config{ - Dogus: map[cescommons.SimpleName]CombinedDoguConfig{ + Dogus: map[cescommons.SimpleName]DoguConfigEntries{ "my-dogu": {}, }, } spec := BlueprintSpec{ Blueprint: Blueprint{Config: config}, - Status: StatusPhaseValidated, } err := spec.CalculateEffectiveBlueprint() assert.ErrorContains(t, err, "setting config for dogu \"my-dogu\" is not allowed as it will not be installed with the blueprint") - assert.Equal(t, spec.Status, StatusPhaseInvalid) - assert.Equal(t, spec.Events, []Event{BlueprintSpecInvalidEvent{err}}) + assert.Equal(t, 1, len(spec.Events)) + assert.Equal(t, "BlueprintSpecInvalid", spec.Events[0].Name()) }) t.Run("add additionalMounts", func(t *testing.T) { dogus := []Dogu{ { - Name: k8sNginxStatic, - Version: version3211, - TargetState: TargetStatePresent, + Name: k8sNginxStatic, + Version: &version3211, + Absent: false, AdditionalMounts: []ecosystem.AdditionalMount{ - {SourceType: ecosystem.DataSourceConfigMap, Name: "html-config", Volume: "customhtml", Subfolder: "test"}, + {SourceType: ecosystem.DataSourceConfigMap, Name: "html-config", Volume: "customhtml", Subfolder: subfolder}, }, }, } spec := BlueprintSpec{ Blueprint: Blueprint{Dogus: dogus}, - Status: StatusPhaseValidated, } err := spec.CalculateEffectiveBlueprint() @@ -344,28 +269,28 @@ func Test_BlueprintSpec_CalculateEffectiveBlueprint(t *testing.T) { }) } -func TestBlueprintSpec_MarkInvalid(t *testing.T) { - spec := BlueprintSpec{ - Config: BlueprintConfiguration{AllowDoguNamespaceSwitch: true}, - Status: StatusPhaseValidated, - } - expectedErr := &InvalidBlueprintError{ - WrappedError: nil, - Message: "test-error", - } - spec.MarkInvalid(expectedErr) +func TestBlueprintSpec_MissingConfigReferences(t *testing.T) { + t.Run("first call -> new event", func(t *testing.T) { + blueprint := BlueprintSpec{} + blueprint.MissingConfigReferences(assert.AnError) - assert.Equal(t, StatusPhaseInvalid, spec.Status) - require.Equal(t, 1, len(spec.Events)) - assert.Equal(t, BlueprintSpecInvalidEvent{ValidationError: expectedErr}, spec.Events[0]) -} + require.Equal(t, 1, len(blueprint.Events)) + assert.Equal(t, "MissingConfigReferences", blueprint.Events[0].Name()) + assert.Equal(t, assert.AnError.Error(), blueprint.Events[0].Message()) + assert.True(t, meta.IsStatusConditionFalse(blueprint.Conditions, ConditionExecutable)) + }) + + t.Run("repeated call -> no event", func(t *testing.T) { + blueprint := BlueprintSpec{} + + blueprint.MissingConfigReferences(assert.AnError) + blueprint.Events = []Event(nil) + blueprint.MissingConfigReferences(assert.AnError) + + assert.True(t, meta.IsStatusConditionFalse(blueprint.Conditions, ConditionExecutable)) + assert.Equal(t, []Event(nil), blueprint.Events, "no additional event if status already was AwaitSelfUpgrade") + }) -func TestBlueprintSpec_MissingConfigReferences(t *testing.T) { - blueprint := BlueprintSpec{} - blueprint.MissingConfigReferences(assert.AnError) - require.Equal(t, 1, len(blueprint.Events)) - assert.Equal(t, "MissingConfigReferences", blueprint.Events[0].Name()) - assert.Equal(t, assert.AnError.Error(), blueprint.Events[0].Message()) } func TestBlueprintSpec_DetermineStateDiff(t *testing.T) { @@ -375,42 +300,79 @@ func TestBlueprintSpec_DetermineStateDiff(t *testing.T) { // given spec := BlueprintSpec{ EffectiveBlueprint: EffectiveBlueprint{ - Dogus: []Dogu{}, - Components: []Component{}, + Dogus: []Dogu{}, }, - Status: StatusPhaseValidated, } clusterState := ecosystem.EcosystemState{ - InstalledDogus: map[cescommons.SimpleName]*ecosystem.DoguInstallation{}, - InstalledComponents: map[common.SimpleComponentName]*ecosystem.ComponentInstallation{}, + InstalledDogus: map[cescommons.SimpleName]*ecosystem.DoguInstallation{}, } // when - err := spec.DetermineStateDiff(clusterState, map[common.SensitiveDoguConfigKey]common.SensitiveDoguConfigValue{}) + err := spec.DetermineStateDiff(clusterState, map[common.DoguConfigKey]common.SensitiveDoguConfigValue{}) // then stateDiff := StateDiff{ - DoguDiffs: DoguDiffs{}, - ComponentDiffs: ComponentDiffs{}, - DoguConfigDiffs: map[cescommons.SimpleName]DoguConfigDiffs{}, - SensitiveDoguConfigDiffs: map[cescommons.SimpleName]SensitiveDoguConfigDiffs{}, + DoguDiffs: DoguDiffs{}, } + + assert.True(t, meta.IsStatusConditionTrue(spec.Conditions, ConditionExecutable)) require.NoError(t, err) - assert.Equal(t, StatusPhaseStateDiffDetermined, spec.Status) - require.Equal(t, 5, len(spec.Events)) - assert.Equal(t, newStateDiffDoguEvent(stateDiff.DoguDiffs), spec.Events[0]) - assert.Equal(t, newStateDiffComponentEvent(stateDiff.ComponentDiffs), spec.Events[1]) - assert.Equal(t, GlobalConfigDiffDeterminedEvent{GlobalConfigDiffs: GlobalConfigDiffs(nil)}, spec.Events[2]) - assert.Equal(t, DoguConfigDiffDeterminedEvent{ - DoguConfigDiffs: map[cescommons.SimpleName]DoguConfigDiffs{}, - }, spec.Events[3]) - assert.Equal(t, SensitiveDoguConfigDiffDeterminedEvent{ - SensitiveDoguConfigDiffs: map[cescommons.SimpleName]SensitiveDoguConfigDiffs{}, - }, spec.Events[4]) + require.Empty(t, spec.Events) assert.Equal(t, stateDiff, spec.StateDiff) }) + t.Run("all ok with filled blueprint", func(t *testing.T) { + // given + spec := BlueprintSpec{ + EffectiveBlueprint: EffectiveBlueprint{ + Dogus: []Dogu{{Name: officialNexus, Version: &version3211}}, + Config: Config{Global: GlobalConfigEntries{{Key: "test", Value: &val1}}}, + }, + } + + clusterState := ecosystem.EcosystemState{ + InstalledDogus: map[cescommons.SimpleName]*ecosystem.DoguInstallation{}, + } + + // when + err := spec.DetermineStateDiff(clusterState, map[common.DoguConfigKey]common.SensitiveDoguConfigValue{}) + + // then + stateDiff := StateDiff{ + DoguDiffs: DoguDiffs{ + { + DoguName: "nexus", + Actual: DoguDiffState{ + Absent: true, + }, + Expected: DoguDiffState{ + Namespace: "official", + Version: &version3211, + }, + NeededActions: []Action{ActionInstall}, + }, + }, + GlobalConfigDiffs: GlobalConfigDiffs{ + { + Key: "test", + Actual: GlobalConfigValueState{}, + Expected: GlobalConfigValueState{ + Value: (*string)(&val1), + Exists: true, + }, + NeededAction: ConfigActionSet, + }, + }, + } + + assert.True(t, meta.IsStatusConditionTrue(spec.Conditions, ConditionExecutable)) + require.NoError(t, err) + require.Equal(t, 1, len(spec.Events)) + assert.Equal(t, newStateDiffEvent(stateDiff), spec.Events[0]) + assert.Empty(t, cmp.Diff(stateDiff, spec.StateDiff)) + }) + t.Run("ok with allowed dogu namespace switch", func(t *testing.T) { // given spec := BlueprintSpec{ @@ -427,7 +389,6 @@ func TestBlueprintSpec_DetermineStateDiff(t *testing.T) { Config: BlueprintConfiguration{ AllowDoguNamespaceSwitch: true, }, - Status: StatusPhaseValidated, } clusterState := ecosystem.EcosystemState{ @@ -437,15 +398,14 @@ func TestBlueprintSpec_DetermineStateDiff(t *testing.T) { SimpleName: "name", }}, }, - InstalledComponents: map[common.SimpleComponentName]*ecosystem.ComponentInstallation{}, } // when - err := spec.DetermineStateDiff(clusterState, map[common.SensitiveDoguConfigKey]common.SensitiveDoguConfigValue{}) + err := spec.DetermineStateDiff(clusterState, map[common.DoguConfigKey]common.SensitiveDoguConfigValue{}) // then require.NoError(t, err) - assert.Equal(t, StatusPhaseStateDiffDetermined, spec.Status) + assert.True(t, meta.IsStatusConditionTrue(spec.Conditions, ConditionExecutable)) }) t.Run("invalid blueprint state with not allowed dogu namespace switch", func(t *testing.T) { @@ -464,7 +424,6 @@ func TestBlueprintSpec_DetermineStateDiff(t *testing.T) { Config: BlueprintConfiguration{ AllowDoguNamespaceSwitch: false, }, - Status: StatusPhaseValidated, } clusterState := ecosystem.EcosystemState{ @@ -474,478 +433,177 @@ func TestBlueprintSpec_DetermineStateDiff(t *testing.T) { SimpleName: "name", }}, }, - InstalledComponents: map[common.SimpleComponentName]*ecosystem.ComponentInstallation{}, } // when - err := spec.DetermineStateDiff(clusterState, map[common.SensitiveDoguConfigKey]common.SensitiveDoguConfigValue{}) + err := spec.DetermineStateDiff(clusterState, map[common.DoguConfigKey]common.SensitiveDoguConfigValue{}) // then + assert.True(t, meta.IsStatusConditionFalse(spec.Conditions, ConditionExecutable)) require.Error(t, err) - assert.Equal(t, StatusPhaseInvalid, spec.Status) assert.ErrorContains(t, err, "action \"dogu namespace switch\" is not allowed") }) +} - notAllowedStatus := []StatusPhase{StatusPhaseNew, StatusPhaseStaticallyValidated, StatusPhaseEffectiveBlueprintGenerated} - for _, initialStatus := range notAllowedStatus { - t.Run(fmt.Sprintf("cannot determine state diff in status %q", initialStatus), func(t *testing.T) { - // given - spec := BlueprintSpec{ - Status: initialStatus, - } - clusterState := ecosystem.EcosystemState{ - InstalledDogus: map[cescommons.SimpleName]*ecosystem.DoguInstallation{}, - InstalledComponents: map[common.SimpleComponentName]*ecosystem.ComponentInstallation{}, - } - // when - err := spec.DetermineStateDiff(clusterState, map[common.SensitiveDoguConfigKey]common.SensitiveDoguConfigValue{}) - - // then - assert.Error(t, err) - assert.Equal(t, spec.Status, initialStatus) - require.Equal(t, 0, len(spec.Events)) - assert.ErrorContains(t, err, fmt.Sprintf("cannot determine state diff in status phase %q", initialStatus)) - }) - } - t.Run("do not re-determine state diff", func(t *testing.T) { - initialStatus := StatusPhaseCompleted +func TestBlueprintSpec_CompletePostProcessing(t *testing.T) { + t.Run("ok with event", func(t *testing.T) { // given - spec := BlueprintSpec{ - Status: initialStatus, - } - clusterState := ecosystem.EcosystemState{ - InstalledDogus: map[cescommons.SimpleName]*ecosystem.DoguInstallation{}, - InstalledComponents: map[common.SimpleComponentName]*ecosystem.ComponentInstallation{}, - } + blueprint := &BlueprintSpec{} // when - err := spec.DetermineStateDiff(clusterState, map[common.SensitiveDoguConfigKey]common.SensitiveDoguConfigValue{}) - + changed := blueprint.Complete() // then - assert.NoError(t, err) - assert.Equal(t, spec.Status, initialStatus) - require.Equal(t, 0, len(spec.Events)) + assert.True(t, changed) + condition := meta.FindStatusCondition(blueprint.Conditions, ConditionCompleted) + assert.Equal(t, metav1.ConditionTrue, condition.Status) + assert.Equal(t, "Completed", condition.Reason) + assert.Empty(t, condition.Message) + assert.Equal(t, []Event{CompletedEvent{}}, blueprint.Events) + }) + t.Run("no change if executed twice", func(t *testing.T) { + // given + blueprint := &BlueprintSpec{} + // when + changed := blueprint.Complete() + assert.True(t, changed) + blueprint.Events = nil + changed = blueprint.Complete() + // then + assert.False(t, changed) + condition := meta.FindStatusCondition(blueprint.Conditions, ConditionCompleted) + assert.Equal(t, metav1.ConditionTrue, condition.Status) + assert.Equal(t, "Completed", condition.Reason) + assert.Empty(t, condition.Message) + assert.Equal(t, 0, len(blueprint.Events)) }) +} - t.Run("should return error with not allowed component namespace switch action", func(t *testing.T) { - // given - spec := BlueprintSpec{ - EffectiveBlueprint: EffectiveBlueprint{ - Components: []Component{ - { - Name: common.QualifiedComponentName{ - Namespace: testChangeDistributionNamespace, - SimpleName: testComponentName.SimpleName, - }, - Version: compVersion3211, - }, - }, - }, - Status: StatusPhaseValidated, - } - clusterState := ecosystem.EcosystemState{ - InstalledDogus: map[cescommons.SimpleName]*ecosystem.DoguInstallation{}, - InstalledComponents: map[common.SimpleComponentName]*ecosystem.ComponentInstallation{ - testComponentName.SimpleName: { - Name: testComponentName, - ExpectedVersion: compVersion3211, - }, - }, - } +func TestBlueprintSpec_ValidateDynamically(t *testing.T) { + t.Run("all ok, no errors", func(t *testing.T) { + blueprint := BlueprintSpec{} - // when - err := spec.DetermineStateDiff(clusterState, map[common.SensitiveDoguConfigKey]common.SensitiveDoguConfigValue{}) + blueprint.ValidateDynamically(nil) + + assert.True(t, meta.IsStatusConditionTrue(blueprint.Conditions, ConditionValid)) + require.Equal(t, 0, len(blueprint.Events)) - // then - require.Error(t, err) - assert.Equal(t, StatusPhaseInvalid, spec.Status) - assert.ErrorContains(t, err, "action \"component namespace switch\" is not allowed") }) - t.Run("should return error with not allowed component downgrade action", func(t *testing.T) { - // given - spec := BlueprintSpec{ - EffectiveBlueprint: EffectiveBlueprint{ - Components: []Component{ - { - Name: common.QualifiedComponentName{ - Namespace: testComponentName.Namespace, - SimpleName: testComponentName.SimpleName, - }, - Version: compVersion3210, - }, - }, - }, - Status: StatusPhaseValidated, - } - clusterState := ecosystem.EcosystemState{ - InstalledDogus: map[cescommons.SimpleName]*ecosystem.DoguInstallation{}, - InstalledComponents: map[common.SimpleComponentName]*ecosystem.ComponentInstallation{ - testComponentName.SimpleName: { - Name: testComponentName, - ExpectedVersion: compVersion3211, - }, - }, - } - // when - err := spec.DetermineStateDiff(clusterState, map[common.SensitiveDoguConfigKey]common.SensitiveDoguConfigValue{}) + t.Run("given dependency error", func(t *testing.T) { + blueprint := BlueprintSpec{} + givenErr := assert.AnError - // then - require.Error(t, err) - assert.Equal(t, StatusPhaseInvalid, spec.Status) - assert.ErrorContains(t, err, "action \"downgrade\" is not allowed") + blueprint.ValidateDynamically(givenErr) + + require.Equal(t, 1, len(blueprint.Events)) + assert.Equal(t, "BlueprintSpecInvalid", blueprint.Events[0].Name()) }) } -func TestBlueprintSpec_CheckEcosystemHealthUpfront(t *testing.T) { - tests := []struct { - name string - inputSpec *BlueprintSpec - healthResult ecosystem.HealthResult - expectedStatus StatusPhase - expectedEventNames []string - expectedEventMsgs []string - }{ - { - name: "should return early if health result is empty", - inputSpec: &BlueprintSpec{Config: BlueprintConfiguration{}}, - healthResult: ecosystem.HealthResult{}, - expectedStatus: StatusPhaseEcosystemHealthyUpfront, - expectedEventNames: []string{"EcosystemHealthyUpfront"}, - expectedEventMsgs: []string{"dogu health ignored: false; component health ignored: false"}, - }, - { - name: "should post ignored dogu health in event", - inputSpec: &BlueprintSpec{Config: BlueprintConfiguration{IgnoreDoguHealth: true}}, - healthResult: ecosystem.HealthResult{}, - expectedStatus: StatusPhaseEcosystemHealthyUpfront, - expectedEventNames: []string{"EcosystemHealthyUpfront"}, - expectedEventMsgs: []string{"dogu health ignored: true; component health ignored: false"}, - }, - { - name: "should post ignored component health in event", - inputSpec: &BlueprintSpec{Config: BlueprintConfiguration{IgnoreComponentHealth: true}}, - healthResult: ecosystem.HealthResult{}, - expectedStatus: StatusPhaseEcosystemHealthyUpfront, - expectedEventNames: []string{"EcosystemHealthyUpfront"}, - expectedEventMsgs: []string{"dogu health ignored: false; component health ignored: true"}, - }, - { - name: "should write unhealthy dogus in event", - inputSpec: &BlueprintSpec{}, - healthResult: ecosystem.HealthResult{ - DoguHealth: ecosystem.DoguHealthResult{ - DogusByStatus: map[ecosystem.HealthStatus][]cescommons.SimpleName{ - ecosystem.AvailableHealthStatus: {"postfix"}, - ecosystem.UnavailableHealthStatus: {"ldap"}, - ecosystem.PendingHealthStatus: {"postgresql"}, - }, - }, +func TestBlueprintSpec_ShouldBeApplied(t *testing.T) { + conditionCompleted := Condition{ + Type: ConditionCompleted, + Status: metav1.ConditionTrue, + } + t.Run("should be applied on global config change", func(t *testing.T) { + spec := &BlueprintSpec{ + Config: BlueprintConfiguration{ + Stopped: false, }, - expectedStatus: StatusPhaseEcosystemUnhealthyUpfront, - expectedEventNames: []string{"EcosystemUnhealthyUpfront"}, - expectedEventMsgs: []string{"ecosystem health:\n 2 dogu(s) are unhealthy: ldap, postgresql\n 0 component(s) are unhealthy: "}, - }, - { - name: "all dogus healthy", - inputSpec: &BlueprintSpec{}, - healthResult: ecosystem.HealthResult{ - DoguHealth: ecosystem.DoguHealthResult{ - DogusByStatus: map[ecosystem.HealthStatus][]cescommons.SimpleName{ - ecosystem.AvailableHealthStatus: {"postfix", "ldap", "postgresql"}, + StateDiff: StateDiff{ + GlobalConfigDiffs: []GlobalConfigEntryDiff{ + { + Key: "test", + NeededAction: ConfigActionSet, }, }, }, - expectedStatus: StatusPhaseEcosystemHealthyUpfront, - expectedEventNames: []string{"EcosystemHealthyUpfront"}, - expectedEventMsgs: []string{"dogu health ignored: false; component health ignored: false"}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.inputSpec.CheckEcosystemHealthUpfront(tt.healthResult) - eventNames := util.Map(tt.inputSpec.Events, Event.Name) - eventMsgs := util.Map(tt.inputSpec.Events, Event.Message) - assert.ElementsMatch(t, tt.expectedEventNames, eventNames) - assert.ElementsMatch(t, tt.expectedEventMsgs, eventMsgs) - }) - } -} - -func TestBlueprintSpec_CheckEcosystemHealthAfterwards(t *testing.T) { - tests := []struct { - name string - inputSpec *BlueprintSpec - healthResult ecosystem.HealthResult - expectedStatus StatusPhase - expectedEventNames []string - expectedEventMsgs []string - }{ - { - name: "should write unhealthy dogus in event", - inputSpec: &BlueprintSpec{}, - healthResult: ecosystem.HealthResult{ - DoguHealth: ecosystem.DoguHealthResult{ - DogusByStatus: map[ecosystem.HealthStatus][]cescommons.SimpleName{ - ecosystem.AvailableHealthStatus: {"postfix"}, - ecosystem.UnavailableHealthStatus: {"ldap"}, - ecosystem.PendingHealthStatus: {"postgresql"}, - }, - }, + Conditions: []Condition{conditionCompleted}, + } + assert.Truef(t, spec.ShouldBeApplied(), "ShouldBeApplied()") + }) + t.Run("should be applied on dogu config change", func(t *testing.T) { + doguKey := common.DoguConfigKey{ + DoguName: "testDogu", + Key: "testKey", + } + spec := &BlueprintSpec{ + Config: BlueprintConfiguration{ + Stopped: false, }, - expectedStatus: StatusPhaseEcosystemUnhealthyUpfront, - expectedEventNames: []string{"EcosystemUnhealthyAfterwards"}, - expectedEventMsgs: []string{"ecosystem health:\n 2 dogu(s) are unhealthy: ldap, postgresql\n 0 component(s) are unhealthy: "}, - }, - { - name: "ecosystem healthy", - inputSpec: &BlueprintSpec{}, - healthResult: ecosystem.HealthResult{ - DoguHealth: ecosystem.DoguHealthResult{ - DogusByStatus: map[ecosystem.HealthStatus][]cescommons.SimpleName{ - ecosystem.AvailableHealthStatus: {"postfix", "ldap", "postgresql"}, + StateDiff: StateDiff{ + DoguConfigDiffs: map[cescommons.SimpleName]DoguConfigDiffs{ + cescommons.SimpleName("testDogu"): { + { + Key: doguKey, + NeededAction: ConfigActionSet, + }, }, }, }, - expectedStatus: StatusPhaseEcosystemHealthyAfterwards, - expectedEventNames: []string{"EcosystemHealthyAfterwards"}, - expectedEventMsgs: []string{""}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.inputSpec.CheckEcosystemHealthAfterwards(tt.healthResult) - eventNames := util.Map(tt.inputSpec.Events, Event.Name) - eventMsgs := util.Map(tt.inputSpec.Events, Event.Message) - assert.ElementsMatch(t, tt.expectedEventNames, eventNames) - assert.ElementsMatch(t, tt.expectedEventMsgs, eventMsgs) - }) - } -} - -func TestBlueprintSpec_CompletePreProcessing(t *testing.T) { - t.Run("ok", func(t *testing.T) { - // given - spec := &BlueprintSpec{ - Status: StatusPhaseEcosystemHealthyUpfront, + Conditions: []Condition{conditionCompleted}, } - // when - spec.CompletePreProcessing() - // then - assert.Equal(t, spec, &BlueprintSpec{ - Status: StatusPhaseBlueprintApplicationPreProcessed, - Events: []Event{BlueprintApplicationPreProcessedEvent{}}, - }) + assert.Truef(t, spec.ShouldBeApplied(), "ShouldBeApplied()") }) - t.Run("dry run", func(t *testing.T) { - // given - spec := &BlueprintSpec{ - Status: StatusPhaseEcosystemHealthyUpfront, - Config: BlueprintConfiguration{DryRun: true}, + t.Run("should be applied on sensitive dogu config change", func(t *testing.T) { + doguKey := common.DoguConfigKey{ + DoguName: "testDogu", + Key: "testKey", } - // when - spec.CompletePreProcessing() - // then - assert.Equal(t, spec, &BlueprintSpec{ - Status: StatusPhaseEcosystemHealthyUpfront, - Config: BlueprintConfiguration{DryRun: true}, - Events: []Event{BlueprintDryRunEvent{}}, - }) - }) -} - -func TestBlueprintSpec_StartApplying(t *testing.T) { - t.Run("ok", func(t *testing.T) { - // given - spec := &BlueprintSpec{} - // when - spec.StartApplying() - // then - assert.Equal(t, spec, &BlueprintSpec{ - Status: StatusPhaseInProgress, - Events: []Event{InProgressEvent{}}, - }) - }) -} - -func TestBlueprintSpec_MarkBlueprintApplicationFailed(t *testing.T) { - // given - spec := &BlueprintSpec{} - err := fmt.Errorf("test-error") - // when - spec.MarkBlueprintApplicationFailed(err) - // then - assert.Equal(t, spec, &BlueprintSpec{ - Status: StatusPhaseBlueprintApplicationFailed, - Events: []Event{ExecutionFailedEvent{err: err}}, - }) -} - -func TestBlueprintSpec_MarkBlueprintApplied(t *testing.T) { - // given - spec := &BlueprintSpec{} - // when - spec.MarkBlueprintApplied() - // then - assert.Equal(t, spec, &BlueprintSpec{ - Status: StatusPhaseBlueprintApplied, - Events: []Event{BlueprintAppliedEvent{}}, - }) -} - -func TestBlueprintSpec_CensorSensitiveData(t *testing.T) { - // given - spec := &BlueprintSpec{ - StateDiff: StateDiff{ - SensitiveDoguConfigDiffs: map[cescommons.SimpleName]SensitiveDoguConfigDiffs{ - "ldapDiff": []SensitiveDoguConfigEntryDiff{{ - Actual: DoguConfigValueState{Value: "Test1"}, - Expected: DoguConfigValueState{Value: "Test2"}, - }}, - }, - }, - } - // when - spec.CensorSensitiveData() - - // then - require.Len(t, spec.StateDiff.SensitiveDoguConfigDiffs, 1) - assert.Contains(t, maps.Keys(spec.StateDiff.SensitiveDoguConfigDiffs), cescommons.SimpleName("ldapDiff")) - require.Len(t, spec.StateDiff.SensitiveDoguConfigDiffs["ldapDiff"], 1) - assert.Equal(t, censorValue, spec.StateDiff.SensitiveDoguConfigDiffs["ldapDiff"][0].Actual.Value) - assert.Equal(t, censorValue, spec.StateDiff.SensitiveDoguConfigDiffs["ldapDiff"][0].Expected.Value) -} - -func TestBlueprintSpec_CompletePostProcessing(t *testing.T) { - t.Run("status change on success EcosystemHealthyAfterwards -> Completed", func(t *testing.T) { - // given spec := &BlueprintSpec{ - Status: StatusPhaseEcosystemHealthyAfterwards, + Config: BlueprintConfiguration{ + Stopped: false, + }, + StateDiff: StateDiff{ + SensitiveDoguConfigDiffs: map[cescommons.SimpleName]SensitiveDoguConfigDiffs{ + cescommons.SimpleName("testDogu"): { + { + Key: doguKey, + NeededAction: ConfigActionSet, + }, + }, + }, + }, + Conditions: []Condition{conditionCompleted}, } - // when - spec.CompletePostProcessing() - // then - assert.Equal(t, spec, &BlueprintSpec{ - Status: StatusPhaseCompleted, - Events: []Event{CompletedEvent{}}, - }) + assert.Truef(t, spec.ShouldBeApplied(), "ShouldBeApplied()") }) - - t.Run("status change on failure InProgress -> Failed", func(t *testing.T) { - // given + t.Run("should be applied on condition completed false", func(t *testing.T) { spec := &BlueprintSpec{ - Status: StatusPhaseInProgress, + Conditions: []Condition{{ + Type: ConditionCompleted, + Status: metav1.ConditionFalse, + }}, } - // when - spec.CompletePostProcessing() - // then - assert.Equal(t, spec, &BlueprintSpec{ - Status: StatusPhaseFailed, - Events: []Event{ExecutionFailedEvent{errors.New(handleInProgressMsg)}}, - }) + assert.Truef(t, spec.ShouldBeApplied(), "ShouldBeApplied()") }) - - t.Run("status change on failure EcosystemUnhealthyAfterwards -> Failed", func(t *testing.T) { - // given + t.Run("should be applied on condition completed unknown", func(t *testing.T) { spec := &BlueprintSpec{ - Status: StatusPhaseEcosystemUnhealthyAfterwards, + Conditions: []Condition{{ + Type: ConditionCompleted, + Status: metav1.ConditionUnknown, + }}, } - // when - spec.CompletePostProcessing() - // then - assert.Equal(t, spec, &BlueprintSpec{ - Status: StatusPhaseFailed, - Events: []Event{ExecutionFailedEvent{errors.New("ecosystem is unhealthy")}}, - }) + assert.Truef(t, spec.ShouldBeApplied(), "ShouldBeApplied()") }) - - t.Run("status change on failure ApplicationFailed -> Failed", func(t *testing.T) { - // given + t.Run("should be applied on condition completed not set", func(t *testing.T) { spec := &BlueprintSpec{ - Status: StatusPhaseBlueprintApplicationFailed, + Conditions: []Condition{}, } - // when - spec.CompletePostProcessing() - // then - assert.Equal(t, spec, &BlueprintSpec{ - Status: StatusPhaseFailed, - Events: []Event{ExecutionFailedEvent{errors.New("could not apply blueprint")}}, - }) + assert.Truef(t, spec.ShouldBeApplied(), "ShouldBeApplied()") }) -} - -func TestBlueprintSpec_ValidateDynamically(t *testing.T) { - type fields struct { - Id string - Blueprint Blueprint - BlueprintMask BlueprintMask - EffectiveBlueprint EffectiveBlueprint - StateDiff StateDiff - Config BlueprintConfiguration - Status StatusPhase - PersistenceContext map[string]interface{} - Events []Event - } - type args struct { - possibleInvalidDependenciesError error - } - tests := []struct { - name string - fields fields - args args - expectedPhase StatusPhase - expectedEvents []Event - }{ - { - name: "statusphase invalid on error", - fields: fields{}, - args: args{possibleInvalidDependenciesError: assert.AnError}, - expectedPhase: "invalid", - expectedEvents: []Event{BlueprintSpecInvalidEvent{ - ValidationError: &InvalidBlueprintError{WrappedError: assert.AnError, Message: "blueprint spec is invalid"}}, - }, - }, - { - name: "statusphase valid on nil", - fields: fields{}, - args: args{possibleInvalidDependenciesError: nil}, - expectedPhase: "validated", - expectedEvents: []Event{BlueprintSpecValidatedEvent{}}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - spec := &BlueprintSpec{ - Id: tt.fields.Id, - Blueprint: tt.fields.Blueprint, - BlueprintMask: tt.fields.BlueprintMask, - EffectiveBlueprint: tt.fields.EffectiveBlueprint, - StateDiff: tt.fields.StateDiff, - Config: tt.fields.Config, - Status: tt.fields.Status, - PersistenceContext: tt.fields.PersistenceContext, - Events: tt.fields.Events, - } - spec.ValidateDynamically(tt.args.possibleInvalidDependenciesError) - - assert.Equal(t, tt.expectedPhase, spec.Status) - assert.Equal(t, tt.expectedEvents, spec.Events) - }) - } -} - -func TestBlueprintSpec_ShouldBeApplied(t *testing.T) { - t.Run("should be applied", func(t *testing.T) { + t.Run("should not be applied without any changes", func(t *testing.T) { spec := &BlueprintSpec{ Config: BlueprintConfiguration{ - DryRun: false, + Stopped: false, }, + Conditions: []Condition{conditionCompleted}, } - assert.Truef(t, spec.ShouldBeApplied(), "ShouldBeApplied()") + assert.Falsef(t, spec.ShouldBeApplied(), "ShouldBeApplied()") }) - t.Run("should not be applied due to dry run", func(t *testing.T) { + t.Run("should not be applied due to being stopped", func(t *testing.T) { spec := &BlueprintSpec{ Config: BlueprintConfiguration{ - DryRun: true, + Stopped: true, }, } assert.Falsef(t, spec.ShouldBeApplied(), "ShouldBeApplied()") @@ -953,118 +611,156 @@ func TestBlueprintSpec_ShouldBeApplied(t *testing.T) { } -func TestBlueprintSpec_MarkWaitingForSelfUpgrade(t *testing.T) { - t.Run("first call -> new event", func(t *testing.T) { - blueprint := BlueprintSpec{ - Status: StatusPhaseBlueprintApplicationPreProcessed, - } - blueprint.MarkWaitingForSelfUpgrade() +func TestBlueprintSpec_HandleHealthResult(t *testing.T) { + t.Run("healthy", func(t *testing.T) { + blueprint := BlueprintSpec{} + + changed := blueprint.HandleHealthResult(ecosystem.HealthResult{}, nil) - assert.Equal(t, StatusPhaseAwaitSelfUpgrade, blueprint.Status) - assert.Equal(t, []Event{AwaitSelfUpgradeEvent{}}, blueprint.Events) + assert.True(t, changed) + condition := meta.FindStatusCondition(blueprint.Conditions, ConditionEcosystemHealthy) + assert.Equal(t, metav1.ConditionTrue, condition.Status) + assert.Equal(t, "Healthy", condition.Reason) + assert.Equal(t, "dogu health ignored: false", condition.Message) }) - t.Run("repeated call -> no event", func(t *testing.T) { - blueprint := BlueprintSpec{ - Status: StatusPhaseAwaitSelfUpgrade, + t.Run("unhealthy", func(t *testing.T) { + blueprint := BlueprintSpec{} + health := ecosystem.HealthResult{ + DoguHealth: ecosystem.DoguHealthResult{ + DogusByStatus: map[ecosystem.HealthStatus][]cescommons.SimpleName{ + ecosystem.UnavailableHealthStatus: {"ldap"}, + }, + }, } - blueprint.MarkWaitingForSelfUpgrade() + changed := blueprint.HandleHealthResult(health, nil) - assert.Equal(t, StatusPhaseAwaitSelfUpgrade, blueprint.Status) - assert.Equal(t, []Event(nil), blueprint.Events, "no additional event if status already was AwaitSelfUpgrade") + assert.True(t, changed) + condition := meta.FindStatusCondition(blueprint.Conditions, ConditionEcosystemHealthy) + assert.Equal(t, metav1.ConditionFalse, condition.Status) + assert.Equal(t, "Unhealthy", condition.Reason) + assert.Contains(t, condition.Message, "ecosystem health:") + assert.Contains(t, condition.Message, "1 dogu(s) are unhealthy: ldap") }) -} -func TestBlueprintSpec_MarkSelfUpgradeCompleted(t *testing.T) { - t.Run("first call -> new event", func(t *testing.T) { - blueprint := BlueprintSpec{ - Status: StatusPhaseAwaitSelfUpgrade, - } - blueprint.MarkSelfUpgradeCompleted() + t.Run("error given, condition Unknown", func(t *testing.T) { + blueprint := BlueprintSpec{} - assert.Equal(t, StatusPhaseSelfUpgradeCompleted, blueprint.Status) - assert.Equal(t, []Event{SelfUpgradeCompletedEvent{}}, blueprint.Events) + changed := blueprint.HandleHealthResult(ecosystem.HealthResult{}, assert.AnError) + + assert.True(t, changed) + condition := meta.FindStatusCondition(blueprint.Conditions, ConditionEcosystemHealthy) + assert.Equal(t, metav1.ConditionUnknown, condition.Status) + assert.Equal(t, "CannotCheckHealth", condition.Reason) + assert.Equal(t, assert.AnError.Error(), condition.Message) }) - t.Run("repeated call -> no event", func(t *testing.T) { - blueprint := BlueprintSpec{ - Status: StatusPhaseSelfUpgradeCompleted, - } + t.Run("no condition change", func(t *testing.T) { + blueprint := BlueprintSpec{} - blueprint.MarkSelfUpgradeCompleted() + changed := blueprint.HandleHealthResult(ecosystem.HealthResult{}, assert.AnError) + assert.True(t, changed, "condition should change after the first call") + changed = blueprint.HandleHealthResult(ecosystem.HealthResult{}, assert.AnError) + assert.False(t, changed, "condition should not change here") + }) +} - assert.Equal(t, StatusPhaseSelfUpgradeCompleted, blueprint.Status) - assert.Equal(t, []Event(nil), blueprint.Events, "no additional event if status already was AwaitSelfUpgrade") +func TestBlueprintSpec_MarkBlueprintStopped(t *testing.T) { + t.Run("should add a blueprint stopped event", func(t *testing.T) { + // given + spec := &BlueprintSpec{} + // when + spec.MarkBlueprintStopped() + // then + require.Len(t, spec.Events, 1) + assert.Equal(t, BlueprintStoppedEvent{}, spec.Events[0]) }) } -func TestBlueprintSpec_GetDogusThatNeedARestart(t *testing.T) { - testDogu1 := Dogu{Name: cescommons.QualifiedName{Namespace: "testNamespace", SimpleName: "testDogu1"}} - testBlueprint1 := Blueprint{Dogus: []Dogu{testDogu1}} - testDoguConfigDiffsChanged := []DoguConfigEntryDiff{{ - Actual: DoguConfigValueState{}, - Expected: DoguConfigValueState{Value: "testValue", Exists: true}, - NeededAction: ConfigActionSet, - }} - testDoguConfigDiffsActionNone := []DoguConfigEntryDiff{{ - NeededAction: ConfigActionNone, - }} - - testDoguConfigChangeDiffChanged := StateDiff{ - DoguConfigDiffs: map[cescommons.SimpleName]DoguConfigDiffs{testDogu1.Name.SimpleName: testDoguConfigDiffsChanged}, - } - testDoguConfigChangeDiffActionNone := StateDiff{ - DoguConfigDiffs: map[cescommons.SimpleName]DoguConfigDiffs{testDogu1.Name.SimpleName: testDoguConfigDiffsActionNone}, - } +func TestBlueprintSpec_MarkDogusApplied(t *testing.T) { + t.Run("should add event if dogus are applied and no error occurred", func(t *testing.T) { + // given + spec := &BlueprintSpec{ + StateDiff: StateDiff{ + DoguDiffs: DoguDiffs{{DoguName: "test"}}, + }, + } + // when + changed := spec.MarkDogusApplied(true, nil) + // then + assert.False(t, changed) + require.Len(t, spec.Events, 1) + assert.Equal(t, DogusAppliedEvent{Diffs: DoguDiffs{{DoguName: "test"}}}, spec.Events[0]) + assert.Nil(t, meta.FindStatusCondition(spec.Conditions, ConditionLastApplySucceeded)) + }) - type fields struct { - Blueprint Blueprint - EffectiveBlueprint EffectiveBlueprint - StateDiff StateDiff - } - tests := []struct { - name string - fields fields - want []cescommons.SimpleName - }{ - { - name: "return nothing on empty blueprint", - fields: fields{}, - want: nil, - }, - { - name: "return nothing on no config change", - fields: fields{Blueprint: testBlueprint1}, - want: nil, - }, - { - name: "return dogu on dogu config change", - fields: fields{ - Blueprint: testBlueprint1, - EffectiveBlueprint: EffectiveBlueprint(testBlueprint1), - StateDiff: testDoguConfigChangeDiffChanged, + t.Run("should not add event if dogus are not applied and no error occurred", func(t *testing.T) { + // given + spec := &BlueprintSpec{} + // when + changed := spec.MarkDogusApplied(false, nil) + // then + assert.False(t, changed) + require.Empty(t, spec.Events) + assert.Nil(t, meta.FindStatusCondition(spec.Conditions, ConditionLastApplySucceeded)) + }) + + t.Run("should add event and set condition on error", func(t *testing.T) { + // given + spec := &BlueprintSpec{ + StateDiff: StateDiff{ + DoguDiffs: DoguDiffs{{DoguName: "test"}}, }, - want: []cescommons.SimpleName{testDogu1.Name.SimpleName}, - }, - { - name: "return nothing on dogu config unchanged", - fields: fields{ - Blueprint: testBlueprint1, - EffectiveBlueprint: EffectiveBlueprint(testBlueprint1), - StateDiff: testDoguConfigChangeDiffActionNone, + } + // when + changed := spec.MarkDogusApplied(true, assert.AnError) + // then + assert.True(t, changed) + require.Len(t, spec.Events, 2) + assert.Equal(t, DogusAppliedEvent{Diffs: DoguDiffs{{DoguName: "test"}}}, spec.Events[0]) + assert.Equal(t, ExecutionFailedEvent{err: assert.AnError}, spec.Events[1]) + + condition := meta.FindStatusCondition(spec.Conditions, ConditionLastApplySucceeded) + require.NotNil(t, condition) + assert.Equal(t, metav1.ConditionFalse, condition.Status) + assert.Equal(t, ReasonLastApplyErrorAtDogus, condition.Reason) + assert.Equal(t, assert.AnError.Error(), condition.Message) + }) + + t.Run("should not add applied event but set condition on error", func(t *testing.T) { + // given + spec := &BlueprintSpec{} + // when + changed := spec.MarkDogusApplied(false, assert.AnError) + // then + assert.True(t, changed) + require.Len(t, spec.Events, 1) + assert.Equal(t, ExecutionFailedEvent{err: assert.AnError}, spec.Events[0]) + + condition := meta.FindStatusCondition(spec.Conditions, ConditionLastApplySucceeded) + require.NotNil(t, condition) + assert.Equal(t, metav1.ConditionFalse, condition.Status) + assert.Equal(t, ReasonLastApplyErrorAtDogus, condition.Reason) + assert.Equal(t, assert.AnError.Error(), condition.Message) + }) + + t.Run("should not change condition if already set", func(t *testing.T) { + // given + spec := &BlueprintSpec{ + Conditions: []Condition{ + { + Type: ConditionLastApplySucceeded, + Status: metav1.ConditionFalse, + Reason: ReasonLastApplyErrorAtDogus, + Message: assert.AnError.Error(), + }, }, - want: nil, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - spec := &BlueprintSpec{ - Blueprint: tt.fields.Blueprint, - EffectiveBlueprint: tt.fields.EffectiveBlueprint, - StateDiff: tt.fields.StateDiff, - } - assert.Equalf(t, tt.want, spec.GetDogusThatNeedARestart(), "GetDogusThatNeedARestart()") - }) - } + } + // when + changed := spec.MarkDogusApplied(false, assert.AnError) + // then + assert.False(t, changed) + require.Empty(t, spec.Events) + }) } diff --git a/pkg/domain/blueprint_test.go b/pkg/domain/blueprint_test.go index ce08d447..4a4e4773 100644 --- a/pkg/domain/blueprint_test.go +++ b/pkg/domain/blueprint_test.go @@ -1,43 +1,27 @@ package domain import ( - "github.com/Masterminds/semver/v3" + "testing" + "github.com/cloudogu/cesapp-lib/core" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/common" + "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" "github.com/stretchr/testify/require" - "testing" + "k8s.io/apimachinery/pkg/api/resource" "github.com/stretchr/testify/assert" ) var version3_2_1_4, _ = core.ParseVersion("3.2.1-4") -var ( - compVersion3210 = semver.MustParse("3.2.1-0") - compVersion3212 = semver.MustParse("3.2.1-2") - compVersion3213 = semver.MustParse("3.2.1-3") - - testComponentName1 = common.QualifiedComponentName{Namespace: "k8s", SimpleName: "my-component1"} - testComponentName2 = common.QualifiedComponentName{Namespace: "official", SimpleName: "my-component2"} - testComponentName3 = common.QualifiedComponentName{Namespace: "testing", SimpleName: "my-component3"} - testComponentName4 = common.QualifiedComponentName{Namespace: "k8s", SimpleName: "my-component4"} -) - func Test_validate_ok(t *testing.T) { dogus := []Dogu{ - {Name: officialDogu1, Version: version3_2_1_0, TargetState: TargetStateAbsent}, - {Name: officialDogu2, TargetState: TargetStateAbsent}, - {Name: officialDogu3, Version: version3_2_1_0, TargetState: TargetStatePresent}, - {Name: officialNexus, Version: version3213}, + {Name: officialDogu1, Version: &version3_2_1_0, Absent: true}, + {Name: officialDogu2, Absent: true}, + {Name: officialDogu3, Version: &version3_2_1_0, Absent: false}, + {Name: officialNexus, Version: &version3213}, } - components := []Component{ - {Name: testComponentName1, Version: compVersion3210, TargetState: TargetStateAbsent}, - {Name: testComponentName2, TargetState: TargetStateAbsent}, - {Name: testComponentName3, Version: compVersion3212, TargetState: TargetStatePresent}, - {Name: testComponentName4, Version: compVersion3213}, - } - blueprint := Blueprint{Dogus: dogus, Components: components} + blueprint := Blueprint{Dogus: dogus} err := blueprint.Validate() @@ -45,21 +29,17 @@ func Test_validate_ok(t *testing.T) { } func Test_validate_multipleErrors(t *testing.T) { + dogus := []Dogu{ - {Version: version3212, TargetState: 666}, - } - components := []Component{ - {Version: compVersion3212}, - {Name: testComponentName, Version: compVersion3212}, + {Version: nil}, } blueprint := Blueprint{ - Dogus: dogus, - Components: components, + Dogus: dogus, Config: Config{ - Global: GlobalConfig{ - Present: nil, - Absent: []common.GlobalConfigKey{ - "", + Global: GlobalConfigEntries{ + { + Key: "", + Absent: true, }, }, }, @@ -70,20 +50,18 @@ func Test_validate_multipleErrors(t *testing.T) { require.Error(t, err) assert.ErrorContains(t, err, "blueprint is invalid") assert.ErrorContains(t, err, "dogu is invalid") - assert.ErrorContains(t, err, "dogu target state is invalid") - assert.ErrorContains(t, err, "component name must not be empty") - assert.ErrorContains(t, err, `namespace of component "" must not be empty`) - assert.ErrorContains(t, err, `key for absent global config should not be empty`) + assert.ErrorContains(t, err, "dogu version must not be empty") + assert.ErrorContains(t, err, `key for global config should not be empty`) } func Test_validateDogus_ok(t *testing.T) { dogus := []Dogu{ - {Name: officialDogu1, Version: version3_2_1_4, TargetState: TargetStateAbsent}, + {Name: officialDogu1, Version: &version3_2_1_4, Absent: true}, //versionIsOptionalForStateAbsent - {Name: officialDogu2, TargetState: TargetStateAbsent}, - {Name: officialDogu3, Version: version3212, TargetState: TargetStatePresent}, + {Name: officialDogu2, Absent: true}, + {Name: officialDogu3, Version: &version3212, Absent: false}, //StateDefaultsToPresent - {Name: officialNexus, Version: version3212}, + {Name: officialNexus, Version: &version3212}, } blueprint := Blueprint{Dogus: dogus} @@ -93,63 +71,26 @@ func Test_validateDogus_ok(t *testing.T) { } func Test_validateDogus_multipleErrors(t *testing.T) { + wrongBodySize := resource.MustParse("1Ki") dogus := []Dogu{ {Name: officialDogu1}, - {Name: officialDogu2, TargetState: 666}, + {Name: officialDogu2, ReverseProxyConfig: ecosystem.ReverseProxyConfig{MaxBodySize: &wrongBodySize}}, } blueprint := Blueprint{Dogus: dogus} err := blueprint.validateDogus() require.Error(t, err) - assert.Contains(t, err.Error(), "dogu target state is invalid") + assert.Contains(t, err.Error(), "dogu proxy body size is not in Decimal SI (\"M\" or \"G\")") assert.Contains(t, err.Error(), "dogu version must not be empty") } -func Test_validateComponents_ok(t *testing.T) { - components := []Component{ - { - Name: common.QualifiedComponentName{ - Namespace: "k8s", - SimpleName: "absent-component", - }, - TargetState: TargetStateAbsent, - }, - { - Name: common.QualifiedComponentName{ - SimpleName: "present-component", - Namespace: "k8s", - }, - Version: compVersion3212, - TargetState: TargetStatePresent, - }, - } - blueprint := Blueprint{Components: components} - - err := blueprint.validateComponents() - - require.NoError(t, err) -} - -func Test_validateComponents_multipleErrors(t *testing.T) { - components := []Component{ - {Name: testComponentName}, - {Version: compVersion3212}, - } - blueprint := Blueprint{Components: components} - err := blueprint.validateComponents() - - require.Error(t, err) - assert.Contains(t, err.Error(), "component name must not be empty") - assert.Contains(t, err.Error(), `version of component "k8s/my-component" must not be empty`) -} - func Test_validateDoguUniqueness(t *testing.T) { dogus := []Dogu{ - {Name: officialDogu1, Version: version3_2_1_0, TargetState: TargetStatePresent}, - {Name: officialDogu1, Version: version3213}, - {Name: officialDogu2, Version: version3213}, - {Name: officialDogu2, Version: version3213}, + {Name: officialDogu1, Version: &version3_2_1_0, Absent: false}, + {Name: officialDogu1, Version: &version3213}, + {Name: officialDogu2, Version: &version3213}, + {Name: officialDogu2, Version: &version3213}, } blueprint := Blueprint{Dogus: dogus} @@ -161,43 +102,3 @@ func Test_validateDoguUniqueness(t *testing.T) { assert.Contains(t, err.Error(), "dogu1") assert.Contains(t, err.Error(), "dogu2") } - -func Test_validateComponentUniqueness(t *testing.T) { - components := []Component{ - { - Name: common.QualifiedComponentName{ - Namespace: "present", - SimpleName: "component1", - }, - Version: compVersion3210, - TargetState: TargetStatePresent, - }, - { - Name: common.QualifiedComponentName{ - Namespace: "present", - SimpleName: "component1", - }, - Version: compVersion3213}, - { - Name: common.QualifiedComponentName{ - Namespace: "present", - SimpleName: "component2", - }, - Version: compVersion3213}, - { - Name: common.QualifiedComponentName{ - Namespace: "present", - SimpleName: "component2", - }, - Version: compVersion3213}, - } - - blueprint := Blueprint{Components: components} - - err := blueprint.validateComponentUniqueness() - - require.Error(t, err) - assert.Contains(t, err.Error(), "there are duplicate components") - assert.Contains(t, err.Error(), "component1") - assert.Contains(t, err.Error(), "component2") -} diff --git a/pkg/domain/common/componentName.go b/pkg/domain/common/componentName.go deleted file mode 100644 index 59502251..00000000 --- a/pkg/domain/common/componentName.go +++ /dev/null @@ -1,52 +0,0 @@ -package common - -import ( - "errors" - "fmt" - "strings" -) - -type QualifiedComponentName struct { - Namespace ComponentNamespace - SimpleName SimpleComponentName -} - -type ComponentNamespace string -type SimpleComponentName string - -func NewQualifiedComponentName(namespace ComponentNamespace, simpleName SimpleComponentName) (QualifiedComponentName, error) { - componentName := QualifiedComponentName{Namespace: namespace, SimpleName: simpleName} - err := componentName.Validate() - if err != nil { - return QualifiedComponentName{}, err - } - return QualifiedComponentName{Namespace: namespace, SimpleName: simpleName}, nil -} - -func (componentName QualifiedComponentName) Validate() error { - var errorList []error - if componentName.Namespace == "" { - errorList = append(errorList, fmt.Errorf("namespace of component %q must not be empty", componentName.SimpleName)) - } - if componentName.SimpleName == "" { - errorList = append(errorList, fmt.Errorf("component name must not be empty: '%s/%s'", componentName.Namespace, componentName.SimpleName)) - } - return errors.Join(errorList...) -} - -// String returns the component name with namespace, e.g. k8s/k8s-dogu-operator -func (componentName QualifiedComponentName) String() string { - return fmt.Sprintf("%s/%s", componentName.Namespace, componentName.SimpleName) -} - -// QualifiedComponentNameFromString converts a qualified component as a string, e.g. "k8s/k8s-dogu-operator", to a dedicated QualifiedComponentName or raises an error if this is not possible. -func QualifiedComponentNameFromString(qualifiedName string) (QualifiedComponentName, error) { - splitName := strings.Split(qualifiedName, "/") - if len(splitName) != 2 { - return QualifiedComponentName{}, fmt.Errorf("component name needs to be in the form 'namespace/component' but is '%s'", qualifiedName) - } - return NewQualifiedComponentName( - ComponentNamespace(splitName[0]), - SimpleComponentName(splitName[1]), - ) -} diff --git a/pkg/domain/common/componentName_test.go b/pkg/domain/common/componentName_test.go deleted file mode 100644 index 55f5b981..00000000 --- a/pkg/domain/common/componentName_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package common - -import ( - "fmt" - "github.com/stretchr/testify/assert" - "testing" -) - -func TestQualifiedComponentNameFromString(t *testing.T) { - tests := []struct { - test string - given string - expected QualifiedComponentName - wantErr assert.ErrorAssertionFunc - }{ - {test: "ok", given: "k8s/k8s-dogu-operator", expected: QualifiedComponentName{ComponentNamespace("k8s"), SimpleComponentName("k8s-dogu-operator")}, wantErr: assert.NoError}, - {test: "no ns", given: "k8s-dogu-operator", expected: QualifiedComponentName{}, wantErr: assert.Error}, - {test: "no name", given: "k8s/", expected: QualifiedComponentName{}, wantErr: assert.Error}, - {test: "double namespace", given: "k8s/test/k8s-dogu-operator", expected: QualifiedComponentName{}, wantErr: assert.Error}, - } - for _, tt := range tests { - t.Run(tt.test, func(t *testing.T) { - got, err := QualifiedComponentNameFromString(tt.given) - if !tt.wantErr(t, err, fmt.Sprintf("TestQualifiedComponentNameFromString(%v)", tt.given)) { - return - } - assert.Equalf(t, tt.expected, got, "TestQualifiedComponentNameFromString(%v)", tt.given) - }) - } -} diff --git a/pkg/domain/common/configNames.go b/pkg/domain/common/configNames.go index c5b3465c..8a4982fd 100644 --- a/pkg/domain/common/configNames.go +++ b/pkg/domain/common/configNames.go @@ -1,8 +1,8 @@ package common import ( - "errors" "fmt" + cescommons "github.com/cloudogu/ces-commons-lib/dogu" "github.com/cloudogu/k8s-registry-lib/config" ) @@ -14,24 +14,10 @@ type DoguConfigKey struct { Key config.Key } -func (k DoguConfigKey) Validate() error { - var errs []error - if k.DoguName == "" { - errs = append(errs, fmt.Errorf("dogu name for dogu config key %q should not be empty", k.Key)) - } - if string(k.Key) == "" { - errs = append(errs, fmt.Errorf("key for dogu config of dogu %q should not be empty", k.DoguName)) - } - - return errors.Join(errs...) -} - func (k DoguConfigKey) String() string { return fmt.Sprintf("key %q of dogu %q", k.Key, k.DoguName) } -type SensitiveDoguConfigKey = DoguConfigKey - // GlobalConfigValue is a single global config value type GlobalConfigValue = config.Value diff --git a/pkg/domain/component.go b/pkg/domain/component.go deleted file mode 100644 index a2f4c9dc..00000000 --- a/pkg/domain/component.go +++ /dev/null @@ -1,37 +0,0 @@ -package domain - -import ( - "errors" - "fmt" - "github.com/Masterminds/semver/v3" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/common" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" -) - -// Component represents a CES component (e.g. operators), its version, and the installation state in which it is supposed to be -// after a blueprint was applied. -type Component struct { - // Name defines the name and namespace of the component. Must not be empty. - Name common.QualifiedComponentName - // Version defines the version of the package that is to be installed. Must not be empty if the targetState is - // "present"; otherwise it is optional and is not going to be interpreted. - Version *semver.Version - // TargetState defines a state of installation of this package. Optional field, but defaults to "TargetStatePresent" - TargetState TargetState - // DeployConfig defines generic properties for the component. This field is optional. - DeployConfig ecosystem.DeployConfig -} - -// Validate checks if the component is semantically correct. -func (component *Component) Validate() error { - nameError := component.Name.Validate() - - var versionErr error - if component.TargetState == TargetStatePresent { - if component.Version == nil { - versionErr = fmt.Errorf("version of component %q must not be empty", component.Name) - } - } - - return errors.Join(versionErr, nameError) -} diff --git a/pkg/domain/component_test.go b/pkg/domain/component_test.go deleted file mode 100644 index 61826a5e..00000000 --- a/pkg/domain/component_test.go +++ /dev/null @@ -1,78 +0,0 @@ -package domain - -import ( - "github.com/Masterminds/semver/v3" - "github.com/cloudogu/cesapp-lib/core" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/common" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "testing" -) - -var ( - compVersion123 = semver.MustParse("1.2.3") - version123, _ = core.ParseVersion("1.2.3") -) - -func TestComponent_Validate(t *testing.T) { - t.Run("errorOnMissingComponentVersion", func(t *testing.T) { - component := Component{Name: testComponentName, TargetState: TargetStatePresent} - - err := component.Validate() - - require.Error(t, err) - assert.Contains(t, err.Error(), `version of component "k8s/my-component" must not be empty`) - }) - - t.Run("errorOnEmptyComponentVersion", func(t *testing.T) { - component := Component{Name: testComponentName, Version: nil, TargetState: TargetStatePresent} - - err := component.Validate() - - require.Error(t, err) - assert.Contains(t, err.Error(), "version of component \"k8s/my-component\" must not be empty") - }) - - t.Run("errorOnMissingComponentName", func(t *testing.T) { - component := Component{Version: compVersion123, TargetState: TargetStatePresent} - - err := component.Validate() - - require.Error(t, err) - assert.Contains(t, err.Error(), "component name must not be empty") - }) - - t.Run("errorOnEmptyComponentNamespace", func(t *testing.T) { - component := Component{Name: common.QualifiedComponentName{Namespace: "", SimpleName: "test"}, Version: compVersion123, TargetState: TargetStatePresent} - err := component.Validate() - - require.Error(t, err) - assert.Contains(t, err.Error(), "namespace of component \"test\" must not be empty") - }) - - t.Run("errorOnEmptyComponentName", func(t *testing.T) { - component := Component{Name: common.QualifiedComponentName{Namespace: "k8s"}, Version: compVersion123, TargetState: TargetStatePresent} - - err := component.Validate() - - require.Error(t, err) - assert.Contains(t, err.Error(), "component name must not be empty") - }) - - t.Run("emptyComponentStateDefaultsToPresent", func(t *testing.T) { - component := Component{Name: testComponentName, Version: compVersion123} - - err := component.Validate() - - require.NoError(t, err) - assert.Equal(t, TargetState(TargetStatePresent), component.TargetState) - }) - - t.Run("missingComponentVersionOkayForAbsent", func(t *testing.T) { - component := Component{Name: testComponentName, TargetState: TargetStateAbsent} - - err := component.Validate() - - require.NoError(t, err) - }) -} diff --git a/pkg/domain/config.go b/pkg/domain/config.go index 1ac6c110..398149b7 100644 --- a/pkg/domain/config.go +++ b/pkg/domain/config.go @@ -3,33 +3,36 @@ package domain import ( "errors" "fmt" + cescommons "github.com/cloudogu/ces-commons-lib/dogu" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/common" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/util" - "golang.org/x/exp/maps" - "slices" + libconfig "github.com/cloudogu/k8s-registry-lib/config" ) type Config struct { - Dogus map[cescommons.SimpleName]CombinedDoguConfig - Global GlobalConfig -} - -type CombinedDoguConfig struct { - DoguName cescommons.SimpleName - Config DoguConfig - SensitiveConfig SensitiveDoguConfig -} - -type DoguConfig struct { - Present map[common.DoguConfigKey]common.DoguConfigValue - Absent []common.DoguConfigKey -} - -type SensitiveDoguConfig struct { - Present map[common.DoguConfigKey]SensitiveValueRef - Absent []common.DoguConfigKey -} + Dogus DoguConfig + Global GlobalConfigEntries +} + +type ConfigEntry struct { + // Key is the configuration key name + Key libconfig.Key + // Absent indicates whether this key should be deleted (true) or set (false) + Absent bool + // Value is used for regular (non-sensitive) configuration entries + // Mutually exclusive with SecretRef + Value *libconfig.Value + // Sensitive indicates whether this config is sensitive and should be stored securely (true) or not (false) + Sensitive bool + // SecretRef is used for sensitive configuration entries + // Mutually exclusive with Value + SecretRef *SensitiveValueRef +} + +type DoguConfig map[cescommons.SimpleName]DoguConfigEntries +type DoguConfigEntries ConfigEntries +type GlobalConfigEntries ConfigEntries +type ConfigEntries []ConfigEntry type SensitiveValueRef struct { // SecretName is the name of the secret, from which the config key should be loaded. @@ -40,220 +43,184 @@ type SensitiveValueRef struct { SecretKey string `json:"secretKey"` } -type GlobalConfig struct { - Present map[common.GlobalConfigKey]common.GlobalConfigValue - Absent []common.GlobalConfigKey -} - -func (config GlobalConfig) GetGlobalConfigKeys() []common.GlobalConfigKey { +func (config GlobalConfigEntries) GetGlobalConfigKeys() []common.GlobalConfigKey { var keys []common.GlobalConfigKey - keys = append(keys, maps.Keys(config.Present)...) - keys = append(keys, config.Absent...) + for _, entry := range config { + keys = append(keys, entry.Key) + } return keys } func (config Config) GetDoguConfigKeys() []common.DoguConfigKey { - var keys []common.DoguConfigKey - for _, doguConfig := range config.Dogus { - keys = append(keys, maps.Keys(doguConfig.Config.Present)...) - keys = append(keys, doguConfig.Config.Absent...) - } - return keys + return config.getDoguKeysBySensitivity(false) } -func (config Config) GetSensitiveConfigReferences() map[common.SensitiveDoguConfigKey]SensitiveValueRef { - refs := map[common.SensitiveDoguConfigKey]SensitiveValueRef{} - for _, doguConfig := range config.Dogus { - for key, ref := range doguConfig.SensitiveConfig.Present { - refs[key] = ref +func (config Config) GetSensitiveConfigReferences() map[common.DoguConfigKey]SensitiveValueRef { + refs := map[common.DoguConfigKey]SensitiveValueRef{} + for doguName, doguConfig := range config.Dogus { + for _, entry := range doguConfig { + if entry.SecretRef != nil { + key := common.DoguConfigKey{ + DoguName: doguName, + Key: entry.Key, + } + refs[key] = *entry.SecretRef + } } } return refs } -func (config Config) GetSensitiveDoguConfigKeys() []common.SensitiveDoguConfigKey { - var keys []common.SensitiveDoguConfigKey - for _, doguConfig := range config.Dogus { - keys = append(keys, maps.Keys(doguConfig.SensitiveConfig.Present)...) - keys = append(keys, doguConfig.SensitiveConfig.Absent...) +func (config Config) GetSensitiveDoguConfigKeys() []common.DoguConfigKey { + return config.getDoguKeysBySensitivity(true) +} + +func (config Config) getDoguKeysBySensitivity(isSensitive bool) []common.DoguConfigKey { + var keys []common.DoguConfigKey + for doguName, doguConfig := range config.Dogus { + for _, entry := range doguConfig { + if entry.Sensitive == isSensitive { + keys = append(keys, common.DoguConfigKey{ + DoguName: doguName, + Key: entry.Key, + }) + } + } } return keys } // GetDogusWithChangedConfig returns a list of dogus for which possible config changes are needed. func (config Config) GetDogusWithChangedConfig() []cescommons.SimpleName { - var dogus []cescommons.SimpleName - for dogu, doguConfig := range config.Dogus { - if len(doguConfig.Config.Present) != 0 || len(doguConfig.Config.Absent) != 0 { - dogus = append(dogus, dogu) - } - } - return dogus + return config.getDogusWithChangedConfigBySenisitivity(false) } // GetDogusWithChangedSensitiveConfig returns a list of dogus for which possible sensitive config changes are needed. func (config Config) GetDogusWithChangedSensitiveConfig() []cescommons.SimpleName { + return config.getDogusWithChangedConfigBySenisitivity(true) +} + +func (config Config) getDogusWithChangedConfigBySenisitivity(isSensitive bool) []cescommons.SimpleName { var dogus []cescommons.SimpleName for dogu, doguConfig := range config.Dogus { - if len(doguConfig.SensitiveConfig.Present) != 0 || len(doguConfig.SensitiveConfig.Absent) != 0 { - dogus = append(dogus, dogu) + for _, entry := range doguConfig { + if entry.Sensitive == isSensitive { + dogus = append(dogus, dogu) + break // we only need to know if this dogu has at least one desired config value, so we can break to next outer loop + } } } return dogus } +func (config Config) IsEmpty() bool { + return len(config.Dogus) == 0 && len(config.Global) == 0 +} + func (config Config) validate() error { var errs []error for doguName, doguConfig := range config.Dogus { - if doguName != doguConfig.DoguName { - errs = append(errs, fmt.Errorf("dogu name %q in map and dogu name %q in value are not equal", doguName, doguConfig.DoguName)) + if doguName == "" { + errs = append(errs, fmt.Errorf("dogu name for dogu config should not be empty")) + } - errs = append(errs, doguConfig.validate()) + errs = append(errs, doguConfig.validate(doguName)) } errs = append(errs, config.Global.validate()) return errors.Join(errs...) } -func (config CombinedDoguConfig) validate() error { - err := errors.Join( - config.Config.validate(config.DoguName), - config.SensitiveConfig.validate(config.DoguName), - config.validateConflictingConfigKeys(), - ) +func (config DoguConfigEntries) validate(doguName cescommons.SimpleName) error { + var allErrs error + for _, entry := range config { + allErrs = errors.Join(allErrs, entry.validate()) + } + allErrs = errors.Join(allErrs, ConfigEntries(config).validateConflictingConfigKeys()) - if err != nil { - return fmt.Errorf("config for dogu %q is invalid: %w", config.DoguName, err) + if allErrs != nil { + return fmt.Errorf("config for dogu %q is invalid: %w", doguName, allErrs) } return nil } // validateConflictingConfigKeys checks that there are no conflicting keys in normal config and sensitive config. // This is a problem as both config types are loaded via the same API in dogus at the moment. -func (config CombinedDoguConfig) validateConflictingConfigKeys() error { - var normalKeys []common.DoguConfigKey - normalKeys = append(normalKeys, maps.Keys(config.Config.Present)...) - normalKeys = append(normalKeys, config.Config.Absent...) - var sensitiveKeys []common.SensitiveDoguConfigKey - sensitiveKeys = append(sensitiveKeys, maps.Keys(config.SensitiveConfig.Present)...) - sensitiveKeys = append(sensitiveKeys, config.SensitiveConfig.Absent...) - +func (config ConfigEntries) validateConflictingConfigKeys() error { var errorList []error - - for _, sensitiveKey := range sensitiveKeys { - if slices.Contains(normalKeys, sensitiveKey) { - errorList = append(errorList, fmt.Errorf("dogu config key %q of dogu %q cannot be in normal and sensitive configuration at the same time", sensitiveKey.Key, sensitiveKey.DoguName)) + seen := make(map[libconfig.Key]struct{}) + for _, entry := range config { + if _, exists := seen[entry.Key]; exists { + errorList = append(errorList, fmt.Errorf("duplicate dogu config Key found: %s", entry.Key)) } + seen[entry.Key] = struct{}{} } return errors.Join(errorList...) } -func validateDoguConfigKeys(keys []common.DoguConfigKey, referencedDoguName cescommons.SimpleName) error { +// Validate ensures ConfigEntry has valid state +func (config ConfigEntry) validate() error { var errs []error - for _, configKey := range keys { - err := configKey.Validate() - if err != nil { - errs = append(errs, fmt.Errorf("dogu config key is invalid: %w", err)) - } - // validate that all keys are of the same dogu - if referencedDoguName != configKey.DoguName { - errs = append(errs, fmt.Errorf("key %q of dogu %q does not match superordinate dogu name %q", configKey.Key, configKey.DoguName, referencedDoguName)) - } + if config.Key == "" { + errs = append(errs, fmt.Errorf("key for config should not be empty")) } - return errors.Join(errs...) -} -func validateNoDuplicates(presentKeys []common.DoguConfigKey, absentKeys []common.DoguConfigKey) error { - var errs []error - // no present keys in absent - // a duplicate needs to be in the present and the absent list, therefore we only need to check one of the lists. - for _, presentKey := range presentKeys { - if slices.Contains(absentKeys, presentKey) { - errs = append(errs, fmt.Errorf("key %q of dogu %q cannot be present and absent at the same time", presentKey.Key, presentKey.DoguName)) + if config.Absent { + if config.Value != nil || config.SecretRef != nil { + errs = append(errs, fmt.Errorf("absent entries cannot have value or secretRef")) } + return errors.Join(errs...) } - // no absent duplicates - absentDuplicates := util.GetDuplicates(absentKeys) - if len(absentDuplicates) > 0 { - errs = append(errs, fmt.Errorf("absent dogu config should not contain duplicate keys: %v", absentDuplicates)) - } - - // present keys cannot have duplicates because of their data structure + // For present entries, exactly one of Value or SecretRef must be set + hasValue := config.Value != nil + hasSecretRef := config.SecretRef != nil - return errors.Join(errs...) -} - -func (config DoguConfig) validate(referencedDoguName cescommons.SimpleName) error { - var errs []error - - presentKeyErr := validateDoguConfigKeys(maps.Keys(config.Present), referencedDoguName) - if presentKeyErr != nil { - errs = append(errs, fmt.Errorf("present dogu config is invalid: %w", presentKeyErr)) + if hasValue && hasSecretRef { + errs = append(errs, fmt.Errorf("config entries can have either a value or a secretRef")) } - absentKeyErr := validateDoguConfigKeys(config.Absent, referencedDoguName) - if absentKeyErr != nil { - errs = append(errs, fmt.Errorf("absent dogu config is invalid: %w", absentKeyErr)) + if hasSecretRef && !config.Sensitive { + errs = append(errs, fmt.Errorf("config entries with secret references have to be sensitive")) } - duplicatesErr := validateNoDuplicates(maps.Keys(config.Present), config.Absent) - if duplicatesErr != nil { - errs = append(errs, fmt.Errorf("dogu config is invalid: %w", duplicatesErr)) + if hasValue && config.Sensitive { + errs = append(errs, fmt.Errorf("sensitive config entries are not allowed to have normal values")) } return errors.Join(errs...) } -func (config SensitiveDoguConfig) validate(referencedDoguName cescommons.SimpleName) error { - var errs []error - - presentKeyErr := validateDoguConfigKeys(maps.Keys(config.Present), referencedDoguName) - if presentKeyErr != nil { - errs = append(errs, fmt.Errorf("present sensitive dogu config is invalid: %w", presentKeyErr)) +func (config GlobalConfigEntries) validate() error { + var allErrs error + for _, entry := range config { + allErrs = errors.Join(allErrs, entry.validateGlobal()) } + allErrs = errors.Join(allErrs, ConfigEntries(config).validateConflictingConfigKeys()) - absentKeyErr := validateDoguConfigKeys(config.Absent, referencedDoguName) - if absentKeyErr != nil { - errs = append(errs, fmt.Errorf("absent sensitive dogu config is invalid: %w", absentKeyErr)) + if allErrs != nil { + return fmt.Errorf("global config is invalid: %w", allErrs) } - - duplicatesErr := validateNoDuplicates(maps.Keys(config.Present), config.Absent) - if duplicatesErr != nil { - errs = append(errs, fmt.Errorf("dogu config is invalid: %w", duplicatesErr)) - } - - return errors.Join(errs...) + return nil } -func (config GlobalConfig) validate() error { +func (config ConfigEntry) validateGlobal() error { var errs []error - for configKey := range config.Present { - // empty key is not allowed - if string(configKey) == "" { - errs = append(errs, fmt.Errorf("key for present global config should not be empty")) - } + if config.Key == "" { + errs = append(errs, fmt.Errorf("key for global config should not be empty")) } - for _, configKey := range config.Absent { - - // empty key is not allowed - if string(configKey) == "" { - errs = append(errs, fmt.Errorf("key for absent global config should not be empty")) - } - - // absent keys cannot be present - _, isPresent := config.Present[configKey] - if isPresent { - errs = append(errs, fmt.Errorf("global config key %q cannot be present and absent at the same time", configKey)) - } + if config.Sensitive || config.SecretRef != nil { + errs = append(errs, fmt.Errorf("global entries cannot be sensitive")) } - absentDuplicates := util.GetDuplicates(config.Absent) - if len(absentDuplicates) > 0 { - errs = append(errs, fmt.Errorf("absent global config should not contain duplicate keys: %v", absentDuplicates)) + if config.Absent { + if config.Value != nil { + errs = append(errs, fmt.Errorf("absent entries cannot have value")) + } + return errors.Join(errs...) } return errors.Join(errs...) diff --git a/pkg/domain/config_test.go b/pkg/domain/config_test.go index c110924a..28e54b10 100644 --- a/pkg/domain/config_test.go +++ b/pkg/domain/config_test.go @@ -1,26 +1,35 @@ package domain import ( + "testing" + cescommons "github.com/cloudogu/ces-commons-lib/dogu" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/common" + libconfig "github.com/cloudogu/k8s-registry-lib/config" "github.com/stretchr/testify/assert" - "testing" ) +var confgiVal1 = libconfig.Value("value1") + func TestGlobalConfig_validate(t *testing.T) { t.Run("empty config is ok", func(t *testing.T) { - config := GlobalConfig{} + config := GlobalConfigEntries{} err := config.validate() assert.NoError(t, err) }) t.Run("config is ok", func(t *testing.T) { - config := GlobalConfig{ - Present: map[common.GlobalConfigKey]common.GlobalConfigValue{ - "my/key1": "", //empty values are ok - "my/key2": "test", + config := GlobalConfigEntries{ + { + Key: "my/key1", + Value: nil, //empty values are ok }, - Absent: []common.GlobalConfigKey{ - "key3", + { + Key: "my/key2", + Value: &confgiVal1, + }, + { + Key: "key3", + Absent: true, }, } @@ -29,204 +38,235 @@ func TestGlobalConfig_validate(t *testing.T) { assert.NoError(t, err) }) t.Run("no empty present keys", func(t *testing.T) { - config := GlobalConfig{ - Present: map[common.GlobalConfigKey]common.GlobalConfigValue{ - "": "", + config := GlobalConfigEntries{ + { + Key: "", + Value: nil, }, } err := config.validate() - assert.ErrorContains(t, err, "key for present global config should not be empty") + assert.ErrorContains(t, err, "key for global config should not be empty") }) t.Run("no empty absent keys", func(t *testing.T) { - config := GlobalConfig{ - Absent: []common.GlobalConfigKey{""}, + config := GlobalConfigEntries{ + { + Key: "", + Absent: true, + }, } err := config.validate() - assert.ErrorContains(t, err, "key for absent global config should not be empty") + assert.ErrorContains(t, err, "key for global config should not be empty") }) t.Run("not present and absent at the same time", func(t *testing.T) { - config := GlobalConfig{ - Present: map[common.GlobalConfigKey]common.GlobalConfigValue{ - "my/key1": "test", + config := GlobalConfigEntries{ + { + Key: "my/key1", + Value: &confgiVal1, }, - Absent: []common.GlobalConfigKey{ - "my/key1", + { + Key: "my/key1", + Absent: true, }, } err := config.validate() - assert.ErrorContains(t, err, "config key \"my/key1\" cannot be present and absent at the same time") + assert.ErrorContains(t, err, "duplicate dogu config Key found: my/key1") }) t.Run("combine errors", func(t *testing.T) { - config := GlobalConfig{ - Present: map[common.GlobalConfigKey]common.GlobalConfigValue{ - "": "", - "my/key1": "test", + config := GlobalConfigEntries{ + { + Key: "", + Value: &confgiVal1, + }, + { + Key: "my/key1", + Value: &confgiVal1, }, - Absent: []common.GlobalConfigKey{ - "my/key1", + { + Key: "my/key1", + Absent: true, }, } err := config.validate() - assert.ErrorContains(t, err, "key for present global config should not be empty") - assert.ErrorContains(t, err, "config key \"my/key1\" cannot be present and absent at the same time") - }) - t.Run("not same key multiple times", func(t *testing.T) { - config := GlobalConfig{ - Absent: []common.GlobalConfigKey{"my/key", "my/key"}, - } - err := config.validate() - assert.ErrorContains(t, err, "absent global config should not contain duplicate keys") + assert.ErrorContains(t, err, "key for global config should not be empty") + assert.ErrorContains(t, err, "duplicate dogu config Key found: my/key1") }) } func TestDoguConfig_validate(t *testing.T) { t.Run("empty is ok", func(t *testing.T) { - config := DoguConfig{} + config := DoguConfigEntries{} err := config.validate("dogu1") assert.NoError(t, err) }) t.Run("config is ok", func(t *testing.T) { - config := DoguConfig{ - Present: map[common.DoguConfigKey]common.DoguConfigValue{ - common.DoguConfigKey{DoguName: "dogu1", Key: "my/key1"}: "value1", + config := DoguConfigEntries{ + { + Key: "my/key1", + Value: &confgiVal1, }, - Absent: []common.DoguConfigKey{ - {DoguName: "dogu1", Key: "my/key2"}, + { + Key: "my/key2", + Absent: true, }, } err := config.validate("dogu1") assert.NoError(t, err) }) t.Run("not absent and present at the same time", func(t *testing.T) { - config := DoguConfig{ - Present: map[common.DoguConfigKey]common.DoguConfigValue{ - common.DoguConfigKey{DoguName: "dogu1", Key: "my/key"}: "value1", + config := DoguConfigEntries{ + { + Key: "my/key1", + Value: &confgiVal1, }, - Absent: []common.DoguConfigKey{ - {DoguName: "dogu1", Key: "my/key"}, + { + Key: "my/key1", + Absent: true, }, } err := config.validate("dogu1") - assert.ErrorContains(t, err, "key \"my/key\" of dogu \"dogu1\" cannot be present and absent at the same time") + assert.ErrorContains(t, err, "duplicate dogu config Key found: my/key1") }) t.Run("not same key multiple times", func(t *testing.T) { - config := DoguConfig{ - Absent: []common.DoguConfigKey{ - {DoguName: "dogu1", Key: "my/key"}, - {DoguName: "dogu1", Key: "my/key"}, + config := DoguConfigEntries{ + { + Key: "my/key1", + Value: &confgiVal1, + }, + { + Key: "my/key1", + Value: &confgiVal1, }, } err := config.validate("dogu1") - assert.ErrorContains(t, err, "absent dogu config should not contain duplicate keys: [key \"my/key\" of dogu \"dogu1\"]") + assert.ErrorContains(t, err, "duplicate dogu config Key found: my/key1") }) - t.Run("only one referenced dogu name", func(t *testing.T) { - config := DoguConfig{ - Present: map[common.DoguConfigKey]common.DoguConfigValue{ - common.DoguConfigKey{DoguName: "dogu1", Key: "test"}: "value1", - }, - Absent: []common.DoguConfigKey{ - {DoguName: "dogu1", Key: "my/key"}, + t.Run("no empty present keys", func(t *testing.T) { + config := DoguConfigEntries{ + { + Key: "", + Value: nil, }, } - err := config.validate("dogu2") - assert.ErrorContains(t, err, "dogu config is invalid: key \"test\" of dogu \"dogu1\" does not match superordinate dogu name \"dogu2\"") - assert.ErrorContains(t, err, "absent dogu config is invalid: key \"my/key\" of dogu \"dogu1\" does not match superordinate dogu name \"dogu2\"") + + err := config.validate("dogu1") + + assert.ErrorContains(t, err, "key for config should not be empty") }) - t.Run("combine errors", func(t *testing.T) { - config := DoguConfig{ - Present: map[common.DoguConfigKey]common.DoguConfigValue{ - common.DoguConfigKey{DoguName: "dogu1", Key: ""}: "value1", - }, - Absent: []common.DoguConfigKey{ - {DoguName: "dogu1", Key: ""}, + t.Run("no empty absent keys", func(t *testing.T) { + config := DoguConfigEntries{ + { + Key: "", + Absent: true, }, } + err := config.validate("dogu1") - assert.ErrorContains(t, err, "present dogu config is invalid") - assert.ErrorContains(t, err, "absent dogu config is invalid") + + assert.ErrorContains(t, err, "key for config should not be empty") }) -} + t.Run("empty value is allowed", func(t *testing.T) { + config := DoguConfigEntries{ + { + Key: "my/key1", + }, + } -func TestSensitiveDoguConfig_validate(t *testing.T) { - t.Run("empty is ok", func(t *testing.T) { - config := SensitiveDoguConfig{} - err := config.validate("") + err := config.validate("dogu1") assert.NoError(t, err) }) - t.Run("config is ok", func(t *testing.T) { - config := SensitiveDoguConfig{ - Present: map[common.SensitiveDoguConfigKey]SensitiveValueRef{ - common.SensitiveDoguConfigKey{DoguName: "dogu1", Key: "my/key1"}: { - SecretName: "mySecret", - SecretKey: "secretKey", - }, + t.Run("combine errors", func(t *testing.T) { + config := DoguConfigEntries{ + { + Key: "", + Value: &confgiVal1, }, - Absent: []common.SensitiveDoguConfigKey{ - {DoguName: "dogu1", Key: "my/key2"}, + { + Key: "my/key1", + Value: &confgiVal1, + }, + { + Key: "my/key1", + Absent: true, }, } err := config.validate("dogu1") - assert.NoError(t, err) + assert.ErrorContains(t, err, "config for dogu \"dogu1\" is invalid") + assert.ErrorContains(t, err, "key for config should not be empty") + assert.ErrorContains(t, err, "duplicate dogu config Key found: my/key1") }) - t.Run("not absent and present at the same time", func(t *testing.T) { - config := SensitiveDoguConfig{ - Present: map[common.SensitiveDoguConfigKey]SensitiveValueRef{ - common.SensitiveDoguConfigKey{DoguName: "dogu1", Key: "my/key"}: { - SecretName: "mySecret", - SecretKey: "secretKey", - }, - }, - Absent: []common.SensitiveDoguConfigKey{ - {DoguName: "dogu1", Key: "my/key"}, + t.Run("No absent with value", func(t *testing.T) { + config := DoguConfigEntries{ + { + Key: "my/key1", + Value: &confgiVal1, + Absent: true, }, } err := config.validate("dogu1") - assert.ErrorContains(t, err, "key \"my/key\" of dogu \"dogu1\" cannot be present and absent at the same time") + assert.ErrorContains(t, err, "absent entries cannot have value or secretRef") }) - t.Run("not same key multiple times", func(t *testing.T) { - config := SensitiveDoguConfig{ - Absent: []common.SensitiveDoguConfigKey{ - {DoguName: "dogu1", Key: "my/key"}, - {DoguName: "dogu1", Key: "my/key"}, + t.Run("No absent with secret", func(t *testing.T) { + config := DoguConfigEntries{ + { + Key: "my/key1", + SecretRef: &SensitiveValueRef{}, + Absent: true, }, } err := config.validate("dogu1") - assert.ErrorContains(t, err, "absent dogu config should not contain duplicate keys: [key \"my/key\" of dogu \"dogu1\"]") + assert.ErrorContains(t, err, "absent entries cannot have value or secretRef") }) - t.Run("only one referenced dogu name", func(t *testing.T) { - config := SensitiveDoguConfig{ - Present: map[common.SensitiveDoguConfigKey]SensitiveValueRef{ - common.SensitiveDoguConfigKey{DoguName: "dogu1", Key: "test"}: {}, + t.Run("No secret and value", func(t *testing.T) { + config := DoguConfigEntries{ + { + Key: "my/key1", + SecretRef: &SensitiveValueRef{}, + Value: &confgiVal1, }, - Absent: []common.SensitiveDoguConfigKey{ - {DoguName: "dogu1", Key: "my/key"}, + } + err := config.validate("dogu1") + assert.ErrorContains(t, err, "config entries can have either a value or a secretRef") + }) + t.Run("No secret without sensitive", func(t *testing.T) { + config := DoguConfigEntries{ + { + Key: "my/key1", + SecretRef: &SensitiveValueRef{}, }, } - err := config.validate("dogu2") - assert.ErrorContains(t, err, "present sensitive dogu config is invalid: key \"test\" of dogu \"dogu1\" does not match superordinate dogu name \"dogu2\"") - assert.ErrorContains(t, err, "absent sensitive dogu config is invalid: key \"my/key\" of dogu \"dogu1\" does not match superordinate dogu name \"dogu2\"") + err := config.validate("dogu1") + assert.ErrorContains(t, err, "config entries with secret references have to be sensitive") }) - t.Run("combine errors", func(t *testing.T) { - config := SensitiveDoguConfig{ - Present: map[common.SensitiveDoguConfigKey]SensitiveValueRef{ - common.SensitiveDoguConfigKey{DoguName: "dogu1", Key: ""}: {}, + t.Run("No sensitive with normal value", func(t *testing.T) { + config := DoguConfigEntries{ + { + Key: "my/key1", + Sensitive: true, + Value: &confgiVal1, }, - Absent: []common.SensitiveDoguConfigKey{ - {DoguName: "dogu1", Key: ""}, + } + err := config.validate("dogu1") + assert.ErrorContains(t, err, "sensitive config entries are not allowed to have normal values") + }) + t.Run("secret with sensitive allowed", func(t *testing.T) { + config := DoguConfigEntries{ + { + Key: "my/key1", + SecretRef: &SensitiveValueRef{}, + Sensitive: true, }, } err := config.validate("dogu1") - assert.ErrorContains(t, err, "present sensitive dogu config is invalid") - assert.ErrorContains(t, err, "absent sensitive dogu config is invalid") + assert.NoError(t, err) }) } @@ -241,44 +281,46 @@ func TestConfig_validate(t *testing.T) { // then assert.NoError(t, err) }) - t.Run("fail if dogu name in dogu config does not match dogu key", func(t *testing.T) { - // given - sut := Config{ - Dogus: map[cescommons.SimpleName]CombinedDoguConfig{ - "some-name": {DoguName: "another-name"}, - }, - } - - // when - err := sut.validate() - - // then - assert.ErrorContains(t, err, "dogu name \"some-name\" in map and dogu name \"another-name\" in value are not equal") - }) t.Run("fail with multiple errors", func(t *testing.T) { // given sut := Config{ - Dogus: map[cescommons.SimpleName]CombinedDoguConfig{ - "some-name": { - DoguName: "another-name", - Config: DoguConfig{ - Absent: []common.DoguConfigKey{{DoguName: ""}}, + Dogus: map[cescommons.SimpleName]DoguConfigEntries{ + "dogu1": { + ConfigEntry{ + Key: "", + Value: &confgiVal1, }, - SensitiveConfig: SensitiveDoguConfig{ - Absent: []common.SensitiveDoguConfigKey{{DoguName: ""}}, + }, + "dogu2": { + ConfigEntry{ + Key: "myKey1", + Value: &confgiVal1, + }, + ConfigEntry{ + Key: "myKey1", + Absent: true, }, }, }, - Global: GlobalConfig{Absent: []common.GlobalConfigKey{""}}, + Global: GlobalConfigEntries{ + ConfigEntry{ + Key: "myKey", + Value: &confgiVal1, + Absent: true, + }, + }, } // when err := sut.validate() // then - assert.ErrorContains(t, err, "dogu name \"some-name\" in map and dogu name \"another-name\" in value are not equal") - assert.ErrorContains(t, err, "config for dogu \"another-name\" is invalid") - assert.ErrorContains(t, err, "key for absent global config should not be empty") + assert.ErrorContains(t, err, "config for dogu \"dogu1\" is invalid") + assert.ErrorContains(t, err, "key for config should not be empty") + assert.ErrorContains(t, err, "config for dogu \"dogu2\" is invalid") + assert.ErrorContains(t, err, "duplicate dogu config Key found: myKey1") + assert.ErrorContains(t, err, "global config is invalid") + assert.ErrorContains(t, err, "absent entries cannot have value") }) } @@ -287,12 +329,14 @@ func TestGlobalConfig_GetGlobalConfigKeys(t *testing.T) { globalKey1 = common.GlobalConfigKey("key1") globalKey2 = common.GlobalConfigKey("key2") ) - config := GlobalConfig{ - Present: map[common.GlobalConfigKey]common.GlobalConfigValue{ - globalKey1: "value", + config := GlobalConfigEntries{ + { + Key: globalKey1, + Value: &confgiVal1, }, - Absent: []common.GlobalConfigKey{ - globalKey2, + { + Key: globalKey2, + Absent: true, }, } @@ -309,32 +353,34 @@ func TestConfig_GetDoguConfigKeys(t *testing.T) { nginxKey2 = common.DoguConfigKey{DoguName: nginx, Key: "key2"} postfixKey1 = common.DoguConfigKey{DoguName: postfix, Key: "key1"} postfixKey2 = common.DoguConfigKey{DoguName: postfix, Key: "key2"} + postfixKey3 = common.DoguConfigKey{DoguName: postfix, Key: "key3"} ) config := Config{ - Dogus: map[cescommons.SimpleName]CombinedDoguConfig{ + Dogus: map[cescommons.SimpleName]DoguConfigEntries{ nginx: { - DoguName: nginx, - Config: DoguConfig{ - Present: map[common.DoguConfigKey]common.DoguConfigValue{ - nginxKey1: "value", - }, - Absent: []common.DoguConfigKey{ - nginxKey2, - }, + { + Key: nginxKey1.Key, + Value: &confgiVal1, + }, + { + Key: nginxKey2.Key, + Absent: true, }, - SensitiveConfig: SensitiveDoguConfig{}, }, postfix: { - DoguName: postfix, - Config: DoguConfig{ - Present: map[common.DoguConfigKey]common.DoguConfigValue{ - postfixKey1: "value", - }, - Absent: []common.DoguConfigKey{ - postfixKey2, - }, + { + Key: postfixKey1.Key, + Value: &confgiVal1, + }, + { + Key: postfixKey2.Key, + Absent: true, + }, + { + Key: postfixKey3.Key, + Absent: true, + Sensitive: true, }, - SensitiveConfig: SensitiveDoguConfig{}, }, }, } @@ -348,33 +394,40 @@ func TestConfig_GetSensitiveDoguConfigKeys(t *testing.T) { var ( nginx = cescommons.SimpleName("nginx") postfix = cescommons.SimpleName("postfix") - nginxKey1 = common.SensitiveDoguConfigKey{DoguName: nginx, Key: "key1"} - nginxKey2 = common.SensitiveDoguConfigKey{DoguName: nginx, Key: "key2"} - postfixKey1 = common.SensitiveDoguConfigKey{DoguName: postfix, Key: "key1"} - postfixKey2 = common.SensitiveDoguConfigKey{DoguName: postfix, Key: "key2"} + nginxKey1 = common.DoguConfigKey{DoguName: nginx, Key: "key1"} + nginxKey2 = common.DoguConfigKey{DoguName: nginx, Key: "key2"} + postfixKey1 = common.DoguConfigKey{DoguName: postfix, Key: "key1"} + postfixKey2 = common.DoguConfigKey{DoguName: postfix, Key: "key2"} + postfixKey3 = common.DoguConfigKey{DoguName: postfix, Key: "key3"} ) config := Config{ - Dogus: map[cescommons.SimpleName]CombinedDoguConfig{ + Dogus: map[cescommons.SimpleName]DoguConfigEntries{ nginx: { - DoguName: nginx, - SensitiveConfig: SensitiveDoguConfig{ - Present: map[common.SensitiveDoguConfigKey]SensitiveValueRef{ - nginxKey1: {}, - }, - Absent: []common.SensitiveDoguConfigKey{ - nginxKey2, - }, + { + Key: nginxKey1.Key, + Value: &confgiVal1, + Sensitive: true, + }, + { + Key: nginxKey2.Key, + Absent: true, + Sensitive: true, }, }, postfix: { - DoguName: postfix, - SensitiveConfig: SensitiveDoguConfig{ - Present: map[common.SensitiveDoguConfigKey]SensitiveValueRef{ - postfixKey1: {}, - }, - Absent: []common.SensitiveDoguConfigKey{ - postfixKey2, - }, + { + Key: postfixKey1.Key, + Value: &confgiVal1, + Sensitive: true, + }, + { + Key: postfixKey2.Key, + Absent: true, + Sensitive: true, + }, + { + Key: postfixKey3.Key, + Absent: true, }, }, }, @@ -382,53 +435,31 @@ func TestConfig_GetSensitiveDoguConfigKeys(t *testing.T) { keys := config.GetSensitiveDoguConfigKeys() - assert.ElementsMatch(t, keys, []common.SensitiveDoguConfigKey{nginxKey1, nginxKey2, postfixKey1, postfixKey2}) + assert.ElementsMatch(t, keys, []common.DoguConfigKey{nginxKey1, nginxKey2, postfixKey1, postfixKey2}) } -func TestCombinedDoguConfig_validate(t *testing.T) { - normalConfig := DoguConfig{ - Present: map[common.DoguConfigKey]common.DoguConfigValue{ - common.DoguConfigKey{DoguName: "dogu1", Key: "my/key1"}: "value1", +func TestConfig_GetDogusWithChangedConfig(t *testing.T) { + doguConfig := DoguConfigEntries{ + { + Key: dogu1Key1.Key, + Value: &confgiVal1, }, - Absent: []common.DoguConfigKey{ - {DoguName: "dogu1", Key: "my/key2"}, + { + Key: dogu1Key2.Key, + Absent: true, }, } - sensitiveConfig := SensitiveDoguConfig{ - Present: map[common.SensitiveDoguConfigKey]SensitiveValueRef{ - common.SensitiveDoguConfigKey{DoguName: "dogu1", Key: "my/key1"}: { - SecretName: "mySecret", - SecretKey: "myKey", - }, - }, - Absent: []common.SensitiveDoguConfigKey{ - {DoguName: "dogu1", Key: "my/key2"}, - }, + configEntryPresent := ConfigEntry{ + Key: dogu1Key1.Key, + Value: &confgiVal1, } - - config := CombinedDoguConfig{ - DoguName: "dogu1", - Config: normalConfig, - SensitiveConfig: sensitiveConfig, + configEntryAbsent := ConfigEntry{ + Key: dogu1Key2.Key, + Absent: true, } - err := config.validate() - - assert.ErrorContains(t, err, "dogu config key \"my/key1\" of dogu \"dogu1\" cannot be in normal and sensitive configuration at the same time") -} - -func TestConfig_GetDogusWithChangedConfig(t *testing.T) { - presentConfig := map[common.DoguConfigKey]common.DoguConfigValue{ - dogu1Key1: "val", - } - AbsentConfig := []common.DoguConfigKey{ - dogu1Key1, - } - emptyPresentConfig := map[common.DoguConfigKey]common.DoguConfigValue{} - var emptyAbsentConfig []common.DoguConfigKey - type args struct { - doguConfig DoguConfig + doguConfig DoguConfigEntries withDogu2Change bool } @@ -440,53 +471,46 @@ func TestConfig_GetDogusWithChangedConfig(t *testing.T) { }{ { name: "should get multiple Dogus", - args: args{doguConfig: DoguConfig{Present: presentConfig, Absent: AbsentConfig}, withDogu2Change: true}, + args: args{doguConfig: DoguConfigEntries{configEntryPresent, configEntryAbsent}, withDogu2Change: true}, want: []cescommons.SimpleName{dogu1, dogu2}, }, { name: "should get Dogus with changed present and absent config", - args: args{doguConfig: DoguConfig{Present: presentConfig, Absent: AbsentConfig}}, + args: args{doguConfig: DoguConfigEntries{configEntryPresent, configEntryAbsent}}, want: []cescommons.SimpleName{dogu1}, }, { name: "should get Dogus with changed present config", - args: args{doguConfig: DoguConfig{Present: presentConfig, Absent: emptyAbsentConfig}}, + args: args{doguConfig: DoguConfigEntries{configEntryPresent}}, want: []cescommons.SimpleName{dogu1}, }, { name: "should get Dogus with changed absent config", - args: args{doguConfig: DoguConfig{Present: emptyPresentConfig, Absent: AbsentConfig}}, + args: args{doguConfig: DoguConfigEntries{configEntryAbsent}}, want: []cescommons.SimpleName{dogu1}, }, { name: "should not get Dogus with no config changes", - args: args{doguConfig: DoguConfig{Present: emptyPresentConfig, Absent: emptyAbsentConfig}}, + args: args{doguConfig: DoguConfigEntries{}}, + want: emptyResult, + }, + { + name: "should not get Dogus with sensitive config changes", + args: args{doguConfig: DoguConfigEntries{ConfigEntry{Key: "mykey", Value: &confgiVal1, Sensitive: true}}}, want: emptyResult, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - emptyDoguConfig := struct { - Present map[common.DoguConfigKey]SensitiveValueRef - Absent []common.DoguConfigKey - }{} config := Config{ - Dogus: map[cescommons.SimpleName]CombinedDoguConfig{ - dogu1: { - DoguName: dogu1, - Config: tt.args.doguConfig, - SensitiveConfig: emptyDoguConfig, - }, + Dogus: map[cescommons.SimpleName]DoguConfigEntries{ + dogu1: tt.args.doguConfig, }, - Global: GlobalConfig{}, + Global: GlobalConfigEntries{}, } if tt.args.withDogu2Change { - config.Dogus[dogu2] = CombinedDoguConfig{ - DoguName: dogu2, - Config: tt.args.doguConfig, - SensitiveConfig: emptyDoguConfig, - } + config.Dogus[dogu2] = doguConfig } changedDogus := config.GetDogusWithChangedConfig() @@ -499,20 +523,22 @@ func TestConfig_GetDogusWithChangedConfig(t *testing.T) { } func TestConfig_GetDogusWithChangedSensitiveConfig(t *testing.T) { - presentConfig := map[common.DoguConfigKey]SensitiveValueRef{ - dogu1Key1: { + presentConfig := ConfigEntry{ + Key: dogu1Key1.Key, + Sensitive: true, + SecretRef: &SensitiveValueRef{ SecretName: "mySecret", SecretKey: "myKey", }, } - AbsentConfig := []common.DoguConfigKey{ - dogu1Key1, + absentConfig := ConfigEntry{ + Key: dogu1Key1.Key, + Sensitive: true, + Absent: true, } - emptyPresentConfig := map[common.DoguConfigKey]SensitiveValueRef{} - var emptyAbsentConfig []common.DoguConfigKey type args struct { - doguConfig SensitiveDoguConfig + doguConfig DoguConfigEntries withDogu2Change bool } @@ -524,53 +550,46 @@ func TestConfig_GetDogusWithChangedSensitiveConfig(t *testing.T) { }{ { name: "should get multiple Dogus", - args: args{doguConfig: SensitiveDoguConfig{Present: presentConfig, Absent: AbsentConfig}, withDogu2Change: true}, + args: args{doguConfig: DoguConfigEntries{presentConfig, absentConfig}, withDogu2Change: true}, want: []cescommons.SimpleName{dogu1, dogu2}, }, { name: "should get Dogus with changed present and absent config", - args: args{doguConfig: SensitiveDoguConfig{Present: presentConfig, Absent: AbsentConfig}}, + args: args{doguConfig: DoguConfigEntries{presentConfig, absentConfig}}, want: []cescommons.SimpleName{dogu1}, }, { name: "should get Dogus with changed present config", - args: args{doguConfig: SensitiveDoguConfig{Present: presentConfig, Absent: emptyAbsentConfig}}, + args: args{doguConfig: DoguConfigEntries{presentConfig}}, want: []cescommons.SimpleName{dogu1}, }, { name: "should get Dogus with changed absent config", - args: args{doguConfig: SensitiveDoguConfig{Present: emptyPresentConfig, Absent: AbsentConfig}}, + args: args{doguConfig: DoguConfigEntries{absentConfig}}, want: []cescommons.SimpleName{dogu1}, }, { name: "should not get Dogus with no config changes", - args: args{doguConfig: SensitiveDoguConfig{Present: emptyPresentConfig, Absent: emptyAbsentConfig}}, + args: args{doguConfig: DoguConfigEntries{}}, + want: emptyResult, + }, + { + name: "should not get Dogus with no sensitive config changes", + args: args{doguConfig: DoguConfigEntries{ConfigEntry{Key: "mykey", Value: &confgiVal1}}}, want: emptyResult, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - emptyDoguConfig := struct { - Present map[common.DoguConfigKey]common.DoguConfigValue - Absent []common.DoguConfigKey - }{} config := Config{ - Dogus: map[cescommons.SimpleName]CombinedDoguConfig{ - dogu1: { - DoguName: dogu1, - Config: emptyDoguConfig, - SensitiveConfig: tt.args.doguConfig, - }, + Dogus: map[cescommons.SimpleName]DoguConfigEntries{ + dogu1: tt.args.doguConfig, }, - Global: GlobalConfig{}, + Global: GlobalConfigEntries{}, } if tt.args.withDogu2Change { - config.Dogus[dogu2] = CombinedDoguConfig{ - DoguName: dogu2, - Config: emptyDoguConfig, - SensitiveConfig: tt.args.doguConfig, - } + config.Dogus[dogu2] = tt.args.doguConfig } changedDogus := config.GetDogusWithChangedSensitiveConfig() diff --git a/pkg/domain/dogu.go b/pkg/domain/dogu.go index bdeab4ee..751b16ed 100644 --- a/pkg/domain/dogu.go +++ b/pkg/domain/dogu.go @@ -3,12 +3,12 @@ package domain import ( "errors" "fmt" + "strings" + cescommons "github.com/cloudogu/ces-commons-lib/dogu" "github.com/cloudogu/cesapp-lib/core" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" "k8s.io/apimachinery/pkg/api/resource" - "slices" - "strings" ) // Dogu defines a Dogu, its version, and the installation state in which it is supposed to be after a blueprint @@ -18,12 +18,12 @@ type Dogu struct { Name cescommons.QualifiedName // Version defines the version of the dogu that is to be installed. Must not be empty if the targetState is "present"; // otherwise it is optional and is not going to be interpreted. - Version core.Version - // TargetState defines a state of installation of this dogu. Optional field, but defaults to "TargetStatePresent" - TargetState TargetState + Version *core.Version + // Absent defines if the dogu should be absent in the ecosystem. Defaults to false. + Absent bool // MinVolumeSize is the minimum storage of the dogu. 0 indicates that the default size should be set. // Reducing this value below the actual volume size has no impact as we do not support downsizing. - MinVolumeSize ecosystem.VolumeSize + MinVolumeSize *ecosystem.VolumeSize // ReverseProxyConfig defines configuration for the ecosystem reverse proxy. This field is optional. ReverseProxyConfig ecosystem.ReverseProxyConfig // AdditionalMounts provides the possibility to mount additional data into the dogu. @@ -33,11 +33,9 @@ type Dogu struct { // validate checks if the Dogu is semantically correct. func (dogu Dogu) validate() error { var errorList []error - if !slices.Contains(PossibleTargetStates, dogu.TargetState) { - errorList = append(errorList, fmt.Errorf("dogu target state is invalid: %s", dogu.Name)) - } + emptyVersion := core.Version{} - if dogu.TargetState != TargetStateAbsent && dogu.Version == emptyVersion { + if !dogu.Absent && (dogu.Version == nil || *dogu.Version == emptyVersion) { errorList = append(errorList, fmt.Errorf("dogu version must not be empty: %s", dogu.Name)) } // minVolumeSize is already checked while unmarshalling json/yaml diff --git a/pkg/domain/dogu_test.go b/pkg/domain/dogu_test.go index 56a16888..24ddca04 100644 --- a/pkg/domain/dogu_test.go +++ b/pkg/domain/dogu_test.go @@ -1,15 +1,21 @@ package domain import ( + "testing" + + "github.com/cloudogu/cesapp-lib/core" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/api/resource" - "testing" +) + +var ( + version123, _ = core.ParseVersion("1.2.3-4") ) func Test_TargetDogu_validate_errorOnMissingVersionForPresentDogu(t *testing.T) { - dogu := Dogu{Name: officialDogu1, TargetState: TargetStatePresent} + dogu := Dogu{Name: officialDogu1, Absent: false} err := dogu.validate() @@ -18,7 +24,7 @@ func Test_TargetDogu_validate_errorOnMissingVersionForPresentDogu(t *testing.T) } func Test_TargetDogu_validate_missingVersionOkayForAbsentDogu(t *testing.T) { - dogu := Dogu{Name: officialDogu1, TargetState: TargetStateAbsent} + dogu := Dogu{Name: officialDogu1, Absent: true} err := dogu.validate() @@ -26,21 +32,12 @@ func Test_TargetDogu_validate_missingVersionOkayForAbsentDogu(t *testing.T) { } func Test_TargetDogu_validate_defaultToPresentState(t *testing.T) { - dogu := Dogu{Name: officialDogu1, Version: version123} + dogu := Dogu{Name: officialDogu1, Version: &version123} err := dogu.validate() require.Nil(t, err) - assert.Equal(t, TargetState(TargetStatePresent), dogu.TargetState) -} - -func Test_TargetDogu_validate_errorOnUnknownTargetState(t *testing.T) { - dogu := Dogu{Name: officialDogu1, TargetState: -1} - - err := dogu.validate() - - require.Error(t, err) - require.ErrorContains(t, err, "dogu target state is invalid: official/dogu1") + assert.False(t, dogu.Absent) } func Test_TargetDogu_validate_ProxySizeFormat(t *testing.T) { @@ -57,7 +54,7 @@ func Test_TargetDogu_validate_ProxySizeFormat(t *testing.T) { t.Run("no error on empty quantity", func(t *testing.T) { // given - dogu := Dogu{Name: officialDogu1, Version: version123} + dogu := Dogu{Name: officialDogu1, Version: &version123} // when err := dogu.validate() // then @@ -67,7 +64,7 @@ func Test_TargetDogu_validate_ProxySizeFormat(t *testing.T) { t.Run("no error on zero size quantity", func(t *testing.T) { // given zeroQuantity := resource.MustParse("0") - dogu := Dogu{Name: officialDogu1, Version: version123, ReverseProxyConfig: ecosystem.ReverseProxyConfig{MaxBodySize: &zeroQuantity}} + dogu := Dogu{Name: officialDogu1, Version: &version123, ReverseProxyConfig: ecosystem.ReverseProxyConfig{MaxBodySize: &zeroQuantity}} // when err := dogu.validate() // then @@ -79,12 +76,12 @@ func Test_TargetDogu_validate_ProxySizeFormat(t *testing.T) { func Test_TargetDogu_validate_AdditionalMounts(t *testing.T) { t.Run("additionalMounts ok", func(t *testing.T) { // given - dogu := Dogu{Name: nginxStatic, Version: version123, AdditionalMounts: []ecosystem.AdditionalMount{ + dogu := Dogu{Name: officialDogu1, Version: &version123, AdditionalMounts: []ecosystem.AdditionalMount{ { SourceType: ecosystem.DataSourceConfigMap, Name: "html-config", Volume: "customhtml", - Subfolder: "test", + Subfolder: subfolder, }, }} // when @@ -95,36 +92,37 @@ func Test_TargetDogu_validate_AdditionalMounts(t *testing.T) { t.Run("unknown sourceType", func(t *testing.T) { // given - dogu := Dogu{Name: nginxStatic, Version: version123, AdditionalMounts: []ecosystem.AdditionalMount{ + dogu := Dogu{Name: officialDogu1, Version: &version123, AdditionalMounts: []ecosystem.AdditionalMount{ { SourceType: "unsupportedType", Name: "html-config", Volume: "customhtml", - Subfolder: "test", + Subfolder: subfolder, }, }} // when err := dogu.validate() // then require.Error(t, err) - require.ErrorContains(t, err, "dogu is invalid: dogu additional mounts sourceType must be one of 'ConfigMap', 'Secret': k8s/nginx-static") + require.ErrorContains(t, err, "dogu is invalid: dogu additional mounts sourceType must be one of 'ConfigMap', 'Secret': official/dogu1") }) t.Run("subfolder is no relative path", func(t *testing.T) { // given - dogu := Dogu{Name: nginxStatic, Version: version123, AdditionalMounts: []ecosystem.AdditionalMount{ + absoluteSubfolder := "/test" + dogu := Dogu{Name: officialDogu1, Version: &version123, AdditionalMounts: []ecosystem.AdditionalMount{ { SourceType: ecosystem.DataSourceConfigMap, Name: "html-config", Volume: "customhtml", - Subfolder: "/test", + Subfolder: absoluteSubfolder, }, }} // when err := dogu.validate() // then require.Error(t, err) - require.ErrorContains(t, err, "dogu is invalid: dogu additional mounts Subfolder must be a relative path : k8s/nginx-static") + require.ErrorContains(t, err, "dogu is invalid: dogu additional mounts Subfolder must be a relative path : official/dogu1") }) } diff --git a/pkg/domain/ecosystem/EcosystemState.go b/pkg/domain/ecosystem/EcosystemState.go index 1c699d75..9afab76a 100644 --- a/pkg/domain/ecosystem/EcosystemState.go +++ b/pkg/domain/ecosystem/EcosystemState.go @@ -2,14 +2,12 @@ package ecosystem import ( cescommons "github.com/cloudogu/ces-commons-lib/dogu" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/common" "github.com/cloudogu/k8s-registry-lib/config" ) // EcosystemState describes the actual state of the ecosystem, which is used to compare it with the expected state in the state diff. type EcosystemState struct { InstalledDogus map[cescommons.SimpleName]*DoguInstallation - InstalledComponents map[common.SimpleComponentName]*ComponentInstallation GlobalConfig config.GlobalConfig ConfigByDogu map[cescommons.SimpleName]config.DoguConfig SensitiveConfigByDogu map[cescommons.SimpleName]config.DoguConfig diff --git a/pkg/domain/ecosystem/componentHealth.go b/pkg/domain/ecosystem/componentHealth.go deleted file mode 100644 index 132b908d..00000000 --- a/pkg/domain/ecosystem/componentHealth.go +++ /dev/null @@ -1,68 +0,0 @@ -package ecosystem - -import ( - "fmt" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/common" - "slices" - "strings" - "time" - - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/util" -) - -type RequiredComponent struct { - Name common.SimpleComponentName -} - -type WaitConfig struct { - Timeout time.Duration - Interval time.Duration -} - -// ComponentHealthResult is a snapshot of all components' health states. -type ComponentHealthResult struct { - ComponentsByStatus map[HealthStatus][]common.SimpleComponentName -} - -func (result ComponentHealthResult) getUnhealthyComponents() []common.SimpleComponentName { - var unhealthyComponents []common.SimpleComponentName - for healthState, componentNames := range result.ComponentsByStatus { - if healthState != AvailableHealthStatus { - unhealthyComponents = append(unhealthyComponents, componentNames...) - } - } - return unhealthyComponents -} - -func (result ComponentHealthResult) String() string { - unhealthyComponents := util.Map(result.getUnhealthyComponents(), func(dogu common.SimpleComponentName) string { return string(dogu) }) - slices.Sort(unhealthyComponents) - return fmt.Sprintf("%d component(s) are unhealthy: %s", len(unhealthyComponents), strings.Join(unhealthyComponents, ", ")) -} - -// CalculateComponentHealthResult checks if all required components are installed, -// collects the health states from ComponentInstallation and creates a ComponentHealthResult. -func CalculateComponentHealthResult(installedComponents map[common.SimpleComponentName]*ComponentInstallation, requiredComponents []RequiredComponent) ComponentHealthResult { - result := ComponentHealthResult{ - ComponentsByStatus: map[HealthStatus][]common.SimpleComponentName{}, - } - for _, required := range requiredComponents { - _, installed := installedComponents[required.Name] - if !installed { - result.ComponentsByStatus[NotInstalledHealthStatus] = append(result.ComponentsByStatus[NotInstalledHealthStatus], common.SimpleComponentName(required.Name)) - } - } - for _, component := range installedComponents { - result.ComponentsByStatus[component.Health] = append(result.ComponentsByStatus[component.Health], component.Name.SimpleName) - } - return result -} - -func (result ComponentHealthResult) AllHealthy() bool { - for healthState, componentNames := range result.ComponentsByStatus { - if healthState != AvailableHealthStatus && len(componentNames) != 0 { - return false - } - } - return true -} diff --git a/pkg/domain/ecosystem/componentHealth_test.go b/pkg/domain/ecosystem/componentHealth_test.go deleted file mode 100644 index 1cc06d75..00000000 --- a/pkg/domain/ecosystem/componentHealth_test.go +++ /dev/null @@ -1,185 +0,0 @@ -package ecosystem - -import ( - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/common" - "github.com/stretchr/testify/assert" - "testing" -) - -var ( - k8sK8sBlueprintOperator = common.QualifiedComponentName{Namespace: "k8s", SimpleName: "k8s-blueprint-operator"} - k8sK8sDoguOperator = common.QualifiedComponentName{Namespace: "k8s", SimpleName: "k8s-dogu-operator"} - k8sK8sLonghorn = common.QualifiedComponentName{Namespace: "k8s", SimpleName: "k8s-longhorn"} - k8sK8sVelero = common.QualifiedComponentName{Namespace: "k8s", SimpleName: "k8s-velero"} -) - -func TestComponentHealthResult_String(t *testing.T) { - tests := []struct { - name string - healthStates map[HealthStatus][]common.SimpleComponentName - contains []string - notContains []string - }{ - { - name: "no components should result in 0 components unhealthy", - healthStates: map[HealthStatus][]common.SimpleComponentName{}, - contains: []string{"0 component(s) are unhealthy: "}, - }, - { - name: "only available components should result in 0 components unhealthy", - healthStates: map[HealthStatus][]common.SimpleComponentName{ - AvailableHealthStatus: {"k8s-dogu-operator"}, - }, - contains: []string{"0 component(s) are unhealthy: "}, - notContains: []string{"k8s-dogu-operator"}, - }, - { - name: "any components not available should be unhealthy", - healthStates: map[HealthStatus][]common.SimpleComponentName{ - AvailableHealthStatus: {"k8s-blueprint-operator"}, - UnavailableHealthStatus: {"k8s-etcd", "k8s-dogu-operator"}, - NotInstalledHealthStatus: {"k8s-service-discovery"}, - "other": {"k8s-component-operator"}, - }, - contains: []string{ - "4 component(s) are unhealthy: ", - "k8s-etcd", - "k8s-dogu-operator", - "k8s-service-discovery", - "k8s-component-operator", - }, - notContains: []string{"ks8-blueprint-operator"}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := ComponentHealthResult{ComponentsByStatus: tt.healthStates} - actual := result.String() - for _, contains := range tt.contains { - assert.Contains(t, actual, contains) - } - for _, notContains := range tt.notContains { - assert.NotContains(t, actual, notContains) - } - }) - } -} - -func TestComponentHealthResult_AllHealthy(t *testing.T) { - tests := []struct { - name string - healthStates map[HealthStatus][]common.SimpleComponentName - want bool - }{ - { - name: "should be healthy if empty", - healthStates: map[HealthStatus][]common.SimpleComponentName{}, - want: true, - }, - { - name: "should be healthy if all are available", - healthStates: map[HealthStatus][]common.SimpleComponentName{ - AvailableHealthStatus: {"k8s-blueprint-operator", "k8s-etcd", "k8s-service-discovery"}, - }, - want: true, - }, - { - name: "should not be healthy if one is not available", - healthStates: map[HealthStatus][]common.SimpleComponentName{ - AvailableHealthStatus: {"k8s-blueprint-operator", "k8s-etcd", "k8s-service-discovery"}, - UnavailableHealthStatus: {"k8s-dogu-operator"}, - }, - want: false, - }, - { - name: "should not be healthy if multiple are not available", - healthStates: map[HealthStatus][]common.SimpleComponentName{ - AvailableHealthStatus: {"k8s-blueprint-operator", "k8s-etcd", "k8s-service-discovery"}, - UnavailableHealthStatus: {"k8s-dogu-operator", "k8s-component-operator"}, - PendingHealthStatus: {"k8s-longhorn"}, - NotInstalledHealthStatus: {"k8s-backup-operator"}, - "other": {"k8s-velero"}, - }, - want: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := ComponentHealthResult{ComponentsByStatus: tt.healthStates} - assert.Equalf(t, tt.want, result.AllHealthy(), "AllHealthy()") - }) - } -} - -func TestCalculateComponentHealthResult(t *testing.T) { - type args struct { - installedComponents map[common.SimpleComponentName]*ComponentInstallation - requiredComponents []RequiredComponent - } - tests := []struct { - name string - args args - want ComponentHealthResult - }{ - { - name: "result should be empty for no required and no installed components", - args: args{ - installedComponents: map[common.SimpleComponentName]*ComponentInstallation{}, - requiredComponents: []RequiredComponent{}, - }, - want: ComponentHealthResult{ComponentsByStatus: map[HealthStatus][]common.SimpleComponentName{}}, - }, - { - name: "result should contain components that are not installed but required", - args: args{ - installedComponents: map[common.SimpleComponentName]*ComponentInstallation{}, - requiredComponents: []RequiredComponent{{Name: "k8s-etcd"}, {Name: "k8s-service-discovery"}}, - }, - want: ComponentHealthResult{ComponentsByStatus: map[HealthStatus][]common.SimpleComponentName{ - NotInstalledHealthStatus: {"k8s-etcd", "k8s-service-discovery"}, - }}, - }, - { - name: "result should contain any components with their health state", - args: args{ - installedComponents: map[common.SimpleComponentName]*ComponentInstallation{ - "k8s-blueprint-operator": {Name: k8sK8sBlueprintOperator, Health: AvailableHealthStatus}, - "k8s-dogu-operator": {Name: k8sK8sDoguOperator, Health: UnavailableHealthStatus}, - "k8s-longhorn": {Name: k8sK8sLonghorn, Health: PendingHealthStatus}, - "k8s-velero": {Name: k8sK8sVelero, Health: "other"}, - }, - requiredComponents: []RequiredComponent{}, - }, - want: ComponentHealthResult{ComponentsByStatus: map[HealthStatus][]common.SimpleComponentName{ - AvailableHealthStatus: {"k8s-blueprint-operator"}, - UnavailableHealthStatus: {"k8s-dogu-operator"}, - PendingHealthStatus: {"k8s-longhorn"}, - "other": {"k8s-velero"}, - }}, - }, - { - name: "result should contain any components with their health state and components that are not installed but required", - args: args{ - installedComponents: map[common.SimpleComponentName]*ComponentInstallation{ - "k8s-blueprint-operator": {Name: k8sK8sBlueprintOperator, Health: AvailableHealthStatus}, - "k8s-dogu-operator": {Name: k8sK8sDoguOperator, Health: UnavailableHealthStatus}, - "k8s-longhorn": {Name: k8sK8sLonghorn, Health: PendingHealthStatus}, - "k8s-velero": {Name: k8sK8sVelero, Health: "other"}, - }, - requiredComponents: []RequiredComponent{{Name: "k8s-etcd"}, {Name: "k8s-service-discovery"}}, - }, - want: ComponentHealthResult{ComponentsByStatus: map[HealthStatus][]common.SimpleComponentName{ - AvailableHealthStatus: {"k8s-blueprint-operator"}, - UnavailableHealthStatus: {"k8s-dogu-operator"}, - PendingHealthStatus: {"k8s-longhorn"}, - "other": {"k8s-velero"}, - NotInstalledHealthStatus: {"k8s-etcd", "k8s-service-discovery"}, - }}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assert.Equalf(t, tt.want, CalculateComponentHealthResult(tt.args.installedComponents, tt.args.requiredComponents), "CalculateComponentHealthResult(%v, %v)", tt.args.installedComponents, tt.args.requiredComponents) - }) - } -} diff --git a/pkg/domain/ecosystem/componentInstallation.go b/pkg/domain/ecosystem/componentInstallation.go deleted file mode 100644 index 3e91e5e6..00000000 --- a/pkg/domain/ecosystem/componentInstallation.go +++ /dev/null @@ -1,69 +0,0 @@ -package ecosystem - -import ( - "github.com/Masterminds/semver/v3" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/common" -) - -// ComponentInstallation represents an installed or to be installed component in the ecosystem. -type ComponentInstallation struct { - // Name identifies the component by simple dogu name and namespace, e.g 'k8s/k8s-dogu-operator'. - Name common.QualifiedComponentName - // ExpectedVersion is the version of the component which should be installed - ExpectedVersion *semver.Version - // ActualVersion is the version of the component which is actually installed - ActualVersion *semver.Version - // Status is the installation status of the component in the ecosystem - Status string - // Health is the current health status of the component in the ecosystem - Health HealthStatus - // PersistenceContext can hold generic values needed for persistence with repositories, e.g. version counters or transaction contexts. - // This field has a generic map type as the values within it highly depend on the used type of repository. - // This field should be ignored in the whole domain. - PersistenceContext map[string]interface{} - DeployConfig DeployConfig -} - -// TODO: Unused constants needed? -const ( - // ComponentStatusNotInstalled represents a status for a component that is not installed - ComponentStatusNotInstalled = "" - // ComponentStatusInstalling represents a status for a component that is currently being installed - ComponentStatusInstalling = "installing" - // ComponentStatusUpgrading represents a status for a component that is currently being upgraded - ComponentStatusUpgrading = "upgrading" - // ComponentStatusDeleting represents a status for a component that is currently being deleted - ComponentStatusDeleting = "deleting" - ComponentStatusIgnored = "ignored" - // ComponentStatusInstalled represents a status for a component that was successfully installed - ComponentStatusInstalled = "installed" - // ComponentStatusTryToInstall represents a status for a component that is not installed but its install process is in requeue loop. - ComponentStatusTryToInstall = "tryToInstall" - // ComponentStatusTryToUpgrade represents a status for a component that is installed but its actual upgrade process is in requeue loop. - // In this state the component can be healthy but the version in the spec is not installed. - ComponentStatusTryToUpgrade = "tryToUpgrade" - // ComponentStatusTryToDelete represents a status for a component that is installed but its delete process is in requeue loop. - // In this state the component can be healthy. - ComponentStatusTryToDelete = "tryToDelete" -) - -// InstallComponent is a factory for new ComponentInstallation's. -func InstallComponent( - componentName common.QualifiedComponentName, - expectedVersion *semver.Version, - deployConfig DeployConfig, -) *ComponentInstallation { - return &ComponentInstallation{ - Name: componentName, - ExpectedVersion: expectedVersion, - DeployConfig: deployConfig, - } -} - -func (ci *ComponentInstallation) Upgrade(expectedVersion *semver.Version) { - ci.ExpectedVersion = expectedVersion -} - -func (ci *ComponentInstallation) UpdateDeployConfig(deployConfig DeployConfig) { - ci.DeployConfig = deployConfig -} diff --git a/pkg/domain/ecosystem/componentInstallation_test.go b/pkg/domain/ecosystem/componentInstallation_test.go deleted file mode 100644 index 207bc910..00000000 --- a/pkg/domain/ecosystem/componentInstallation_test.go +++ /dev/null @@ -1,102 +0,0 @@ -package ecosystem - -import ( - "github.com/Masterminds/semver/v3" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/common" - "github.com/stretchr/testify/assert" - "testing" -) - -var testComponentName = k8sK8sDoguOperator - -var ( - testVersion1, _ = semver.NewVersion("1.0.0") - testVersion2, _ = semver.NewVersion("2.0.0") -) - -func TestInstallComponent(t *testing.T) { - type args struct { - componentName common.QualifiedComponentName - version *semver.Version - deployConfig DeployConfig - } - tests := []struct { - name string - args args - want *ComponentInstallation - }{ - { - name: "success", - args: args{ - componentName: testComponentName, - version: testVersion1, - deployConfig: map[string]interface{}{"deployNamespace": "longhorn-system"}, - }, - want: &ComponentInstallation{ - Name: testComponentName, - ExpectedVersion: testVersion1, - DeployConfig: map[string]interface{}{"deployNamespace": "longhorn-system"}, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assert.Equalf(t, tt.want, InstallComponent(tt.args.componentName, tt.args.version, tt.args.deployConfig), "InstallComponent(%v, %v, %v)", tt.args.componentName, tt.args.version) - }) - } -} - -func TestComponentInstallation_Upgrade(t *testing.T) { - type fields struct { - Name common.QualifiedComponentName - Version *semver.Version - Status string - PersistenceContext map[string]interface{} - Health HealthStatus - } - type args struct { - version *semver.Version - } - tests := []struct { - name string - fields fields - args args - }{ - { - name: "should set the version parameter in struct", - fields: fields{ - Version: testVersion1, - }, - args: args{ - version: testVersion2, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ci := &ComponentInstallation{ - Name: tt.fields.Name, - ExpectedVersion: tt.fields.Version, - Status: tt.fields.Status, - PersistenceContext: tt.fields.PersistenceContext, - Health: tt.fields.Health, - } - ci.Upgrade(tt.args.version) - assert.Equal(t, tt.args.version, ci.ExpectedVersion) - }) - } -} - -func TestComponentInstallation_UpdateDeployConfig(t *testing.T) { - t.Run("should set config", func(t *testing.T) { - // given - sut := ComponentInstallation{} - config := map[string]interface{}{"key": "value"} - - // when - sut.UpdateDeployConfig(config) - - // then - assert.Equal(t, DeployConfig(config), sut.DeployConfig) - }) -} diff --git a/pkg/domain/ecosystem/doguHealth_test.go b/pkg/domain/ecosystem/doguHealth_test.go index 8a87e2a2..9a655470 100644 --- a/pkg/domain/ecosystem/doguHealth_test.go +++ b/pkg/domain/ecosystem/doguHealth_test.go @@ -1,9 +1,10 @@ package ecosystem import ( + "testing" + cescommons "github.com/cloudogu/ces-commons-lib/dogu" "github.com/stretchr/testify/assert" - "testing" ) const ( @@ -74,12 +75,12 @@ func TestDoguHealthResult_String(t *testing.T) { notContains []string }{ { - name: "no dogus should result in 0 components unhealthy", + name: "no dogus should result in 0 dogus unhealthy", healthStates: map[HealthStatus][]cescommons.SimpleName{}, contains: []string{"0 dogu(s) are unhealthy: "}, }, { - name: "only available dogus should result in 0 components unhealthy", + name: "only available dogus should result in 0 dogus unhealthy", healthStates: map[HealthStatus][]cescommons.SimpleName{ AvailableHealthStatus: {"nginx-ingress"}, }, diff --git a/pkg/domain/ecosystem/doguInstallation.go b/pkg/domain/ecosystem/doguInstallation.go index 6b75851f..df20554c 100644 --- a/pkg/domain/ecosystem/doguInstallation.go +++ b/pkg/domain/ecosystem/doguInstallation.go @@ -2,45 +2,43 @@ package ecosystem import ( "fmt" + cescommons "github.com/cloudogu/ces-commons-lib/dogu" "github.com/cloudogu/cesapp-lib/core" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // DoguInstallation represents an installed or to be installed dogu in the ecosystem. type DoguInstallation struct { // Name identifies the dogu by simple dogu name and namespace. Name cescommons.QualifiedName - // Version is the version of the dogu + // Version is the desired version of the dogu Version core.Version // Status is the installation status of the dogu in the ecosystem Status string // Health is the current health status of the dogu in the ecosystem Health HealthStatus + // InstalledVersion is the current version of the dogu + InstalledVersion core.Version + // StartedAt contains the time of the last restart of the dogu. + StartedAt metav1.Time // UpgradeConfig contains configuration for dogu upgrades UpgradeConfig UpgradeConfig + // PauseReconciliation indicates whether the reconciliation loop should be running (pauseReconciliation=false) or not (pauseReconciliation=true). + PauseReconciliation bool // PersistenceContext can hold generic values needed for persistence with repositories, e.g. version counters or transaction contexts. // This field has a generic map type as the values within it highly depend on the used type of repository. // This field should be ignored in the whole domain. PersistenceContext map[string]interface{} // MinVolumeSize is the minimum storage of the dogu. This field is optional and can be nil to indicate that no // storage is needed. - MinVolumeSize VolumeSize + MinVolumeSize *VolumeSize // ReverseProxyConfig defines configuration for the ecosystem reverse proxy. This field is optional. ReverseProxyConfig ReverseProxyConfig // AdditionalMounts provides the possibility to mount additional data into the dogu. AdditionalMounts []AdditionalMount } -// TODO: Unused constants needed? -const ( - DoguStatusNotInstalled = "" - DoguStatusInstalling = "installing" - DoguStatusUpgrading = "upgrading" - DoguStatusDeleting = "deleting" - DoguStatusInstalled = "installed" - DoguStatusPVCResizing = "resizing PVC" -) - // Specific Nginx annotations. In future those annotations will be replaced be generalized fields in the dogu cr. // The dogu-operator or service-discovery will interpret them. const ( @@ -55,6 +53,10 @@ type ReverseProxyConfig struct { AdditionalConfig AdditionalConfig } +func (r *ReverseProxyConfig) IsEmpty() bool { + return r.MaxBodySize == nil && r.RewriteTarget == "" && r.AdditionalConfig == "" +} + // UpgradeConfig contains configuration hints regarding aspects during the upgrade of dogus. type UpgradeConfig struct { // AllowNamespaceSwitch lets a dogu switch its dogu namespace during an upgrade. The dogu must be technically the @@ -84,20 +86,25 @@ type AdditionalMount struct { // Volume is the name of the volume to which the data should be mounted. It is defined in the respective dogu.json. Volume string // Subfolder defines a subfolder in which the data should be put within the volume. - // +optional Subfolder string } // InstallDogu is a factory for new DoguInstallation's. func InstallDogu( name cescommons.QualifiedName, - version core.Version, - minVolumeSize VolumeSize, + version *core.Version, + minVolumeSize *VolumeSize, reverseProxyConfig ReverseProxyConfig, additionalMounts []AdditionalMount) *DoguInstallation { + + doguVersion := core.Version{} + if version != nil { + doguVersion = *version + } + return &DoguInstallation{ Name: name, - Version: version, + Version: doguVersion, UpgradeConfig: UpgradeConfig{AllowNamespaceSwitch: false}, MinVolumeSize: minVolumeSize, ReverseProxyConfig: reverseProxyConfig, @@ -109,8 +116,20 @@ func (dogu *DoguInstallation) IsHealthy() bool { return dogu.Health == AvailableHealthStatus } -func (dogu *DoguInstallation) Upgrade(newVersion core.Version) { - dogu.Version = newVersion +func (dogu *DoguInstallation) IsVersionUpToDate() bool { + return dogu.Version.IsEqualTo(dogu.InstalledVersion) +} + +func (dogu *DoguInstallation) IsConfigUpToDate(globalConfigUpdateTime *metav1.Time, doguConfigUpdateTime *metav1.Time) bool { + return !dogu.StartedAt.Before(globalConfigUpdateTime) && !dogu.StartedAt.Before(doguConfigUpdateTime) +} + +func (dogu *DoguInstallation) Upgrade(newVersion *core.Version) { + dogu.Version = core.Version{} + if newVersion != nil { + dogu.Version = *newVersion + } + dogu.UpgradeConfig.AllowNamespaceSwitch = false } @@ -123,22 +142,18 @@ func (dogu *DoguInstallation) SwitchNamespace(newNamespace cescommons.Namespace, return nil } -func (dogu *DoguInstallation) UpdateProxyBodySize(value *BodySize) { - dogu.ReverseProxyConfig.MaxBodySize = value -} - -func (dogu *DoguInstallation) UpdateMinVolumeSize(size VolumeSize) { +func (dogu *DoguInstallation) UpdateMinVolumeSize(size *VolumeSize) { dogu.MinVolumeSize = size } -func (dogu *DoguInstallation) UpdateProxyRewriteTarget(value RewriteTarget) { - dogu.ReverseProxyConfig.RewriteTarget = value -} - -func (dogu *DoguInstallation) UpdateProxyAdditionalConfig(value AdditionalConfig) { - dogu.ReverseProxyConfig.AdditionalConfig = value +func (dogu *DoguInstallation) UpdateProxyConfig(config ReverseProxyConfig) { + dogu.ReverseProxyConfig = config } func (dogu *DoguInstallation) UpdateAdditionalMounts(mounts []AdditionalMount) { dogu.AdditionalMounts = mounts } + +func (dogu *DoguInstallation) SetReconciliationPaused(isPaused bool) { + dogu.PauseReconciliation = isPaused +} diff --git a/pkg/domain/ecosystem/doguInstallation_test.go b/pkg/domain/ecosystem/doguInstallation_test.go index c90f131f..b52c17f0 100644 --- a/pkg/domain/ecosystem/doguInstallation_test.go +++ b/pkg/domain/ecosystem/doguInstallation_test.go @@ -1,31 +1,39 @@ package ecosystem import ( + "testing" + "time" + cescommons "github.com/cloudogu/ces-commons-lib/dogu" "github.com/cloudogu/cesapp-lib/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/api/resource" - "testing" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -var version1231, _ = core.ParseVersion("1.2.3-1") -var version1232, _ = core.ParseVersion("1.2.3-2") +var ( + version1231, _ = core.ParseVersion("1.2.3-1") + version1232, _ = core.ParseVersion("1.2.3-2") + rewriteTarget = "/" + additionalConfig = "additional" + subfolder = "different_subfolder" +) func TestInstallDogu(t *testing.T) { volumeSize := resource.MustParse("1Gi") proxyBodySize := resource.MustParse("1G") dogu := InstallDogu( postgresqlQualifiedName, - version1231, - volumeSize, - ReverseProxyConfig{MaxBodySize: &proxyBodySize, RewriteTarget: "/", AdditionalConfig: "additional"}, + &version1231, + &volumeSize, + ReverseProxyConfig{MaxBodySize: &proxyBodySize, RewriteTarget: RewriteTarget(rewriteTarget), AdditionalConfig: AdditionalConfig(additionalConfig)}, []AdditionalMount{ { SourceType: DataSourceConfigMap, Name: "configmap", Volume: "volume", - Subfolder: "different_subfolder", + Subfolder: subfolder, }, }, ) @@ -33,18 +41,18 @@ func TestInstallDogu(t *testing.T) { Name: postgresqlQualifiedName, Version: version1231, UpgradeConfig: UpgradeConfig{AllowNamespaceSwitch: false}, - MinVolumeSize: volumeSize, + MinVolumeSize: &volumeSize, ReverseProxyConfig: ReverseProxyConfig{ MaxBodySize: &proxyBodySize, - RewriteTarget: "/", - AdditionalConfig: "additional", + RewriteTarget: RewriteTarget(rewriteTarget), + AdditionalConfig: AdditionalConfig(additionalConfig), }, AdditionalMounts: []AdditionalMount{ { SourceType: DataSourceConfigMap, Name: "configmap", Volume: "volume", - Subfolder: "different_subfolder", + Subfolder: subfolder, }, }, }, dogu) @@ -80,7 +88,7 @@ func TestDoguInstallation_Upgrade(t *testing.T) { Version: version1231, } - dogu.Upgrade(version1232) + dogu.Upgrade(&version1232) assert.Equal(t, &DoguInstallation{ Name: postgresqlQualifiedName, @@ -119,56 +127,198 @@ func TestDoguInstallation_SwitchNamespace(t *testing.T) { }) } -func TestDoguInstallation_UpdateProxyBodySize(t *testing.T) { +func TestDoguInstallation_UpdateProxyConfig(t *testing.T) { t.Run("should set property", func(t *testing.T) { // given bodySize := resource.MustParse("1G") dogu := DoguInstallation{} // when - dogu.UpdateProxyBodySize(&bodySize) + reverseProxyConfig := ReverseProxyConfig{ + MaxBodySize: &bodySize, + RewriteTarget: RewriteTarget(rewriteTarget), + AdditionalConfig: AdditionalConfig(additionalConfig), + } + dogu.UpdateProxyConfig(reverseProxyConfig) // then assert.Equal(t, &bodySize, dogu.ReverseProxyConfig.MaxBodySize) + assert.Equal(t, RewriteTarget(rewriteTarget), dogu.ReverseProxyConfig.RewriteTarget) + assert.Equal(t, AdditionalConfig(additionalConfig), dogu.ReverseProxyConfig.AdditionalConfig) }) } -func TestDoguInstallation_UpdateProxyRewriteTarget(t *testing.T) { +func TestDoguInstallation_UpdateMinVolumeSize(t *testing.T) { t.Run("should set property", func(t *testing.T) { // given + volumeSize := resource.MustParse("1Gi") dogu := DoguInstallation{} // when - dogu.UpdateProxyRewriteTarget("/") + dogu.UpdateMinVolumeSize(&volumeSize) // then - assert.Equal(t, RewriteTarget("/"), dogu.ReverseProxyConfig.RewriteTarget) + assert.Equal(t, &volumeSize, dogu.MinVolumeSize) }) } -func TestDoguInstallation_UpdateProxyAdditionalConfig(t *testing.T) { - t.Run("should set property", func(t *testing.T) { - // given - dogu := DoguInstallation{} - - // when - dogu.UpdateProxyAdditionalConfig("config") - - // then - assert.Equal(t, AdditionalConfig("config"), dogu.ReverseProxyConfig.AdditionalConfig) - }) +func TestDoguInstallation_IsVersionUpToDate(t *testing.T) { + type fields struct { + Version core.Version + InstalledVersion core.Version + } + tests := []struct { + name string + fields fields + want bool + }{ + { + name: "equal Versions is up to date", + fields: fields{ + Version: version1231, + InstalledVersion: version1231, + }, + want: true, + }, + { + name: "Version newer than installed version is not up to date", + fields: fields{ + Version: version1232, + InstalledVersion: version1231, + }, + want: false, + }, + { + name: "Installed version empty is not up to date", + fields: fields{ + Version: version1232, + InstalledVersion: core.Version{}, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dogu := &DoguInstallation{ + Version: tt.fields.Version, + InstalledVersion: tt.fields.InstalledVersion, + } + assert.Equalf(t, tt.want, dogu.IsVersionUpToDate(), "IsVersionUpToDate()") + }) + } } -func TestDoguInstallation_UpdateMinVolumeSize(t *testing.T) { - t.Run("should set property", func(t *testing.T) { - // given - volumeSize := resource.MustParse("1Gi") - dogu := DoguInstallation{} - - // when - dogu.UpdateMinVolumeSize(volumeSize) - - // then - assert.Equal(t, volumeSize, dogu.MinVolumeSize) - }) +func TestDoguInstallation_IsConfigUpToDate(t *testing.T) { + timeMay := v1.NewTime(time.Date(2024, time.May, 23, 10, 0, 0, 0, time.UTC)) + timeJune := v1.NewTime(time.Date(2024, time.June, 23, 10, 0, 0, 0, time.UTC)) + timeJuly := v1.NewTime(time.Date(2024, time.July, 23, 10, 0, 0, 0, time.UTC)) + type args struct { + globalConfigUpdateTime *v1.Time + doguConfigUpdateTime *v1.Time + } + tests := []struct { + name string + StartedAt v1.Time + args args + want bool + }{ + { + name: "all equal is up to date", + StartedAt: timeMay, + args: args{ + globalConfigUpdateTime: &timeMay, + doguConfigUpdateTime: &timeMay, + }, + want: true, + }, + { + name: "StartedAt newest date is up to date", + StartedAt: timeJuly, + args: args{ + globalConfigUpdateTime: &timeMay, + doguConfigUpdateTime: &timeJune, + }, + want: true, + }, + { + name: "globalConfigUpdateTime newest date is not up to date", + StartedAt: timeJune, + args: args{ + globalConfigUpdateTime: &timeJuly, + doguConfigUpdateTime: &timeMay, + }, + want: false, + }, + { + name: "doguConfigUpdateTime newest date is not up to date", + StartedAt: timeJune, + args: args{ + globalConfigUpdateTime: &timeMay, + doguConfigUpdateTime: &timeJuly, + }, + want: false, + }, + { + name: "doguConfigUpdateTime nil and StartedAt newest is up to date", + StartedAt: timeJune, + args: args{ + globalConfigUpdateTime: &timeMay, + doguConfigUpdateTime: nil, + }, + want: true, + }, + { + name: "doguConfigUpdateTime nil and StartedAt not newest is not up to date", + StartedAt: timeJune, + args: args{ + globalConfigUpdateTime: &timeJuly, + doguConfigUpdateTime: nil, + }, + want: false, + }, + { + name: "globalConfigUpdateTime nil and StartedAt newest is up to date", + StartedAt: timeJune, + args: args{ + globalConfigUpdateTime: nil, + doguConfigUpdateTime: &timeMay, + }, + want: true, + }, + { + name: "globalConfigUpdateTime nil and StartedAt not newest is not up to date", + StartedAt: timeJune, + args: args{ + globalConfigUpdateTime: nil, + doguConfigUpdateTime: &timeJuly, + }, + want: false, + }, + { + name: "globalConfigUpdateTime nil and doguConfigUpdateTime nil is up to date", + StartedAt: timeJune, + args: args{ + globalConfigUpdateTime: nil, + doguConfigUpdateTime: nil, + }, + want: true, + }, + { + name: "StartedAt empty is not up to date", + StartedAt: v1.Time{}, + args: args{ + globalConfigUpdateTime: &timeMay, + doguConfigUpdateTime: nil, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dogu := &DoguInstallation{ + StartedAt: tt.StartedAt, + } + assert.Equalf(t, tt.want, dogu.IsConfigUpToDate(tt.args.globalConfigUpdateTime, tt.args.doguConfigUpdateTime), "IsConfigUpToDate(%v, %v)", tt.args.globalConfigUpdateTime, tt.args.doguConfigUpdateTime) + }) + } } diff --git a/pkg/domain/ecosystem/ecosystemConfig.go b/pkg/domain/ecosystem/ecosystemConfig.go index c0d33d11..50cf0d4d 100644 --- a/pkg/domain/ecosystem/ecosystemConfig.go +++ b/pkg/domain/ecosystem/ecosystemConfig.go @@ -25,7 +25,7 @@ type DoguConfigEntry struct { } type SensitiveDoguConfigEntry struct { - Key common.SensitiveDoguConfigKey + Key common.DoguConfigKey Value common.SensitiveDoguConfigValue // PersistenceContext can hold generic values needed for persistence with repositories, e.g. version counters or transaction contexts. // This field has a generic map type as the values within it highly depend on the used type of repository. diff --git a/pkg/domain/ecosystem/ecosystemHealth.go b/pkg/domain/ecosystem/ecosystemHealth.go index 4517ae04..add1c479 100644 --- a/pkg/domain/ecosystem/ecosystemHealth.go +++ b/pkg/domain/ecosystem/ecosystemHealth.go @@ -1,29 +1,22 @@ package ecosystem -import ( - "fmt" -) - type HealthStatus = string const ( - PendingHealthStatus HealthStatus = "" - AvailableHealthStatus HealthStatus = "available" - UnavailableHealthStatus HealthStatus = "unavailable" - NotInstalledHealthStatus HealthStatus = "not installed" + PendingHealthStatus HealthStatus = "" + AvailableHealthStatus HealthStatus = "available" + UnavailableHealthStatus HealthStatus = "unavailable" ) // HealthResult is a snapshot of the health states of all relevant parts of the running ecosystem. type HealthResult struct { - DoguHealth DoguHealthResult - ComponentHealth ComponentHealthResult + DoguHealth DoguHealthResult } func (result HealthResult) String() string { - return fmt.Sprintf("ecosystem health:\n %s\n %s", result.DoguHealth, result.ComponentHealth) + return result.DoguHealth.String() } func (result HealthResult) AllHealthy() bool { - return result.DoguHealth.AllHealthy() && - result.ComponentHealth.AllHealthy() + return result.DoguHealth.AllHealthy() } diff --git a/pkg/domain/ecosystem/ecosystemHealth_test.go b/pkg/domain/ecosystem/ecosystemHealth_test.go index a75f4044..ada02f4d 100644 --- a/pkg/domain/ecosystem/ecosystemHealth_test.go +++ b/pkg/domain/ecosystem/ecosystemHealth_test.go @@ -1,16 +1,15 @@ package ecosystem import ( + "testing" + cescommons "github.com/cloudogu/ces-commons-lib/dogu" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/common" "github.com/stretchr/testify/assert" - "testing" ) func TestHealthResult_String(t *testing.T) { type fields struct { - DoguHealth DoguHealthResult - ComponentHealth ComponentHealthResult + DoguHealth DoguHealthResult } tests := []struct { name string @@ -18,27 +17,24 @@ func TestHealthResult_String(t *testing.T) { want string }{ { - name: "should print dogu and component health results with no unhealthy", + name: "should print dogu health results with no unhealthy", fields: fields{ - DoguHealth: DoguHealthResult{}, - ComponentHealth: ComponentHealthResult{}, + DoguHealth: DoguHealthResult{}, }, - want: "ecosystem health:\n 0 dogu(s) are unhealthy: \n 0 component(s) are unhealthy: ", + want: "0 dogu(s) are unhealthy: ", }, { - name: "should print dogu and component health results with unhealthy", + name: "should print dogu health results with unhealthy", fields: fields{ - DoguHealth: DoguHealthResult{DogusByStatus: map[HealthStatus][]cescommons.SimpleName{UnavailableHealthStatus: {"nginx-ingress"}}}, - ComponentHealth: ComponentHealthResult{ComponentsByStatus: map[HealthStatus][]common.SimpleComponentName{UnavailableHealthStatus: {"k8s-etcd"}}}, + DoguHealth: DoguHealthResult{DogusByStatus: map[HealthStatus][]cescommons.SimpleName{UnavailableHealthStatus: {"nginx-ingress"}}}, }, - want: "ecosystem health:\n 1 dogu(s) are unhealthy: nginx-ingress\n 1 component(s) are unhealthy: k8s-etcd", + want: "1 dogu(s) are unhealthy: nginx-ingress", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := HealthResult{ - DoguHealth: tt.fields.DoguHealth, - ComponentHealth: tt.fields.ComponentHealth, + DoguHealth: tt.fields.DoguHealth, } assert.Equalf(t, tt.want, result.String(), "String()") }) @@ -47,8 +43,7 @@ func TestHealthResult_String(t *testing.T) { func TestHealthResult_AllHealthy(t *testing.T) { type fields struct { - DoguHealth DoguHealthResult - ComponentHealth ComponentHealthResult + DoguHealth DoguHealthResult } tests := []struct { name string @@ -56,34 +51,16 @@ func TestHealthResult_AllHealthy(t *testing.T) { want bool }{ { - name: "should be healthy if no dogus or components are unavailable", + name: "should be healthy if no dogus are unavailable", fields: fields{ - DoguHealth: DoguHealthResult{DogusByStatus: map[HealthStatus][]cescommons.SimpleName{AvailableHealthStatus: {"nginx-ingress"}}}, - ComponentHealth: ComponentHealthResult{ComponentsByStatus: map[HealthStatus][]common.SimpleComponentName{AvailableHealthStatus: {"k8s-etcd"}}}, + DoguHealth: DoguHealthResult{DogusByStatus: map[HealthStatus][]cescommons.SimpleName{AvailableHealthStatus: {"nginx-ingress"}}}, }, want: true, }, { name: "should be unhealthy if dogus are unavailable", fields: fields{ - DoguHealth: DoguHealthResult{DogusByStatus: map[HealthStatus][]cescommons.SimpleName{UnavailableHealthStatus: {"nginx-ingress"}}}, - ComponentHealth: ComponentHealthResult{ComponentsByStatus: map[HealthStatus][]common.SimpleComponentName{AvailableHealthStatus: {"k8s-etcd"}}}, - }, - want: false, - }, - { - name: "should be unhealthy if components are unavailable", - fields: fields{ - DoguHealth: DoguHealthResult{DogusByStatus: map[HealthStatus][]cescommons.SimpleName{AvailableHealthStatus: {"nginx-ingress"}}}, - ComponentHealth: ComponentHealthResult{ComponentsByStatus: map[HealthStatus][]common.SimpleComponentName{UnavailableHealthStatus: {"k8s-etcd"}}}, - }, - want: false, - }, - { - name: "should be unhealthy if dogus and components are unavailable", - fields: fields{ - DoguHealth: DoguHealthResult{DogusByStatus: map[HealthStatus][]cescommons.SimpleName{UnavailableHealthStatus: {"nginx-ingress"}}}, - ComponentHealth: ComponentHealthResult{ComponentsByStatus: map[HealthStatus][]common.SimpleComponentName{UnavailableHealthStatus: {"k8s-etcd"}}}, + DoguHealth: DoguHealthResult{DogusByStatus: map[HealthStatus][]cescommons.SimpleName{UnavailableHealthStatus: {"nginx-ingress"}}}, }, want: false, }, @@ -91,8 +68,7 @@ func TestHealthResult_AllHealthy(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := HealthResult{ - DoguHealth: tt.fields.DoguHealth, - ComponentHealth: tt.fields.ComponentHealth, + DoguHealth: tt.fields.DoguHealth, } assert.Equalf(t, tt.want, result.AllHealthy(), "AllHealthy()") }) diff --git a/pkg/domain/ecosystem/volumeSize.go b/pkg/domain/ecosystem/volumeSize.go index 25d1e01b..c478f142 100644 --- a/pkg/domain/ecosystem/volumeSize.go +++ b/pkg/domain/ecosystem/volumeSize.go @@ -5,16 +5,15 @@ import "k8s.io/apimachinery/pkg/api/resource" type VolumeSize = resource.Quantity func GetQuantityReference(quantityStr string) (*resource.Quantity, error) { - var quantityPtr *resource.Quantity var quantityValue resource.Quantity var err error if quantityStr != "" && quantityStr != "" { quantityValue, err = resource.ParseQuantity(quantityStr) if err == nil { - quantityPtr = &quantityValue + return &quantityValue, nil } } - return quantityPtr, err + return nil, err } func GetNonNilQuantityRef(quantityStr string) (*resource.Quantity, error) { @@ -25,10 +24,10 @@ func GetNonNilQuantityRef(quantityStr string) (*resource.Quantity, error) { return quantityPtr, err } -func GetQuantityString(quantity *resource.Quantity) string { +func GetQuantityString(quantity *resource.Quantity) *string { if quantity == nil { - return "" + return nil } - - return quantity.String() + quantityStr := quantity.String() + return &quantityStr } diff --git a/pkg/domain/ecosystem/volumeSize_test.go b/pkg/domain/ecosystem/volumeSize_test.go index aa676f1c..92957a89 100644 --- a/pkg/domain/ecosystem/volumeSize_test.go +++ b/pkg/domain/ecosystem/volumeSize_test.go @@ -2,9 +2,10 @@ package ecosystem import ( "fmt" + "testing" + "github.com/stretchr/testify/assert" "k8s.io/apimachinery/pkg/api/resource" - "testing" ) func TestGetQuantityReference(t *testing.T) { @@ -44,23 +45,24 @@ func TestGetQuantityReference(t *testing.T) { func TestGetQuantityString(t *testing.T) { twoGigaByte := resource.MustParse("2G") + twoGigaByteString := twoGigaByte.String() type args struct { quantity *resource.Quantity } tests := []struct { name string args args - want string + want *string }{ { name: "should return string if reference is not nil", args: args{quantity: &twoGigaByte}, - want: "2G", + want: &twoGigaByteString, }, { name: "should return empty string if reference is nil", args: args{quantity: nil}, - want: "", + want: nil, }, } for _, tt := range tests { diff --git a/pkg/domain/effectiveBlueprint.go b/pkg/domain/effectiveBlueprint.go index 18bfb60c..025565d4 100644 --- a/pkg/domain/effectiveBlueprint.go +++ b/pkg/domain/effectiveBlueprint.go @@ -3,9 +3,10 @@ package domain import ( "errors" "fmt" + "slices" + cescommons "github.com/cloudogu/ces-commons-lib/dogu" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/util" - "slices" ) // EffectiveBlueprint describes what the wanted state after evaluating the blueprint and the blueprintMask is. @@ -14,10 +15,7 @@ type EffectiveBlueprint struct { // Dogus contains a set of exact dogu versions which should be present or absent in the CES instance after which this // blueprint was applied. Optional. Dogus []Dogu - // Components contains a set of exact components versions which should be present or absent in the CES instance after which - // this blueprint was applied. Optional. - Components []Component - // Config contains all config entries to set via blueprint. + // Config contains all config entries to set via blueprint. Optional. Config Config } @@ -25,7 +23,7 @@ type EffectiveBlueprint struct { func (effectiveBlueprint *EffectiveBlueprint) GetWantedDogus() []Dogu { var wantedDogus []Dogu for _, dogu := range effectiveBlueprint.Dogus { - if dogu.TargetState == TargetStatePresent { + if !dogu.Absent { wantedDogus = append(wantedDogus, dogu) } } diff --git a/pkg/domain/effectiveBlueprint_test.go b/pkg/domain/effectiveBlueprint_test.go new file mode 100644 index 00000000..88448836 --- /dev/null +++ b/pkg/domain/effectiveBlueprint_test.go @@ -0,0 +1,43 @@ +package domain + +import ( + "testing" + + cescommons "github.com/cloudogu/ces-commons-lib/dogu" + "github.com/stretchr/testify/assert" +) + +func TestEffectiveBlueprint_GetWantedDogus(t *testing.T) { + t.Run("should get only present dogus", func(t *testing.T) { + ldapDogu := Dogu{ + Name: cescommons.QualifiedName{ + SimpleName: "ldap", + Namespace: "official", + }, + Version: &version3213, + } + absentMysqlDogu := Dogu{ + Name: cescommons.QualifiedName{ + SimpleName: "mysql", + Namespace: "official", + }, + Absent: true, + } + postgresqlDogu := Dogu{ + Name: cescommons.QualifiedName{ + SimpleName: "postgresql", + Namespace: "official", + }, + Version: &version3213, + } + effectiveBlueprint := &EffectiveBlueprint{ + Dogus: []Dogu{ldapDogu, absentMysqlDogu, postgresqlDogu}, + } + + result := effectiveBlueprint.GetWantedDogus() + + assert.Len(t, result, 2) + assert.Contains(t, result, ldapDogu) + assert.Contains(t, result, postgresqlDogu) + }) +} diff --git a/pkg/domain/errors.go b/pkg/domain/errors.go index 1ee71abf..6cc97364 100644 --- a/pkg/domain/errors.go +++ b/pkg/domain/errors.go @@ -2,6 +2,7 @@ package domain import ( "fmt" + "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" ) @@ -31,9 +32,8 @@ type UnhealthyEcosystemError struct { } func (e *UnhealthyEcosystemError) Error() string { - unhealthyComponentsText := e.healthResult.ComponentHealth.String() unhealthyDogusText := e.healthResult.DoguHealth.String() - combinedMessage := fmt.Sprintf("%s - %s - %s", e.Message, unhealthyDogusText, unhealthyComponentsText) + combinedMessage := fmt.Sprintf("%s - %s", e.Message, unhealthyDogusText) if e.WrappedError != nil { return fmt.Errorf("%s: %w", combinedMessage, e.WrappedError).Error() } @@ -52,3 +52,30 @@ func NewUnhealthyEcosystemError( ) *UnhealthyEcosystemError { return &UnhealthyEcosystemError{WrappedError: wrappedError, Message: message, healthResult: healthResult} } + +// DogusNotUpToDateError indicates that there are dogus that are not yet up to date. +type DogusNotUpToDateError struct { + Message string +} + +func (e *DogusNotUpToDateError) Error() string { + return e.Message +} + +// MultipleBlueprintsError indicates that there are multiple blueprint-resources in this namespace, which the controller cannot handle. +type MultipleBlueprintsError struct { + Message string +} + +// Error marks the struct as an error. +func (e *MultipleBlueprintsError) Error() string { + return e.Message +} + +type StateDiffNotEmptyError struct { + Message string +} + +func (e *StateDiffNotEmptyError) Error() string { + return e.Message +} diff --git a/pkg/domain/events.go b/pkg/domain/events.go index baf018ad..8fa3cace 100644 --- a/pkg/domain/events.go +++ b/pkg/domain/events.go @@ -1,11 +1,14 @@ package domain import ( + "bytes" "fmt" - cescommons "github.com/cloudogu/ces-commons-lib/dogu" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" "slices" "strings" + + cescommons "github.com/cloudogu/ces-commons-lib/dogu" + "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" + "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/util" ) type Event interface { @@ -25,80 +28,6 @@ func (b BlueprintSpecInvalidEvent) Message() string { return b.ValidationError.Error() } -type BlueprintSpecStaticallyValidatedEvent struct{} - -func (b BlueprintSpecStaticallyValidatedEvent) Name() string { - return "BlueprintSpecStaticallyValidated" -} - -func (b BlueprintSpecStaticallyValidatedEvent) Message() string { - return "" -} - -type BlueprintSpecValidatedEvent struct{} - -func (b BlueprintSpecValidatedEvent) Name() string { - return "BlueprintSpecValidated" -} - -func (b BlueprintSpecValidatedEvent) Message() string { - return "" -} - -type EffectiveBlueprintCalculatedEvent struct { - Result EffectiveBlueprint -} - -func (e EffectiveBlueprintCalculatedEvent) Name() string { - return "EffectiveBlueprintCalculated" -} - -func (e EffectiveBlueprintCalculatedEvent) Message() string { - return "" -} - -type GlobalConfigDiffDeterminedEvent struct { - GlobalConfigDiffs GlobalConfigDiffs -} - -func (e GlobalConfigDiffDeterminedEvent) Name() string { - return "GlobalConfigDiffDetermined" -} - -func (e GlobalConfigDiffDeterminedEvent) Message() string { - var stringPerAction []string - var actionsCounter int - for action, amount := range e.GlobalConfigDiffs.countByAction() { - stringPerAction = append(stringPerAction, fmt.Sprintf("%q: %d", action, amount)) - if action != ConfigActionNone { - actionsCounter += amount - } - } - slices.Sort(stringPerAction) - return fmt.Sprintf("global config diff determined: %d changes (%s)", actionsCounter, strings.Join(stringPerAction, ", ")) -} - -type DoguConfigDiffDeterminedEvent struct { - DoguConfigDiffs map[cescommons.SimpleName]DoguConfigDiffs -} - -func NewDoguConfigDiffDeterminedEvent( - doguConfigDiffs map[cescommons.SimpleName]DoguConfigDiffs, -) DoguConfigDiffDeterminedEvent { - return DoguConfigDiffDeterminedEvent{DoguConfigDiffs: doguConfigDiffs} -} - -func (e DoguConfigDiffDeterminedEvent) Name() string { - return "DoguConfigDiffDetermined" -} - -func (e DoguConfigDiffDeterminedEvent) Message() string { - return fmt.Sprintf( - "dogu config diff determined: %s", - generateDoguConfigChangeCounter(e.DoguConfigDiffs), - ) -} - type MissingConfigReferencesEvent struct { err error } @@ -115,70 +44,6 @@ func (e MissingConfigReferencesEvent) Message() string { return e.err.Error() } -type SensitiveDoguConfigDiffDeterminedEvent struct { - SensitiveDoguConfigDiffs map[cescommons.SimpleName]SensitiveDoguConfigDiffs -} - -func NewSensitiveDoguConfigDiffDeterminedEvent( - sensitiveDoguConfigDiffs map[cescommons.SimpleName]SensitiveDoguConfigDiffs, -) SensitiveDoguConfigDiffDeterminedEvent { - return SensitiveDoguConfigDiffDeterminedEvent{SensitiveDoguConfigDiffs: sensitiveDoguConfigDiffs} -} - -func (e SensitiveDoguConfigDiffDeterminedEvent) Name() string { - return "SensitiveDoguConfigDiffDetermined" -} - -func (e SensitiveDoguConfigDiffDeterminedEvent) Message() string { - return fmt.Sprintf( - "sensitive dogu config diff determined: %s", - generateDoguConfigChangeCounter(e.SensitiveDoguConfigDiffs), - ) -} - -func generateDoguConfigChangeCounter(doguConfigDiffs map[cescommons.SimpleName]DoguConfigDiffs) string { - var stringPerAction []string - var actionsCounter int - for action, amount := range countByAction(doguConfigDiffs) { - stringPerAction = append(stringPerAction, fmt.Sprintf("%q: %d", action, amount)) - if action != ConfigActionNone { - actionsCounter += amount - } - } - slices.Sort(stringPerAction) - return fmt.Sprintf("%d changes (%s)", actionsCounter, strings.Join(stringPerAction, ", ")) -} - -// StateDiffComponentDeterminedEvent provides event information over detected changes regarding components. -type StateDiffComponentDeterminedEvent struct { - componentDiffs []ComponentDiff -} - -func newStateDiffComponentEvent(componentDiffs ComponentDiffs) StateDiffComponentDeterminedEvent { - return StateDiffComponentDeterminedEvent{ - componentDiffs: componentDiffs, - } -} - -// Name contains the StateDiffComponentDeterminedEvent display name. -func (s StateDiffComponentDeterminedEvent) Name() string { - return "StateDiffComponentDetermined" -} - -// Message contains the StateDiffComponentDeterminedEvent's statistics message. -func (s StateDiffComponentDeterminedEvent) Message() string { - var amountActions = map[Action]int{} - for _, diff := range s.componentDiffs { - for _, action := range diff.NeededActions { - amountActions[action]++ - } - } - - message, amount := getActionAmountMessage(amountActions) - - return fmt.Sprintf("component state diff determined: %d actions (%s)", amount, message) -} - func getActionAmountMessage(amountActions map[Action]int) (message string, totalAmount int) { var messages []string for action, amount := range amountActions { @@ -190,96 +55,128 @@ func getActionAmountMessage(amountActions map[Action]int) (message string, total return } -// StateDiffDoguDeterminedEvent provides event information over detected changes regarding dogus. -type StateDiffDoguDeterminedEvent struct { - doguDiffs DoguDiffs +// StateDiffDeterminedEvent provides event information over detected changes regarding dogus. +type StateDiffDeterminedEvent struct { + doguDiffs DoguDiffs + GlobalConfigDiffs GlobalConfigDiffs + DoguConfigDiffs map[cescommons.SimpleName]DoguConfigDiffs + SensitiveConfigDiffs map[cescommons.SimpleName]SensitiveDoguConfigDiffs } -func newStateDiffDoguEvent(doguDiffs DoguDiffs) StateDiffDoguDeterminedEvent { - return StateDiffDoguDeterminedEvent{ - doguDiffs: doguDiffs, +func newStateDiffEvent(stateDiff StateDiff) StateDiffDeterminedEvent { + return StateDiffDeterminedEvent{ + doguDiffs: stateDiff.DoguDiffs, + DoguConfigDiffs: stateDiff.DoguConfigDiffs, + GlobalConfigDiffs: stateDiff.GlobalConfigDiffs, + SensitiveConfigDiffs: stateDiff.SensitiveDoguConfigDiffs, } } // Name contains the StateDiffDoguDeterminedEvent display name. -func (s StateDiffDoguDeterminedEvent) Name() string { - return "StateDiffDoguDetermined" +func (s StateDiffDeterminedEvent) Name() string { + return "StateDiffDetermined" } -const groupedDoguProxyAction = "update reverse proxy" - // Message contains the StateDiffDoguDeterminedEvent's statistics message. -func (s StateDiffDoguDeterminedEvent) Message() string { +func (s StateDiffDeterminedEvent) Message() string { var amountActions = map[Action]int{} for _, diff := range s.doguDiffs { for _, action := range diff.NeededActions { - if action.IsDoguProxyAction() { - amountActions[groupedDoguProxyAction]++ - } else { - amountActions[action]++ - } + amountActions[action]++ } } - message, amount := getActionAmountMessage(amountActions) + doguMessage, doguAmount := getActionAmountMessage(amountActions) - return fmt.Sprintf("dogu state diff determined: %d actions (%s)", amount, message) + return fmt.Sprintf("state diff determined:\n %s\n %d dogu actions (%s)", s.generateConfigChangeCounter(), doguAmount, doguMessage) } -type EcosystemHealthyUpfrontEvent struct { - doguHealthIgnored bool - componentHealthIgnored bool -} +func (s StateDiffDeterminedEvent) generateConfigChangeCounter() string { + configActions := util.Map(s.GlobalConfigDiffs, func(entryDiff GlobalConfigEntryDiff) ConfigAction { + return entryDiff.NeededAction + }) + for _, doguDiff := range s.DoguConfigDiffs { + configActions = append(configActions, util.Map(doguDiff, func(entryDiff DoguConfigEntryDiff) ConfigAction { + return entryDiff.NeededAction + })...) + } + for _, doguDiff := range s.SensitiveConfigDiffs { + configActions = append(configActions, util.Map(doguDiff, func(entryDiff SensitiveDoguConfigEntryDiff) ConfigAction { + return entryDiff.NeededAction + })...) + } -func (d EcosystemHealthyUpfrontEvent) Name() string { - return "EcosystemHealthyUpfront" + var stringPerAction []string + var actionsCounter int + for action, amount := range countByAction(configActions) { + stringPerAction = append(stringPerAction, fmt.Sprintf("%q: %d", action, amount)) + if action != ConfigActionNone { + actionsCounter += amount + } + } + slices.Sort(stringPerAction) + return fmt.Sprintf("%d config changes (%s)", actionsCounter, strings.Join(stringPerAction, ", ")) } -func (d EcosystemHealthyUpfrontEvent) Message() string { - return fmt.Sprintf("dogu health ignored: %t; component health ignored: %t", d.doguHealthIgnored, d.componentHealthIgnored) +type EcosystemHealthyEvent struct { + doguHealthIgnored bool } -type EcosystemUnhealthyUpfrontEvent struct { - HealthResult ecosystem.HealthResult +func (d EcosystemHealthyEvent) Name() string { + return "EcosystemHealthy" } -func (d EcosystemUnhealthyUpfrontEvent) Name() string { - return "EcosystemUnhealthyUpfront" +func (d EcosystemHealthyEvent) Message() string { + return fmt.Sprintf("dogu health ignored: %t", d.doguHealthIgnored) } -func (d EcosystemUnhealthyUpfrontEvent) Message() string { - return d.HealthResult.String() +type EcosystemUnhealthyEvent struct { + HealthResult ecosystem.HealthResult } -type BlueprintDryRunEvent struct{} - -func (b BlueprintDryRunEvent) Name() string { - return "BlueprintDryRun" +func (d EcosystemUnhealthyEvent) Name() string { + return "EcosystemUnhealthy" } -func (b BlueprintDryRunEvent) Message() string { - return "Executed blueprint in dry run mode. Remove flag to continue" +func (d EcosystemUnhealthyEvent) Message() string { + return "Ecosystem became unhealthy (up-to-date list is in the EcosystemHealthy condition):\n " + d.HealthResult.String() } -type BlueprintApplicationPreProcessedEvent struct { +type DogusAppliedEvent struct { + Diffs DoguDiffs } -func (e BlueprintApplicationPreProcessedEvent) Name() string { - return "BlueprintApplicationPreProcessed" +func (e DogusAppliedEvent) Name() string { + return "DogusApplied" } -func (e BlueprintApplicationPreProcessedEvent) Message() string { - return "" +func (e DogusAppliedEvent) Message() string { + var buffer bytes.Buffer + buffer.WriteString("dogus applied: ") + var details []string + for _, diff := range e.Diffs { + actionsAsStrings := util.Map(diff.NeededActions, func(action Action) string { + return string(action) + }) + actions := strings.Join(actionsAsStrings, ", ") + details = append(details, fmt.Sprintf("%q: [%v]", diff.DoguName, actions)) + } + buffer.WriteString(strings.Join(details, ", ")) + return buffer.String() } -type InProgressEvent struct{} +type DogusNotUpToDateEvent struct { + DogusNotUpToDate []cescommons.SimpleName +} -func (e InProgressEvent) Name() string { - return "InProgress" +func (e DogusNotUpToDateEvent) Name() string { + return "DogusNotUpToDate" } -func (e InProgressEvent) Message() string { - return "" +func (e DogusNotUpToDateEvent) Message() string { + dogusNotUpToDate := util.Map(e.DogusNotUpToDate, func(dogu cescommons.SimpleName) string { return string(dogu) }) + slices.Sort(dogusNotUpToDate) + return fmt.Sprintf("%d dogu(s) not up to date yet: %s", len(dogusNotUpToDate), strings.Join(dogusNotUpToDate, ", ")) } type BlueprintAppliedEvent struct{} @@ -292,42 +189,24 @@ func (e BlueprintAppliedEvent) Message() string { return "waiting for ecosystem health" } -type EcosystemHealthyAfterwardsEvent struct{} +type BlueprintStoppedEvent struct{} -func (e EcosystemHealthyAfterwardsEvent) Name() string { - return "EcosystemHealthyAfterwards" +func (e BlueprintStoppedEvent) Name() string { + return "BlueprintStopped" } -func (e EcosystemHealthyAfterwardsEvent) Message() string { - return "" -} - -type EcosystemUnhealthyAfterwardsEvent struct { - HealthResult ecosystem.HealthResult -} - -func (e EcosystemUnhealthyAfterwardsEvent) Name() string { - return "EcosystemUnhealthyAfterwards" -} - -func (e EcosystemUnhealthyAfterwardsEvent) Message() string { - return e.HealthResult.String() -} - -type SensitiveConfigDataCensoredEvent struct{} - -func (e SensitiveConfigDataCensoredEvent) Name() string { - return "sensitiveConfigDataCensored" -} - -func (e SensitiveConfigDataCensoredEvent) Message() string { - return "" +func (e BlueprintStoppedEvent) Message() string { + return "Blueprint is set as stopped and will not be applied. Remove flag to continue" } type ExecutionFailedEvent struct { err error } +func NewExecutionFailedEvent(err error) ExecutionFailedEvent { + return ExecutionFailedEvent{err: err} +} + func (e ExecutionFailedEvent) Name() string { return "ExecutionFailed" } @@ -356,18 +235,6 @@ func (e ApplyEcosystemConfigEvent) Message() string { return "apply ecosystem config" } -type ApplyEcosystemConfigFailedEvent struct { - err error -} - -func (e ApplyEcosystemConfigFailedEvent) Name() string { - return "ApplyEcosystemConfigFailed" -} - -func (e ApplyEcosystemConfigFailedEvent) Message() string { - return e.err.Error() -} - type EcosystemConfigAppliedEvent struct{} func (e EcosystemConfigAppliedEvent) Name() string { @@ -377,23 +244,3 @@ func (e EcosystemConfigAppliedEvent) Name() string { func (e EcosystemConfigAppliedEvent) Message() string { return "ecosystem config applied" } - -type AwaitSelfUpgradeEvent struct{} - -func (e AwaitSelfUpgradeEvent) Name() string { - return "AwaitSelfUpgrade" -} - -func (e AwaitSelfUpgradeEvent) Message() string { - return "the operator awaits an upgrade for itself before other changes will be applied" -} - -type SelfUpgradeCompletedEvent struct{} - -func (e SelfUpgradeCompletedEvent) Name() string { - return "SelfUpgradeCompleted" -} - -func (e SelfUpgradeCompletedEvent) Message() string { - return "if a self upgrade was necessary, it was successful" -} diff --git a/pkg/domain/events_test.go b/pkg/domain/events_test.go index 01eaec5f..5c331949 100644 --- a/pkg/domain/events_test.go +++ b/pkg/domain/events_test.go @@ -2,10 +2,11 @@ package domain import ( "fmt" + "testing" + cescommons "github.com/cloudogu/ces-commons-lib/dogu" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" "github.com/stretchr/testify/assert" - "testing" ) func TestEvents(t *testing.T) { @@ -15,51 +16,33 @@ func TestEvents(t *testing.T) { expectedName string expectedMessage string }{ - { - name: "blueprint dry run", - event: BlueprintDryRunEvent{}, - expectedName: "BlueprintDryRun", - expectedMessage: "Executed blueprint in dry run mode. Remove flag to continue", - }, { name: "blueprint spec invalid", event: BlueprintSpecInvalidEvent{ValidationError: assert.AnError}, expectedName: "BlueprintSpecInvalid", expectedMessage: assert.AnError.Error(), }, - { - name: "blueprint spec statically validated", - event: BlueprintSpecStaticallyValidatedEvent{}, - expectedName: "BlueprintSpecStaticallyValidated", - expectedMessage: "", - }, - { - name: "blueprint spec validated", - event: BlueprintSpecValidatedEvent{}, - expectedName: "BlueprintSpecValidated", - expectedMessage: "", - }, { name: "ecosystem healthy", - event: EcosystemHealthyUpfrontEvent{}, - expectedName: "EcosystemHealthyUpfront", - expectedMessage: "dogu health ignored: false; component health ignored: false", + event: EcosystemHealthyEvent{}, + expectedName: "EcosystemHealthy", + expectedMessage: "dogu health ignored: false", }, { name: "ignore dogu health", - event: EcosystemHealthyUpfrontEvent{doguHealthIgnored: true}, - expectedName: "EcosystemHealthyUpfront", - expectedMessage: "dogu health ignored: true; component health ignored: false", + event: EcosystemHealthyEvent{doguHealthIgnored: true}, + expectedName: "EcosystemHealthy", + expectedMessage: "dogu health ignored: true", }, { - name: "ignore component health", - event: EcosystemHealthyUpfrontEvent{componentHealthIgnored: true}, - expectedName: "EcosystemHealthyUpfront", - expectedMessage: "dogu health ignored: false; component health ignored: true", + name: "blueprint stopped", + event: BlueprintStoppedEvent{}, + expectedName: "BlueprintStopped", + expectedMessage: "Blueprint is set as stopped and will not be applied. Remove flag to continue", }, { name: "ecosystem unhealthy upfront", - event: EcosystemUnhealthyUpfrontEvent{ + event: EcosystemUnhealthyEvent{ HealthResult: ecosystem.HealthResult{ DoguHealth: ecosystem.DoguHealthResult{ DogusByStatus: map[ecosystem.HealthStatus][]cescommons.SimpleName{ @@ -70,59 +53,33 @@ func TestEvents(t *testing.T) { }, }, }, - expectedName: "EcosystemUnhealthyUpfront", - expectedMessage: "ecosystem health:\n 2 dogu(s) are unhealthy: admin, ldap\n 0 component(s) are unhealthy: ", - }, - { - name: "effective blueprint calculated", - event: EffectiveBlueprintCalculatedEvent{}, - expectedName: "EffectiveBlueprintCalculated", - expectedMessage: "", + expectedName: "EcosystemUnhealthy", + expectedMessage: "Ecosystem became unhealthy (up-to-date list is in the EcosystemHealthy condition):\n 2 dogu(s) are unhealthy: admin, ldap", }, { name: "dogu state diff determined", - event: newStateDiffDoguEvent( - DoguDiffs{ - {NeededActions: []Action{ActionInstall}}, - {NeededActions: []Action{ActionUninstall}}, - {NeededActions: []Action{ActionInstall}}, - {NeededActions: []Action{ActionUninstall}}, - {NeededActions: []Action{ActionUninstall}}, - {NeededActions: []Action{ActionUpgrade, ActionUpdateDoguResourceMinVolumeSize, ActionUpdateDoguProxyBodySize, ActionUpdateDoguProxyRewriteTarget, ActionUpdateDoguProxyAdditionalConfig}}, - {NeededActions: []Action{ActionDowngrade}}, - }), - expectedName: "StateDiffDoguDetermined", - expectedMessage: "dogu state diff determined: 11 actions (\"downgrade\": 1, \"install\": 2, \"uninstall\": 3, \"update resource minimum volume size\": 1, \"update reverse proxy\": 3, \"upgrade\": 1)", - }, - { - name: "component state diff determined", - event: newStateDiffComponentEvent( - ComponentDiffs{ + event: newStateDiffEvent( + StateDiff{DoguDiffs: DoguDiffs{ {NeededActions: []Action{ActionInstall}}, {NeededActions: []Action{ActionUninstall}}, {NeededActions: []Action{ActionInstall}}, {NeededActions: []Action{ActionUninstall}}, {NeededActions: []Action{ActionUninstall}}, - {NeededActions: []Action{ActionUpgrade, ActionUpdateComponentDeployConfig, ActionSwitchComponentNamespace}}, + {NeededActions: []Action{ActionUpgrade, ActionUpdateDoguResourceMinVolumeSize, ActionUpdateDoguReverseProxyConfig}}, {NeededActions: []Action{ActionDowngrade}}, - }), - expectedName: "StateDiffComponentDetermined", - expectedMessage: "component state diff determined: 9 actions (\"component namespace switch\": 1, \"downgrade\": 1, \"install\": 2, \"uninstall\": 3, \"update component package config\": 1, \"upgrade\": 1)", - }, - { - name: "global config diff determined", - event: GlobalConfigDiffDeterminedEvent{GlobalConfigDiffs: GlobalConfigDiffs{ - {NeededAction: ConfigActionNone}, - {NeededAction: ConfigActionNone}, - {NeededAction: ConfigActionSet}, - {NeededAction: ConfigActionRemove}, - }}, - expectedName: "GlobalConfigDiffDetermined", - expectedMessage: "global config diff determined: 2 changes (\"none\": 2, \"remove\": 1, \"set\": 1)", - }, - { - name: "dogu config diff determined", - event: DoguConfigDiffDeterminedEvent{ + }}), + expectedName: "StateDiffDetermined", + expectedMessage: "state diff determined:\n 0 config changes ()\n 9 dogu actions (\"downgrade\": 1, \"install\": 2, \"uninstall\": 3, \"update resource minimum volume size\": 1, \"update reverse proxy\": 1, \"upgrade\": 1)", + }, + { + name: "config diff determined", + event: newStateDiffEvent(StateDiff{ + GlobalConfigDiffs: GlobalConfigDiffs{ + {NeededAction: ConfigActionNone}, + {NeededAction: ConfigActionNone}, + {NeededAction: ConfigActionSet}, + {NeededAction: ConfigActionRemove}, + }, DoguConfigDiffs: map[cescommons.SimpleName]DoguConfigDiffs{ "dogu1": []DoguConfigEntryDiff{ {NeededAction: ConfigActionNone}, @@ -130,9 +87,31 @@ func TestEvents(t *testing.T) { {NeededAction: ConfigActionRemove}, }, }, - }, - expectedName: "DoguConfigDiffDetermined", - expectedMessage: "dogu config diff determined: 2 changes (\"none\": 1, \"remove\": 1, \"set\": 1)", + SensitiveDoguConfigDiffs: map[cescommons.SimpleName]SensitiveDoguConfigDiffs{ + "dogu1": []SensitiveDoguConfigEntryDiff{ + {NeededAction: ConfigActionNone}, + {NeededAction: ConfigActionSet}, + {NeededAction: ConfigActionRemove}, + }, + }, + }), + expectedName: "StateDiffDetermined", + expectedMessage: "state diff determined:\n 6 config changes (\"none\": 4, \"remove\": 3, \"set\": 3)\n 0 dogu actions ()", + }, + { + name: "config and dogu diff determined", + event: newStateDiffEvent(StateDiff{ + DoguDiffs: DoguDiffs{ + {NeededActions: []Action{ActionInstall}}, + {NeededActions: []Action{ActionUninstall}}, + }, + GlobalConfigDiffs: GlobalConfigDiffs{ + {NeededAction: ConfigActionSet}, + {NeededAction: ConfigActionRemove}, + }, + }), + expectedName: "StateDiffDetermined", + expectedMessage: "state diff determined:\n 2 config changes (\"remove\": 1, \"set\": 1)\n 2 dogu actions (\"install\": 1, \"uninstall\": 1)", }, { name: "config references missing", @@ -143,30 +122,19 @@ func TestEvents(t *testing.T) { expectedMessage: assert.AnError.Error(), }, { - name: "sensitive dogu config diff determined", - event: SensitiveDoguConfigDiffDeterminedEvent{ - SensitiveDoguConfigDiffs: map[cescommons.SimpleName]SensitiveDoguConfigDiffs{ - "dogu1": []SensitiveDoguConfigEntryDiff{ - {NeededAction: ConfigActionNone}, - {NeededAction: ConfigActionSet}, - {NeededAction: ConfigActionRemove}, + name: "dogus applied", + event: DogusAppliedEvent{ + Diffs: DoguDiffs{ + { + DoguName: "jenkins", + NeededActions: []Action{ + ActionUpgrade, ActionSwitchDoguNamespace, + }, }, }, }, - expectedName: "SensitiveDoguConfigDiffDetermined", - expectedMessage: "sensitive dogu config diff determined: 2 changes (\"none\": 1, \"remove\": 1, \"set\": 1)", - }, - { - name: "blueprint application pre-processed", - event: BlueprintApplicationPreProcessedEvent{}, - expectedName: "BlueprintApplicationPreProcessed", - expectedMessage: "", - }, - { - name: "In progress", - event: InProgressEvent{}, - expectedName: "InProgress", - expectedMessage: "", + expectedName: "DogusApplied", + expectedMessage: "dogus applied: \"jenkins\": [upgrade, dogu namespace switch]", }, { name: "blueprint applied", @@ -198,24 +166,6 @@ func TestEvents(t *testing.T) { expectedName: "EcosystemConfigApplied", expectedMessage: "ecosystem config applied", }, - { - name: "applying ecosystem config failed", - event: ApplyEcosystemConfigFailedEvent{fmt.Errorf("test-error")}, - expectedName: "ApplyEcosystemConfigFailed", - expectedMessage: "test-error", - }, - { - name: "await self upgrade", - event: AwaitSelfUpgradeEvent{}, - expectedName: "AwaitSelfUpgrade", - expectedMessage: "the operator awaits an upgrade for itself before other changes will be applied", - }, - { - name: "self upgrade completed", - event: SelfUpgradeCompletedEvent{}, - expectedName: "SelfUpgradeCompleted", - expectedMessage: "if a self upgrade was necessary, it was successful", - }, } for _, tt := range tests { diff --git a/pkg/domain/maskDogu.go b/pkg/domain/maskDogu.go index 2015079a..d1054508 100644 --- a/pkg/domain/maskDogu.go +++ b/pkg/domain/maskDogu.go @@ -1,11 +1,8 @@ package domain import ( - "errors" - "fmt" cescommons "github.com/cloudogu/ces-commons-lib/dogu" "github.com/cloudogu/cesapp-lib/core" - "slices" ) // MaskDogu defines a Dogu, its version, and the installation state in which it is supposed to be after a blueprint @@ -16,17 +13,10 @@ type MaskDogu struct { // Version defines the version of the dogu that is to be installed. This version is optional and overrides // the version of the dogu from the blueprint. Version core.Version - // TargetState defines a state of installation of this dogu. Optional field, but defaults to "TargetStatePresent" - TargetState TargetState + // Absent defines if the dogu should be absent in the ecosystem. Defaults to false. + Absent bool } func (dogu MaskDogu) validate() error { - var errorList []error - errorList = append(errorList, dogu.Name.Validate()) - - if !slices.Contains(PossibleTargetStates, dogu.TargetState) { - errorList = append(errorList, fmt.Errorf("dogu mask is invalid: dogu target state is invalid: %s", dogu.Name)) - } - - return errors.Join(errorList...) + return dogu.Name.Validate() } diff --git a/pkg/domain/maskDogu_test.go b/pkg/domain/maskDogu_test.go index 3591cdd9..ef402d76 100644 --- a/pkg/domain/maskDogu_test.go +++ b/pkg/domain/maskDogu_test.go @@ -1,15 +1,16 @@ package domain import ( + "testing" + cescommons "github.com/cloudogu/ces-commons-lib/dogu" "github.com/cloudogu/cesapp-lib/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "testing" ) func Test_MaskTargetDogu_validate_noErrorOnMissingVersionForPresentDogu(t *testing.T) { - dogu := MaskDogu{Name: officialDogu1, TargetState: TargetStatePresent} + dogu := MaskDogu{Name: officialDogu1, Absent: false} err := dogu.validate() @@ -17,7 +18,7 @@ func Test_MaskTargetDogu_validate_noErrorOnMissingVersionForPresentDogu(t *testi } func Test_MaskTargetDogu_validate_missingVersionOkayForAbsentDogu(t *testing.T) { - dogu := MaskDogu{Name: officialDogu1, TargetState: TargetStateAbsent} + dogu := MaskDogu{Name: officialDogu1, Absent: true} err := dogu.validate() @@ -31,7 +32,7 @@ func Test_MaskTargetDogu_validate_defaultToPresentState(t *testing.T) { err := dogu.validate() require.Nil(t, err) - assert.Equal(t, TargetState(TargetStatePresent), dogu.TargetState) + assert.False(t, dogu.Absent) } func Test_MaskTargetDogu_validate_errorOnMissingNameForDogu(t *testing.T) { @@ -42,12 +43,3 @@ func Test_MaskTargetDogu_validate_errorOnMissingNameForDogu(t *testing.T) { require.Error(t, err) require.ErrorContains(t, err, "dogu name must not be empty") } - -func Test_MaskTargetDogu_validate_errorOnUnknownTargetState(t *testing.T) { - dogu := MaskDogu{Name: officialDogu1, TargetState: -1} - - err := dogu.validate() - - require.Error(t, err) - require.ErrorContains(t, err, "dogu target state is invalid: official/dogu1") -} diff --git a/pkg/domain/stateDiff.go b/pkg/domain/stateDiff.go index 19019870..a7e99810 100644 --- a/pkg/domain/stateDiff.go +++ b/pkg/domain/stateDiff.go @@ -8,7 +8,6 @@ import ( // If there is a state in the ecosystem, which is not represented in the effective blueprint, then the expected state is the actual state. type StateDiff struct { DoguDiffs DoguDiffs - ComponentDiffs ComponentDiffs DoguConfigDiffs map[cescommons.SimpleName]DoguConfigDiffs SensitiveDoguConfigDiffs map[cescommons.SimpleName]SensitiveDoguConfigDiffs GlobalConfigDiffs GlobalConfigDiffs @@ -23,15 +22,36 @@ const ( ActionUpgrade = "upgrade" ActionDowngrade = "downgrade" ActionSwitchDoguNamespace = "dogu namespace switch" - ActionUpdateDoguProxyBodySize = "update proxy body size" - ActionUpdateDoguProxyRewriteTarget = "update proxy rewrite target" - ActionUpdateDoguProxyAdditionalConfig = "update proxy additional config" + ActionUpdateDoguReverseProxyConfig = "update reverse proxy" ActionUpdateDoguResourceMinVolumeSize = "update resource minimum volume size" - ActionSwitchComponentNamespace = "component namespace switch" - ActionUpdateComponentDeployConfig = "update component package config" ActionUpdateAdditionalMounts = "update additional mounts" ) -func (a Action) IsDoguProxyAction() bool { - return a == ActionUpdateDoguProxyBodySize || a == ActionUpdateDoguProxyAdditionalConfig || a == ActionUpdateDoguProxyRewriteTarget +func (diff StateDiff) HasChanges() bool { + return diff.DoguDiffs.HasChanges() || + diff.HasConfigChanges() +} + +func (diff StateDiff) HasConfigChanges() bool { + return diff.GlobalConfigDiffs.HasChanges() || + diff.HasDoguConfigChanges() || + diff.HasSensitiveDoguConfigChanges() +} + +func (diff StateDiff) HasDoguConfigChanges() bool { + for _, configDiff := range diff.DoguConfigDiffs { + if configDiff.HasChanges() { + return true + } + } + return false +} + +func (diff StateDiff) HasSensitiveDoguConfigChanges() bool { + for _, configDiff := range diff.SensitiveDoguConfigDiffs { + if configDiff.HasChanges() { + return true + } + } + return false } diff --git a/pkg/domain/stateDiffComponent.go b/pkg/domain/stateDiffComponent.go deleted file mode 100644 index 822c5303..00000000 --- a/pkg/domain/stateDiffComponent.go +++ /dev/null @@ -1,234 +0,0 @@ -package domain - -import ( - "fmt" - "github.com/Masterminds/semver/v3" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/common" - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" - "golang.org/x/exp/maps" - "reflect" -) - -// ComponentDiffs contains the differences for all expected Components to the current ecosystem.ComponentInstallations. -type ComponentDiffs []ComponentDiff - -// GetComponentDiffByName returns the diff for the given component name or an empty struct if it was not found. -func (diffs ComponentDiffs) GetComponentDiffByName(name common.SimpleComponentName) ComponentDiff { - for _, diff := range diffs { - if diff.Name == name { - return diff - } - } - return ComponentDiff{} -} - -// ComponentDiff represents the Diff for a single expected Component to the current ecosystem.ComponentInstallation. -type ComponentDiff struct { - // Name contains the component's name. - Name common.SimpleComponentName - // Actual contains that state of a component how it is currently found in the system. - Actual ComponentDiffState - // Expected contains that desired state of a component how it is supposed to be. - Expected ComponentDiffState - // NeededActions hints how the component should be handled by the application change automaton in order to reconcile - // differences between Actual and Expected in the current system. - NeededActions []Action -} - -// ComponentDiffState contains all fields to make a diff for components in respect to another ComponentDiffState. -type ComponentDiffState struct { - // Namespace is part of the address under which the component will be obtained. This namespace must NOT - // to be confused with the K8s cluster namespace. - Namespace common.ComponentNamespace - // Version contains the component's version. - Version *semver.Version - // InstallationState contains the component's target state. - InstallationState TargetState - // DeployConfig contains generic properties for the component. - DeployConfig ecosystem.DeployConfig -} - -// IsExpectedVersion checks if the given version es equal to the expected version -func (diff ComponentDiff) IsExpectedVersion(actualVersion *semver.Version) bool { - // expected is nil if the component is not in the blueprint, therefore no upgrade needs to happen - if diff.Expected.Version == nil { - return true - } - // actualVersion is nil if there is no component or no actual version in it yet. - if actualVersion == nil { - return false - } - return diff.Expected.Version.Equal(actualVersion) -} - -func (diff ComponentDiff) HasChanges() bool { - return len(diff.NeededActions) != 0 -} - -// String returns a string representation of the ComponentDiff. -func (diff *ComponentDiff) String() string { - return fmt.Sprintf( - "{Name: %q, Actual: %s, Expected: %s, NeededActions: %q}", - diff.Name, - diff.Actual.String(), - diff.Expected.String(), - diff.NeededActions, - ) -} - -// String returns a string representation of the ComponentDiffState. -func (diff *ComponentDiffState) String() string { - return fmt.Sprintf( - "{Namespace: %q, Version: %q, InstallationState: %q}", - diff.Namespace, - diff.getSafeVersionString(), - diff.InstallationState, - ) -} - -func (diff *ComponentDiffState) getSafeVersionString() string { - if diff.Version != nil { - return diff.Version.String() - } else { - return "" - } -} - -// determineComponentDiffs creates ComponentDiffs for all components in the blueprint and all installed components as well. -func determineComponentDiffs(blueprintComponents []Component, installedComponents map[common.SimpleComponentName]*ecosystem.ComponentInstallation) ([]ComponentDiff, error) { - var componentDiffs = map[common.SimpleComponentName]ComponentDiff{} - for _, blueprintComponent := range blueprintComponents { - installedComponent := installedComponents[blueprintComponent.Name.SimpleName] - compDiff, err := determineComponentDiff(&blueprintComponent, installedComponent) - if err != nil { - return nil, err - } - componentDiffs[blueprintComponent.Name.SimpleName] = compDiff - } - - for _, installedComponent := range installedComponents { - _, found := findComponentByName(blueprintComponents, installedComponent.Name.SimpleName) - // Only create ComponentDiff if the installed component is not found in the blueprint. - // If the installed component is in blueprint the ComponentDiff was already determined above. - if !found { - compDiff, err := determineComponentDiff(nil, installedComponent) - if err != nil { - return nil, err - } - componentDiffs[installedComponent.Name.SimpleName] = compDiff - } - } - return maps.Values(componentDiffs), nil -} - -// determineComponentDiff creates a ComponentDiff out of a Component from the blueprint and the ecosystem.ComponentInstallation in the ecosystem. -// If the Component is nil (was not in the blueprint), the actual state is also the expected state. -// If the installedComponent is nil, it is considered to be not installed currently. -func determineComponentDiff(blueprintComponent *Component, installedComponent *ecosystem.ComponentInstallation) (ComponentDiff, error) { - var expectedState, actualState ComponentDiffState - componentName := common.SimpleComponentName("") // either blueprintComponent or installedComponent could be nil - - if installedComponent == nil { - actualState = ComponentDiffState{ - InstallationState: TargetStateAbsent, - } - } else { - componentName = installedComponent.Name.SimpleName - actualState = ComponentDiffState{ - Namespace: installedComponent.Name.Namespace, - Version: installedComponent.ExpectedVersion, - InstallationState: TargetStatePresent, - DeployConfig: installedComponent.DeployConfig, - } - } - - if blueprintComponent == nil { - expectedState = actualState - } else { - componentName = blueprintComponent.Name.SimpleName - expectedState = ComponentDiffState{ - Namespace: blueprintComponent.Name.Namespace, - Version: blueprintComponent.Version, - InstallationState: blueprintComponent.TargetState, - DeployConfig: blueprintComponent.DeployConfig, - } - } - - nextActions, err := getComponentActions(expectedState, actualState) - if err != nil { - return ComponentDiff{}, fmt.Errorf("failed to determine diff for component %q : %w", componentName, err) - } - - return ComponentDiff{ - Name: componentName, - Expected: expectedState, - Actual: actualState, - NeededActions: nextActions, - }, nil -} - -func findComponentByName(components []Component, name common.SimpleComponentName) (Component, bool) { - for _, component := range components { - if component.Name.SimpleName == name { - return component, true - } - } - return Component{}, false -} - -func getComponentActions(expected ComponentDiffState, actual ComponentDiffState) ([]Action, error) { - if expected.InstallationState == actual.InstallationState { - return decideOnEqualState(expected, actual) - } - - return decideOnDifferentState(expected) -} - -func decideOnEqualState(expected ComponentDiffState, actual ComponentDiffState) ([]Action, error) { - var neededActions []Action - - switch expected.InstallationState { - case TargetStatePresent: - return getActionsForEqualPresentState(expected, actual), nil - case TargetStateAbsent: - return neededActions, nil - default: - return nil, fmt.Errorf("component has unexpected target state %q", expected.InstallationState) - } -} - -func getActionsForEqualPresentState(expected ComponentDiffState, actual ComponentDiffState) []Action { - var neededActions []Action - - if expected.Namespace != actual.Namespace { - neededActions = append(neededActions, ActionSwitchComponentNamespace) - } - - if !reflect.DeepEqual(expected.DeployConfig, actual.DeployConfig) { - // Do update only if any DeployConfig contains data. - // A nil DeployConfig and an empty DeployConfig are not deeply equal. But in this case we do not want to update the DeployConfig. - if len(expected.DeployConfig) != 0 || len(actual.DeployConfig) != 0 { - neededActions = append(neededActions, ActionUpdateComponentDeployConfig) - } - } - - if expected.Version.GreaterThan(actual.Version) { - neededActions = append(neededActions, ActionUpgrade) - } else if actual.Version.GreaterThan(expected.Version) { - neededActions = append(neededActions, ActionDowngrade) - } - - return neededActions -} - -func decideOnDifferentState(expected ComponentDiffState) ([]Action, error) { - // at this place, the actual state is always the opposite to the expected state so just follow the expected state. - switch expected.InstallationState { - case TargetStatePresent: - return []Action{ActionInstall}, nil - case TargetStateAbsent: - return []Action{ActionUninstall}, nil - default: - return nil, fmt.Errorf("component has unexpected installation state %q", expected.InstallationState) - } -} diff --git a/pkg/domain/stateDiffComponent_test.go b/pkg/domain/stateDiffComponent_test.go deleted file mode 100644 index 9188b06d..00000000 --- a/pkg/domain/stateDiffComponent_test.go +++ /dev/null @@ -1,503 +0,0 @@ -package domain - -import ( - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/common" - "testing" - - "github.com/go-logr/logr" - "github.com/stretchr/testify/assert" - - "github.com/Masterminds/semver/v3" - - "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" -) - -var ( - testComponentName = common.QualifiedComponentName{ - Namespace: "k8s", - SimpleName: "my-component", - } - blueprintOperatorSimpleName = common.SimpleComponentName("k8s-blueprint-operator") -) - -var ( - compVersion3211 = semver.MustParse("3.2.1-1") -) - -func Test_determineComponentDiff(t *testing.T) { - type args struct { - logger logr.Logger - blueprintComponent *Component - installedComponent *ecosystem.ComponentInstallation - } - tests := []struct { - name string - args args - want ComponentDiff - }{ - { - name: "equal, no action", - args: args{ - blueprintComponent: mockTargetComponent(compVersion3211, TargetStatePresent, nil), - installedComponent: mockComponentInstallation(compVersion3211), - }, - want: ComponentDiff{ - Name: testComponentName.SimpleName, - Actual: mockComponentDiffState(testDistributionNamespace, compVersion3211, TargetStatePresent, nil), - Expected: mockComponentDiffState(testDistributionNamespace, compVersion3211, TargetStatePresent, nil), - NeededActions: nil, - }, - }, - { - name: "install", - args: args{ - blueprintComponent: mockTargetComponent(compVersion3211, TargetStatePresent, nil), - installedComponent: nil, - }, - want: ComponentDiff{ - Name: testComponentName.SimpleName, - Actual: mockComponentDiffState("", nil, TargetStateAbsent, nil), - Expected: mockComponentDiffState(testDistributionNamespace, compVersion3211, TargetStatePresent, nil), - NeededActions: []Action{ActionInstall}, - }, - }, - { - name: "uninstall", - args: args{ - blueprintComponent: mockTargetComponent(nil, TargetStateAbsent, nil), - installedComponent: mockComponentInstallation(compVersion3211), - }, - want: ComponentDiff{ - Name: testComponentName.SimpleName, - Actual: mockComponentDiffState(testDistributionNamespace, compVersion3211, TargetStatePresent, nil), - Expected: mockComponentDiffState(testDistributionNamespace, nil, TargetStateAbsent, nil), - NeededActions: []Action{ActionUninstall}, - }, - }, - { - name: "upgrade", - args: args{ - blueprintComponent: mockTargetComponent(compVersion3212, TargetStatePresent, nil), - installedComponent: mockComponentInstallation(compVersion3211), - }, - want: ComponentDiff{ - Name: testComponentName.SimpleName, - Actual: mockComponentDiffState(testDistributionNamespace, compVersion3211, TargetStatePresent, nil), - Expected: mockComponentDiffState(testDistributionNamespace, compVersion3212, TargetStatePresent, nil), - NeededActions: []Action{ActionUpgrade}, - }, - }, - { - name: "update package config", - args: args{ - blueprintComponent: mockTargetComponent(compVersion3211, TargetStatePresent, map[string]interface{}{"deployNamespace": "k8s-longhorn"}), - installedComponent: mockComponentInstallation(compVersion3211), - }, - want: ComponentDiff{ - Name: testComponentName.SimpleName, - Actual: mockComponentDiffState(testDistributionNamespace, compVersion3211, TargetStatePresent, nil), - Expected: mockComponentDiffState(testDistributionNamespace, compVersion3211, TargetStatePresent, map[string]interface{}{"deployNamespace": "k8s-longhorn"}), - NeededActions: []Action{ActionUpdateComponentDeployConfig}, - }, - }, - { - name: "update package config and upgrade", - args: args{ - blueprintComponent: mockTargetComponent(compVersion3212, TargetStatePresent, map[string]interface{}{"deployNamespace": "k8s-longhorn"}), - installedComponent: mockComponentInstallation(compVersion3211), - }, - want: ComponentDiff{ - Name: testComponentName.SimpleName, - Actual: mockComponentDiffState(testDistributionNamespace, compVersion3211, TargetStatePresent, nil), - Expected: mockComponentDiffState(testDistributionNamespace, compVersion3212, TargetStatePresent, map[string]interface{}{"deployNamespace": "k8s-longhorn"}), - NeededActions: []Action{ActionUpdateComponentDeployConfig, ActionUpgrade}, - }, - }, - { - name: "downgrade", - args: args{ - blueprintComponent: mockTargetComponent(compVersion3211, TargetStatePresent, nil), - installedComponent: mockComponentInstallation(compVersion3212), - }, - want: ComponentDiff{ - Name: testComponentName.SimpleName, - Actual: mockComponentDiffState(testDistributionNamespace, compVersion3212, TargetStatePresent, nil), - Expected: mockComponentDiffState(testDistributionNamespace, compVersion3211, TargetStatePresent, nil), - NeededActions: []Action{ActionDowngrade}, - }, - }, - { - name: "ignore present component, no action", - args: args{ - blueprintComponent: nil, - installedComponent: mockComponentInstallation(compVersion3211), - }, - want: ComponentDiff{ - Name: testComponentName.SimpleName, - Actual: mockComponentDiffState(testDistributionNamespace, compVersion3211, TargetStatePresent, nil), - Expected: mockComponentDiffState(testDistributionNamespace, compVersion3211, TargetStatePresent, nil), - NeededActions: nil, - }, - }, - { - name: "should stay absent, no action", // this is empty set comparison is weird and should basically not occur - args: args{ - blueprintComponent: nil, - installedComponent: nil, - }, - want: ComponentDiff{ - Name: "", - Actual: ComponentDiffState{InstallationState: TargetStateAbsent}, - Expected: ComponentDiffState{InstallationState: TargetStateAbsent}, - NeededActions: nil, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - compDiff, err := determineComponentDiff(tt.args.blueprintComponent, tt.args.installedComponent) - assert.NoError(t, err) - assert.Equalf(t, tt.want, compDiff, "determineComponentDiff(%v, %v, %v)", tt.args.logger, tt.args.blueprintComponent, tt.args.installedComponent) - }) - } -} - -func TestComponentDiff_String(t *testing.T) { - actual := ComponentDiffState{ - Version: compVersion3211, - InstallationState: TargetStatePresent, - } - expected := ComponentDiffState{ - Version: compVersion3212, - InstallationState: TargetStatePresent, - } - diff := &ComponentDiff{ - Name: testComponentName.SimpleName, - Actual: actual, - Expected: expected, - NeededActions: []Action{ActionInstall}, - } - - assert.Equal(t, "{"+ - "Name: \"my-component\", "+ - "Actual: {Namespace: \"\", Version: \"3.2.1-1\", InstallationState: \"present\"}, "+ - "Expected: {Namespace: \"\", Version: \"3.2.1-2\", InstallationState: \"present\"}, "+ - "NeededActions: [\"install\"]"+ - "}", diff.String()) -} - -func TestComponentDiffState_String(t *testing.T) { - diff := &ComponentDiffState{ - Namespace: "k8s", - Version: compVersion3211, - InstallationState: TargetStatePresent, - } - - assert.Equal(t, `{Namespace: "k8s", Version: "3.2.1-1", InstallationState: "present"}`, diff.String()) -} - -func mockTargetComponent(version *semver.Version, state TargetState, deployConfig ecosystem.DeployConfig) *Component { - return &Component{ - Name: testComponentName, - Version: version, - TargetState: state, - DeployConfig: deployConfig, - } -} - -func mockComponentInstallation(version *semver.Version) *ecosystem.ComponentInstallation { - return &ecosystem.ComponentInstallation{ - Name: testComponentName, - ExpectedVersion: version, - } -} - -func mockComponentDiffState(namespace common.ComponentNamespace, version *semver.Version, state TargetState, deployConfig ecosystem.DeployConfig) ComponentDiffState { - return ComponentDiffState{ - Namespace: namespace, - Version: version, - InstallationState: state, - DeployConfig: deployConfig, - } -} - -func Test_determineComponentDiffs(t *testing.T) { - type args struct { - logger logr.Logger - blueprintComponents []Component - installedComponents map[common.SimpleComponentName]*ecosystem.ComponentInstallation - } - tests := []struct { - name string - args args - want []ComponentDiff - }{ - { - name: "no components", - args: args{ - blueprintComponents: nil, - installedComponents: nil, - }, - want: []ComponentDiff{}, - }, - { - name: "a not installed component in the blueprint", - args: args{ - blueprintComponents: []Component{ - { - Name: testComponentName, - Version: compVersion3211, - TargetState: TargetStatePresent, - }, - }, - installedComponents: nil, - }, - want: []ComponentDiff{ - { - Name: testComponentName.SimpleName, - Actual: ComponentDiffState{ - InstallationState: TargetStateAbsent, - }, - Expected: ComponentDiffState{ - Namespace: testComponentName.Namespace, - Version: compVersion3211, - InstallationState: TargetStatePresent, - }, - NeededActions: []Action{ActionInstall}, - }, - }, - }, - { - name: "an installed component which is not in the blueprint", - args: args{ - blueprintComponents: nil, - installedComponents: map[common.SimpleComponentName]*ecosystem.ComponentInstallation{ - testComponentName.SimpleName: { - Name: testComponentName, - ExpectedVersion: compVersion3211, - }, - }, - }, - want: []ComponentDiff{ - { - Name: testComponentName.SimpleName, - Actual: ComponentDiffState{ - Namespace: testComponentName.Namespace, - Version: compVersion3211, - InstallationState: TargetStatePresent, - }, - Expected: ComponentDiffState{ - Namespace: testComponentName.Namespace, - Version: compVersion3211, - InstallationState: TargetStatePresent, - }, - NeededActions: nil, - }, - }, - }, - { - name: "determine distribution namespace switch", - args: args{ - blueprintComponents: []Component{ - { - Name: common.QualifiedComponentName{Namespace: "k8s-testing", SimpleName: "my-component"}, - Version: compVersion3211, - TargetState: TargetStatePresent, - }, - }, - installedComponents: map[common.SimpleComponentName]*ecosystem.ComponentInstallation{ - testComponentName.SimpleName: { - Name: common.QualifiedComponentName{Namespace: "k8s", SimpleName: "my-component"}, - ExpectedVersion: compVersion3211, - }, - }, - }, - want: []ComponentDiff{ - { - Name: testComponentName.SimpleName, - Actual: ComponentDiffState{ - Version: compVersion3211, - InstallationState: TargetStatePresent, - Namespace: testDistributionNamespace, - }, - Expected: ComponentDiffState{ - Version: compVersion3211, - InstallationState: TargetStatePresent, - Namespace: testChangeDistributionNamespace, - }, - NeededActions: []Action{ActionSwitchComponentNamespace}, - }, - }, - }, - { - name: "determine upgrade for an installed component which is also in the blueprint", - args: args{ - blueprintComponents: []Component{ - { - Name: testComponentName, - Version: compVersion3212, - TargetState: TargetStatePresent, - }, - }, - installedComponents: map[common.SimpleComponentName]*ecosystem.ComponentInstallation{ - testComponentName.SimpleName: { - Name: testComponentName, - ExpectedVersion: compVersion3211, - }, - }, - }, - want: []ComponentDiff{ - { - Name: testComponentName.SimpleName, - Actual: ComponentDiffState{ - Version: compVersion3211, - Namespace: testComponentName.Namespace, - InstallationState: TargetStatePresent, - }, - Expected: ComponentDiffState{ - Version: compVersion3212, - Namespace: testComponentName.Namespace, - InstallationState: TargetStatePresent, - }, - NeededActions: []Action{ActionUpgrade}, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - compDiffs, err := determineComponentDiffs(tt.args.blueprintComponents, tt.args.installedComponents) - assert.NoError(t, err) - assert.Equalf(t, tt.want, compDiffs, "determineComponentDiffs(%v, %v, %v)", tt.args.logger, tt.args.blueprintComponents, tt.args.installedComponents) - }) - } -} - -func TestComponentDiffState_getSafeVersionString(t *testing.T) { - version1, _ := semver.NewVersion("1.0.0") - - type fields struct { - Namespace common.ComponentNamespace - Version *semver.Version - InstallationState TargetState - } - tests := []struct { - name string - fields fields - want string - }{ - { - name: "success", - fields: fields{Version: version1}, - want: "1.0.0", - }, - { - name: "should return empty string and no panic on nil version", - fields: fields{Version: nil}, - want: "", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - diff := &ComponentDiffState{ - Namespace: tt.fields.Namespace, - Version: tt.fields.Version, - InstallationState: tt.fields.InstallationState, - } - assert.Equalf(t, tt.want, diff.getSafeVersionString(), "getSafeVersionString()") - }) - } -} - -func TestComponentDiffs_GetComponentDiffByName(t *testing.T) { - t.Run("find diff", func(t *testing.T) { - blueprintOpDiff := ComponentDiff{ - Name: blueprintOperatorSimpleName, - Actual: ComponentDiffState{}, - Expected: ComponentDiffState{}, - NeededActions: []Action{ActionUninstall}, - } - diffs := ComponentDiffs{ - blueprintOpDiff, - ComponentDiff{ - Name: testComponentName.SimpleName, - Actual: ComponentDiffState{}, - Expected: ComponentDiffState{}, - NeededActions: []Action{ActionUninstall}, - }, - } - - foundDiff := diffs.GetComponentDiffByName(blueprintOperatorSimpleName) - - assert.Equal(t, blueprintOpDiff, foundDiff) - }) - - t.Run("don't find diff", func(t *testing.T) { - diffs := ComponentDiffs{} - - foundDiff := diffs.GetComponentDiffByName(blueprintOperatorSimpleName) - - assert.Equal(t, ComponentDiff{}, foundDiff) - }) -} - -func TestComponentDiff_HasChanges(t *testing.T) { - t.Run("no change", func(t *testing.T) { - diff := ComponentDiff{ - NeededActions: []Action{}, - } - assert.False(t, diff.HasChanges()) - }) - t.Run("change for any", func(t *testing.T) { - diff := ComponentDiff{ - NeededActions: []Action{ActionInstall}, - } - assert.True(t, diff.HasChanges()) - }) -} - -func TestComponentDiff_IsExpectedVersion(t *testing.T) { - tests := []struct { - name string - expected *semver.Version - actual *semver.Version - want bool - }{ - { - name: "equal", - expected: semver.MustParse("1.0"), - actual: semver.MustParse("1.0"), - want: true, - }, - { - name: "equal dev versions", - expected: semver.MustParse("0.2.0-dev"), - actual: semver.MustParse("0.2.0-dev"), - want: true, - }, - { - name: "higher expected", - expected: semver.MustParse("1.1"), - actual: semver.MustParse("1.0"), - want: false, - }, - { - name: "nothing expected", - expected: nil, - actual: semver.MustParse("1.0"), - want: true, - }, - { - name: "nothing installed", - expected: semver.MustParse("1.0"), - actual: nil, - want: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - diff := ComponentDiff{Expected: ComponentDiffState{Version: tt.expected}} - assert.Equalf(t, tt.want, diff.IsExpectedVersion(tt.actual), "{version:%v}.IsExpectedVersion(%v)", tt.expected, tt.actual) - }) - } -} diff --git a/pkg/domain/stateDiffConfig.go b/pkg/domain/stateDiffConfig.go index 8ce1083c..a84a6468 100644 --- a/pkg/domain/stateDiffConfig.go +++ b/pkg/domain/stateDiffConfig.go @@ -4,7 +4,6 @@ import ( cescommons "github.com/cloudogu/ces-commons-lib/dogu" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/common" "github.com/cloudogu/k8s-registry-lib/config" - "maps" ) type ConfigAction string @@ -18,23 +17,12 @@ const ( ConfigActionRemove ConfigAction = "remove" ) -// censorValues censors all sensitive configuration data to make them unrecognisable. -func censorValues(sensitiveConfigByDogu map[cescommons.SimpleName]SensitiveDoguConfigDiffs) map[cescommons.SimpleName]SensitiveDoguConfigDiffs { - censoredByDogu := maps.Clone(sensitiveConfigByDogu) - for dogu, entryDiffs := range sensitiveConfigByDogu { - censoredByDogu[dogu] = entryDiffs.CensorValues() +func countByAction(configActions []ConfigAction) map[ConfigAction]int { + result := map[ConfigAction]int{} + for _, action := range configActions { + result[action]++ } - return censoredByDogu -} - -func countByAction(diffsByDogu map[cescommons.SimpleName]DoguConfigDiffs) map[ConfigAction]int { - countByAction := map[ConfigAction]int{} - for _, doguDiffs := range diffsByDogu { - for _, diff := range doguDiffs { - countByAction[diff.NeededAction]++ - } - } - return countByAction + return result } func determineConfigDiffs( @@ -42,7 +30,7 @@ func determineConfigDiffs( globalConfig config.GlobalConfig, configByDogu map[cescommons.SimpleName]config.DoguConfig, SensitiveConfigByDogu map[cescommons.SimpleName]config.DoguConfig, - referencedSensitiveConfig map[common.SensitiveDoguConfigKey]common.SensitiveDoguConfigValue, + referencedSensitiveConfig map[common.DoguConfigKey]common.SensitiveDoguConfigValue, ) ( map[cescommons.SimpleName]DoguConfigDiffs, map[cescommons.SimpleName]SensitiveDoguConfigDiffs, @@ -54,49 +42,60 @@ func determineConfigDiffs( } func determineDogusConfigDiffs( - combinedDoguConfigs map[cescommons.SimpleName]CombinedDoguConfig, + blueprintDoguConfigs map[cescommons.SimpleName]DoguConfigEntries, configByDogu map[cescommons.SimpleName]config.DoguConfig, ) map[cescommons.SimpleName]DoguConfigDiffs { - diffsPerDogu := map[cescommons.SimpleName]DoguConfigDiffs{} - for doguName, combinedDoguConfig := range combinedDoguConfigs { - diffsPerDogu[doguName] = determineDoguConfigDiffs(combinedDoguConfig.Config, configByDogu) + var diffsPerDogu map[cescommons.SimpleName]DoguConfigDiffs + for doguName, bluprintDoguConfig := range blueprintDoguConfigs { + configDiffs := determineDoguConfigDiffs(doguName, bluprintDoguConfig, configByDogu, false) + if len(configDiffs) > 0 { + if diffsPerDogu == nil { + diffsPerDogu = make(map[cescommons.SimpleName]DoguConfigDiffs) + } + diffsPerDogu[doguName] = configDiffs + } } return diffsPerDogu } func determineSensitiveDogusConfigDiffs( - combinedDoguConfigs map[cescommons.SimpleName]CombinedDoguConfig, + blueprintDoguConfigs map[cescommons.SimpleName]DoguConfigEntries, configByDogu map[cescommons.SimpleName]config.DoguConfig, referencedValues map[common.DoguConfigKey]common.SensitiveDoguConfigValue, ) map[cescommons.SimpleName]DoguConfigDiffs { - diffsPerDogu := map[cescommons.SimpleName]DoguConfigDiffs{} - for doguName, combinedDoguConfig := range combinedDoguConfigs { - expectedSensitiveConfig := createDoguConfigFromReferencedValues(combinedDoguConfig.SensitiveConfig, referencedValues) - diffsPerDogu[doguName] = determineDoguConfigDiffs(expectedSensitiveConfig, configByDogu) + var diffsPerDogu map[cescommons.SimpleName]DoguConfigDiffs + for doguName, blueprintDoguConfig := range blueprintDoguConfigs { + setSensitiveConfigValues(doguName, blueprintDoguConfig, referencedValues) + configDiffs := determineDoguConfigDiffs(doguName, blueprintDoguConfig, configByDogu, true) + if len(configDiffs) > 0 { + if diffsPerDogu == nil { + diffsPerDogu = make(map[cescommons.SimpleName]DoguConfigDiffs) + } + diffsPerDogu[doguName] = configDiffs + } } return diffsPerDogu } -// createDoguConfigFromReferencedValues maps the referenced values to normal dogu config, +// setSensitiveConfigValues maps the referenced values to normal dogu config, // so that the stateDiff can handle sensitive dogu config the same way as normal dogu config -func createDoguConfigFromReferencedValues( - sensitiveConfig SensitiveDoguConfig, - referencedValues map[common.DoguConfigKey]common.SensitiveDoguConfigValue, -) DoguConfig { - configEntries := map[common.DoguConfigKey]common.DoguConfigValue{} - for key := range sensitiveConfig.Present { - // we checked previously that all referenced values exist. Therefore, we need no error here. - // in case of a bug, this will cause, that no expected value gets set while applying the blueprint. - configEntries[key] = referencedValues[key] - } - return DoguConfig{ - Present: configEntries, - Absent: sensitiveConfig.Absent, +func setSensitiveConfigValues(doguName cescommons.SimpleName, configEntries DoguConfigEntries, referencedValues map[common.DoguConfigKey]common.SensitiveDoguConfigValue) { + for i, entry := range configEntries { + if !entry.Absent && entry.Sensitive { + key := common.DoguConfigKey{ + DoguName: doguName, + Key: entry.Key, + } + // we checked previously that all referenced values exist. Therefore, we need no error here. + // in case of a bug, this will cause, that no expected value gets set while applying the blueprint. + sensitiveValue := referencedValues[key] + configEntries[i].Value = &sensitiveValue + } } } func getNeededConfigAction(expected ConfigValueState, actual ConfigValueState) ConfigAction { - if expected == actual { + if expected.Equal(actual) { return ConfigActionNone } if !expected.Exists { diff --git a/pkg/domain/stateDiffConfig_test.go b/pkg/domain/stateDiffConfig_test.go index 547e9458..b9045123 100644 --- a/pkg/domain/stateDiffConfig_test.go +++ b/pkg/domain/stateDiffConfig_test.go @@ -1,24 +1,26 @@ package domain import ( + "testing" + cescommons "github.com/cloudogu/ces-commons-lib/dogu" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/common" "github.com/cloudogu/k8s-registry-lib/config" + "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "testing" ) var ( - dogu1 = cescommons.SimpleName("dogu1") - dogu2 = cescommons.SimpleName("dogu2") - dogu1Key1 = common.DoguConfigKey{DoguName: dogu1, Key: "key1"} - dogu1Key2 = common.DoguConfigKey{DoguName: dogu1, Key: "key2"} - dogu1Key3 = common.DoguConfigKey{DoguName: dogu1, Key: "key3"} - dogu1Key4 = common.DoguConfigKey{DoguName: dogu1, Key: "key4"} - sensitiveDogu1Key1 = common.SensitiveDoguConfigKey{DoguName: dogu1, Key: "key1"} - sensitiveDogu1Key2 = common.SensitiveDoguConfigKey{DoguName: dogu1, Key: "key2"} - sensitiveDogu1Key3 = common.SensitiveDoguConfigKey{DoguName: dogu1, Key: "key3"} + dogu1 = cescommons.SimpleName("dogu1") + dogu2 = cescommons.SimpleName("dogu2") + dogu1Key1 = common.DoguConfigKey{DoguName: dogu1, Key: "key1"} + dogu1Key2 = common.DoguConfigKey{DoguName: dogu1, Key: "key2"} + dogu1Key3 = common.DoguConfigKey{DoguName: dogu1, Key: "key3"} + dogu1Key4 = common.DoguConfigKey{DoguName: dogu1, Key: "key4"} + val1 = config.Value("value1") + val2 = config.Value("value2") + val3 = config.Value("value3") ) func Test_determineConfigDiff(t *testing.T) { @@ -30,32 +32,41 @@ func Test_determineConfigDiff(t *testing.T) { config.CreateGlobalConfig(map[config.Key]config.Value{}), map[cescommons.SimpleName]config.DoguConfig{}, map[cescommons.SimpleName]config.DoguConfig{}, - map[common.SensitiveDoguConfigKey]common.SensitiveDoguConfigValue{}, + map[common.DoguConfigKey]common.SensitiveDoguConfigValue{}, ) - assert.Equal(t, map[cescommons.SimpleName]DoguConfigDiffs{}, dogusConfigDiffs) - assert.Equal(t, map[cescommons.SimpleName]SensitiveDoguConfigDiffs{}, sensitiveConfigDiffs) + assert.Nil(t, dogusConfigDiffs) + assert.Nil(t, sensitiveConfigDiffs) assert.Equal(t, GlobalConfigDiffs(nil), globalConfigDiff) }) t.Run("all actions global config", func(t *testing.T) { //given ecosystem config entries, _ := config.MapToEntries(map[string]any{ - "key1": "value1", // for action none - "key2": "value2", // for action set - "key3": "value3", // for action delete + "key1": val1.String(), // for action none + "key2": val2.String(), // for action set + "key3": val3.String(), // for action delete // key4 is absent -> action none }) globalConfig := config.CreateGlobalConfig(entries) //given blueprint config givenConfig := Config{ - Global: GlobalConfig{ - Present: map[common.GlobalConfigKey]common.GlobalConfigValue{ - "key1": "value1", - "key2": "value2.2", + Global: GlobalConfigEntries{ + { + Key: "key1", + Value: &val1, + }, + { + Key: "key2", + Value: &val3, + }, + { + Key: "key3", + Absent: true, }, - Absent: []common.GlobalConfigKey{ - "key3", "key4", + { + Key: "key4", + Absent: true, }, }, } @@ -66,88 +77,81 @@ func Test_determineConfigDiff(t *testing.T) { globalConfig, map[cescommons.SimpleName]config.DoguConfig{}, map[cescommons.SimpleName]config.DoguConfig{}, - map[common.SensitiveDoguConfigKey]common.SensitiveDoguConfigValue{}, + map[common.DoguConfigKey]common.SensitiveDoguConfigValue{}, ) //then - assert.Equal(t, map[cescommons.SimpleName]DoguConfigDiffs{}, dogusConfigDiffs) - assert.Equal(t, map[cescommons.SimpleName]SensitiveDoguConfigDiffs{}, sensitiveConfigDiffs) - assert.Equal(t, 4, len(globalConfigDiff)) - assert.Contains(t, globalConfigDiff, GlobalConfigEntryDiff{ - Key: "key1", - Actual: GlobalConfigValueState{ - Value: "value1", - Exists: true, - }, - Expected: GlobalConfigValueState{ - Value: "value1", - Exists: true, - }, - NeededAction: ConfigActionNone, - }) - assert.Contains(t, globalConfigDiff, GlobalConfigEntryDiff{ - Key: "key2", - Actual: GlobalConfigValueState{ - Value: "value2", - Exists: true, - }, - Expected: GlobalConfigValueState{ - Value: "value2.2", - Exists: true, - }, - NeededAction: ConfigActionSet, - }) - assert.Contains(t, globalConfigDiff, GlobalConfigEntryDiff{ - Key: "key3", - Actual: GlobalConfigValueState{ - Value: "value3", - Exists: true, - }, - Expected: GlobalConfigValueState{ - Value: "", - Exists: false, - }, - NeededAction: ConfigActionRemove, - }) - assert.Contains(t, globalConfigDiff, GlobalConfigEntryDiff{ - Key: "key4", - Actual: GlobalConfigValueState{ - Value: "", - Exists: false, - }, - Expected: GlobalConfigValueState{ - Value: "", - Exists: false, - }, - NeededAction: ConfigActionNone, - }) + assert.Nil(t, dogusConfigDiffs) + assert.Nil(t, sensitiveConfigDiffs) + assert.Equal(t, 2, len(globalConfigDiff)) // only changes + hitKeys := make(map[string]bool) + for _, diff := range globalConfigDiff { + if diff.Key == "key2" { + assert.Empty(t, cmp.Diff(diff, GlobalConfigEntryDiff{ + Key: "key2", + Actual: GlobalConfigValueState{ + Value: (*string)(&val2), + Exists: true, + }, + Expected: GlobalConfigValueState{ + Value: (*string)(&val3), + Exists: true, + }, + NeededAction: ConfigActionSet, + })) + hitKeys["key2"] = true + } + if diff.Key == "key3" { + assert.Empty(t, cmp.Diff(diff, GlobalConfigEntryDiff{ + Key: "key3", + Actual: GlobalConfigValueState{ + Value: (*string)(&val3), + Exists: true, + }, + Expected: GlobalConfigValueState{ + Value: nil, + Exists: false, + }, + NeededAction: ConfigActionRemove, + })) + hitKeys["key3"] = true + } + } + assert.Equal(t, 2, len(hitKeys)) }) + t.Run("all actions normal dogu config", func(t *testing.T) { //given ecosystem config globalConfigEntries, _ := config.MapToEntries(map[string]any{}) globalConfig := config.CreateGlobalConfig(globalConfigEntries) doguConfigEntries, _ := config.MapToEntries(map[string]any{ - "key1": "value", //action none - "key2": "value", //action set - "key3": "value", //action delete + "key1": "value1", //action none + "key2": "value1", //action set + "key3": "value1", //action delete //key4 -> absent, so action none }) doguConfig := config.CreateDoguConfig(dogu1, doguConfigEntries) //given blueprint config givenConfig := Config{ - Dogus: map[cescommons.SimpleName]CombinedDoguConfig{ + Dogus: map[cescommons.SimpleName]DoguConfigEntries{ "dogu1": { - DoguName: "dogu1", - Config: DoguConfig{ - Present: map[common.DoguConfigKey]common.DoguConfigValue{ - dogu1Key1: "value", - dogu1Key2: "updatedValue", - }, - Absent: []common.DoguConfigKey{ - dogu1Key3, dogu1Key4, - }, + { + Key: dogu1Key1.Key, + Value: &val1, + }, + { + Key: dogu1Key2.Key, + Value: &val2, + }, + { + Key: dogu1Key3.Key, + Absent: true, + }, + { + Key: dogu1Key4.Key, + Absent: true, }, }, }, @@ -161,65 +165,47 @@ func Test_determineConfigDiff(t *testing.T) { dogu1: doguConfig, }, map[cescommons.SimpleName]config.DoguConfig{}, - map[common.SensitiveDoguConfigKey]common.SensitiveDoguConfigValue{}, + map[common.DoguConfigKey]common.SensitiveDoguConfigValue{}, ) //then assert.Equal(t, GlobalConfigDiffs(nil), globalConfigDiff) require.NotNil(t, dogusConfigDiffs["dogu1"]) assert.Equal(t, SensitiveDoguConfigDiffs(nil), sensitiveConfigDiffs["dogu1"]) - assert.Equal(t, 4, len(dogusConfigDiffs["dogu1"])) - assert.Contains(t, dogusConfigDiffs["dogu1"], DoguConfigEntryDiff{ - Key: dogu1Key1, - Actual: DoguConfigValueState{ - Value: "value", - Exists: true, - }, - Expected: DoguConfigValueState{ - Value: "value", - Exists: true, - }, - NeededAction: ConfigActionNone, - }) - assert.Contains(t, dogusConfigDiffs["dogu1"], DoguConfigEntryDiff{ - Key: dogu1Key2, - Actual: DoguConfigValueState{ - Value: "value", - Exists: true, - }, - Expected: DoguConfigValueState{ - Value: "updatedValue", - Exists: true, - }, - NeededAction: ConfigActionSet, - }) - assert.Contains(t, dogusConfigDiffs["dogu1"], DoguConfigEntryDiff{ - Key: dogu1Key3, - Actual: DoguConfigValueState{ - Value: "value", - Exists: true, - }, - Expected: DoguConfigValueState{ - Value: "", - Exists: false, - }, - NeededAction: ConfigActionRemove, - }) - //domain.DoguConfigEntryDiff{Key:common.DoguConfigKey{DoguName:"dogu1", Key:"key3"}, - //Actual:domain.DoguConfigValueState{Value:"value", Exists:true}, - //Expected:domain.DoguConfigValueState{Value:"", Exists:false}, - //NeededAction:"set"} - assert.Contains(t, dogusConfigDiffs["dogu1"], DoguConfigEntryDiff{ - Key: dogu1Key4, - Actual: DoguConfigValueState{ - Value: "", - Exists: false, - }, - Expected: DoguConfigValueState{ - Value: "", - Exists: false, - }, - NeededAction: ConfigActionNone, - }) + assert.Equal(t, 2, len(dogusConfigDiffs["dogu1"])) // only changes + hitKeys := make(map[common.DoguConfigKey]bool) + for _, diff := range dogusConfigDiffs["dogu1"] { + if diff.Key == dogu1Key2 { + assert.Empty(t, cmp.Diff(diff, DoguConfigEntryDiff{ + Key: dogu1Key2, + Actual: DoguConfigValueState{ + Value: (*string)(&val1), + Exists: true, + }, + Expected: DoguConfigValueState{ + Value: (*string)(&val2), + Exists: true, + }, + NeededAction: ConfigActionSet, + })) + hitKeys[dogu1Key2] = true + } + if diff.Key == dogu1Key3 { + assert.Empty(t, cmp.Diff(diff, DoguConfigEntryDiff{ + Key: dogu1Key3, + Actual: DoguConfigValueState{ + Value: (*string)(&val1), + Exists: true, + }, + Expected: DoguConfigValueState{ + Value: nil, + Exists: false, + }, + NeededAction: ConfigActionRemove, + })) + hitKeys[dogu1Key3] = true + } + } + assert.Equal(t, 2, len(hitKeys)) }) t.Run("all actions for sensitive dogu config for present dogu", func(t *testing.T) { //given ecosystem config @@ -227,32 +213,37 @@ func Test_determineConfigDiff(t *testing.T) { globalConfig := config.CreateGlobalConfig(globalConfigEntries) sensitiveDoguConfigEntries, _ := config.MapToEntries(map[string]any{ - "key1": "value", //action none - "key2": "value", //action set + "key1": "value1", //action none + "key2": "value1", //action set //key3 absent, action none }) sensitiveDoguConfig := config.CreateDoguConfig(dogu1, sensitiveDoguConfigEntries) //given blueprint config givenConfig := Config{ - Dogus: map[cescommons.SimpleName]CombinedDoguConfig{ + Dogus: map[cescommons.SimpleName]DoguConfigEntries{ "dogu1": { - DoguName: "dogu1", - SensitiveConfig: SensitiveDoguConfig{ - Present: map[common.SensitiveDoguConfigKey]SensitiveValueRef{ - sensitiveDogu1Key1: { - SecretName: "mySecret1", - SecretKey: "myKey1", - }, - sensitiveDogu1Key2: { - SecretName: "mySecret2", - SecretKey: "myKey2", - }, + { + Key: dogu1Key1.Key, + Sensitive: true, + SecretRef: &SensitiveValueRef{ + SecretName: "mySecret1", + SecretKey: "myKey1", }, - Absent: []common.SensitiveDoguConfigKey{ - sensitiveDogu1Key3, + }, + { + Key: dogu1Key2.Key, + Sensitive: true, + SecretRef: &SensitiveValueRef{ + SecretName: "mySecret2", + SecretKey: "myKey2", }, }, + { + Key: dogu1Key3.Key, + Sensitive: true, + Absent: true, + }, }, }, } @@ -266,54 +257,30 @@ func Test_determineConfigDiff(t *testing.T) { dogu1: sensitiveDoguConfig, }, //loaded referenced sensitive config - map[common.SensitiveDoguConfigKey]common.SensitiveDoguConfigValue{ - sensitiveDogu1Key1: "value", - sensitiveDogu1Key2: "updated value", + map[common.DoguConfigKey]common.SensitiveDoguConfigValue{ + dogu1Key1: "value1", + dogu1Key2: "value2", }, ) //then assert.Equal(t, GlobalConfigDiffs(nil), globalConfigDiff) assert.Equal(t, DoguConfigDiffs(nil), dogusConfigDiffs["dogu1"]) require.NotNil(t, sensitiveConfigDiffs["dogu1"]) - assert.Equal(t, 3, len(sensitiveConfigDiffs["dogu1"])) + assert.Equal(t, 1, len(sensitiveConfigDiffs["dogu1"])) // only changes - entriesDogu1 := []SensitiveDoguConfigEntryDiff{ - { - Key: sensitiveDogu1Key1, - Actual: DoguConfigValueState{ - Value: "value", - Exists: true, - }, - Expected: DoguConfigValueState{ - Value: "value", - Exists: true, - }, - NeededAction: ConfigActionNone, - }, + entriesDogu1 := SensitiveDoguConfigDiffs{ { - Key: sensitiveDogu1Key2, + Key: dogu1Key2, Actual: DoguConfigValueState{ - Value: "value", + Value: (*string)(&val1), Exists: true, }, Expected: DoguConfigValueState{ - Value: "updated value", + Value: (*string)(&val2), Exists: true, }, NeededAction: ConfigActionSet, }, - { - Key: sensitiveDogu1Key3, - Actual: DoguConfigValueState{ - Value: "", - Exists: false, - }, - Expected: DoguConfigValueState{ - Value: "", - Exists: false, - }, - NeededAction: ConfigActionNone, - }, } assert.ElementsMatch(t, sensitiveConfigDiffs["dogu1"], entriesDogu1) }) @@ -330,17 +297,15 @@ func Test_determineConfigDiff(t *testing.T) { //given blueprint config givenConfig := Config{ - Dogus: map[cescommons.SimpleName]CombinedDoguConfig{ + Dogus: map[cescommons.SimpleName]DoguConfigEntries{ "dogu1": { - DoguName: "dogu1", - SensitiveConfig: SensitiveDoguConfig{ - Present: map[common.SensitiveDoguConfigKey]SensitiveValueRef{ - sensitiveDogu1Key1: { - SecretName: "secret1", - SecretKey: "key1", - }, + { + Key: dogu1Key1.Key, + Sensitive: true, + SecretRef: &SensitiveValueRef{ + SecretName: "mySecret1", + SecretKey: "myKey1", }, - Absent: []common.SensitiveDoguConfigKey{}, }, }, }, @@ -357,8 +322,8 @@ func Test_determineConfigDiff(t *testing.T) { dogu1: sensitiveDoguConfig, }, //loaded referenced sensitive config - map[common.SensitiveDoguConfigKey]common.SensitiveDoguConfigValue{ - sensitiveDogu1Key1: "value", + map[common.DoguConfigKey]common.SensitiveDoguConfigValue{ + dogu1Key1: "value1", }, ) //then @@ -366,18 +331,18 @@ func Test_determineConfigDiff(t *testing.T) { require.NotNil(t, sensitiveConfigDiffs["dogu1"]) require.Equal(t, 1, len(sensitiveConfigDiffs["dogu1"])) - assert.Equal(t, sensitiveConfigDiffs["dogu1"][0], SensitiveDoguConfigEntryDiff{ - Key: sensitiveDogu1Key1, + assert.Empty(t, cmp.Diff(sensitiveConfigDiffs["dogu1"][0], SensitiveDoguConfigEntryDiff{ + Key: dogu1Key1, Actual: DoguConfigValueState{ - Value: "", + Value: nil, Exists: false, }, Expected: DoguConfigValueState{ - Value: "value", + Value: (*string)(&val1), Exists: true, }, NeededAction: ConfigActionSet, - }) + })) }) } @@ -391,44 +356,44 @@ func Test_getNeededConfigAction(t *testing.T) { }{ { name: "action none, both do not exist", - expected: ConfigValueState{Value: "", Exists: false}, - actual: ConfigValueState{Value: "", Exists: false}, + expected: ConfigValueState{Value: nil, Exists: false}, + actual: ConfigValueState{Value: nil, Exists: false}, want: ConfigActionNone, }, { name: "action none, for some reason the values are different", - expected: ConfigValueState{Value: "1", Exists: false}, - actual: ConfigValueState{Value: "2", Exists: false}, + expected: ConfigValueState{Value: (*string)(&val1), Exists: false}, + actual: ConfigValueState{Value: (*string)(&val2), Exists: false}, want: ConfigActionNone, }, { name: "action none, equal values", - expected: ConfigValueState{Value: "1", Exists: true}, - actual: ConfigValueState{Value: "1", Exists: true}, + expected: ConfigValueState{Value: (*string)(&val1), Exists: true}, + actual: ConfigValueState{Value: (*string)(&val1), Exists: true}, want: ConfigActionNone, }, { name: "set new value", - expected: ConfigValueState{Value: "", Exists: true}, - actual: ConfigValueState{Value: "", Exists: false}, + expected: ConfigValueState{Value: nil, Exists: true}, + actual: ConfigValueState{Value: nil, Exists: false}, want: ConfigActionSet, }, { name: "update value", - expected: ConfigValueState{Value: "1", Exists: true}, - actual: ConfigValueState{Value: "2", Exists: true}, + expected: ConfigValueState{Value: (*string)(&val1), Exists: true}, + actual: ConfigValueState{Value: (*string)(&val2), Exists: true}, want: ConfigActionSet, }, { name: "remove value", - expected: ConfigValueState{Value: "", Exists: false}, - actual: ConfigValueState{Value: "", Exists: true}, + expected: ConfigValueState{Value: nil, Exists: false}, + actual: ConfigValueState{Value: nil, Exists: true}, want: ConfigActionRemove, }, { name: "remove value", - expected: ConfigValueState{Value: "", Exists: false}, - actual: ConfigValueState{Value: "value3", Exists: true}, + expected: ConfigValueState{Value: nil, Exists: false}, + actual: ConfigValueState{Value: (*string)(&val3), Exists: true}, want: ConfigActionRemove, }, } @@ -438,62 +403,3 @@ func Test_getNeededConfigAction(t *testing.T) { }) } } - -func Test_censorValues(t *testing.T) { - tests := []struct { - name string - configByDogu map[cescommons.SimpleName]SensitiveDoguConfigDiffs - want map[cescommons.SimpleName]SensitiveDoguConfigDiffs - }{ - { - name: "no diff at all", - configByDogu: map[cescommons.SimpleName]SensitiveDoguConfigDiffs{}, - want: map[cescommons.SimpleName]SensitiveDoguConfigDiffs{}, - }, - { - name: "no diff for dogu", - configByDogu: map[cescommons.SimpleName]SensitiveDoguConfigDiffs{ - dogu1: nil, - }, - want: map[cescommons.SimpleName]SensitiveDoguConfigDiffs{ - dogu1: nil, - }, - }, - { - name: "censored actual and expected values", - configByDogu: map[cescommons.SimpleName]SensitiveDoguConfigDiffs{ - dogu1: {DoguConfigEntryDiff{ - Key: dogu1Key1, - Actual: DoguConfigValueState{ - Value: "123", - Exists: true, - }, - Expected: DoguConfigValueState{ - Value: "1234", - Exists: true, - }, - NeededAction: ConfigActionSet, - }}, - }, - want: map[cescommons.SimpleName]SensitiveDoguConfigDiffs{ - dogu1: {DoguConfigEntryDiff{ - Key: dogu1Key1, - Actual: DoguConfigValueState{ - Value: censorValue, - Exists: true, - }, - Expected: DoguConfigValueState{ - Value: censorValue, - Exists: true, - }, - NeededAction: ConfigActionSet, - }}, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assert.Equalf(t, tt.want, censorValues(tt.configByDogu), "censorValues(%v)", tt.configByDogu) - }) - } -} diff --git a/pkg/domain/stateDiffDogu.go b/pkg/domain/stateDiffDogu.go index eb3956b6..328608c4 100644 --- a/pkg/domain/stateDiffDogu.go +++ b/pkg/domain/stateDiffDogu.go @@ -2,16 +2,26 @@ package domain import ( "fmt" + "slices" + cescommons "github.com/cloudogu/ces-commons-lib/dogu" "github.com/cloudogu/cesapp-lib/core" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" "golang.org/x/exp/maps" - "slices" ) // DoguDiffs contains the Diff for all expected Dogus to the current ecosystem.DoguInstallations. type DoguDiffs []DoguDiff +func (diffs DoguDiffs) HasChanges() bool { + for _, diff := range diffs { + if diff.HasChanges() { + return true + } + } + return false +} + // DoguDiff represents the Diff for a single expected Dogu to the current ecosystem.DoguInstallation. type DoguDiff struct { DoguName cescommons.SimpleName @@ -23,13 +33,17 @@ type DoguDiff struct { // DoguDiffState contains all fields to make a diff for dogus in respect to another DoguDiffState. type DoguDiffState struct { Namespace cescommons.Namespace - Version core.Version - InstallationState TargetState - MinVolumeSize ecosystem.VolumeSize + Version *core.Version + Absent bool + MinVolumeSize *ecosystem.VolumeSize ReverseProxyConfig ecosystem.ReverseProxyConfig AdditionalMounts []ecosystem.AdditionalMount } +func (diff DoguDiff) HasChanges() bool { + return len(diff.NeededActions) != 0 +} + // String returns a string representation of the DoguDiff. func (diff *DoguDiff) String() string { return fmt.Sprintf( @@ -44,27 +58,43 @@ func (diff *DoguDiff) String() string { // String returns a string representation of the DoguDiffState. func (diff *DoguDiffState) String() string { return fmt.Sprintf( - "{Version: %q, Namespace: %q, InstallationState: %q}", - diff.Version.Raw, + "{Version: %q, Namespace: %q, Absent: %t}", + diff.getSafeVersionString(), diff.Namespace, - diff.InstallationState, + diff.Absent, ) } +func (diff *DoguDiffState) getSafeVersionString() string { + if diff.Version != nil { + return diff.Version.String() + } else { + return "" + } +} + // determineDoguDiffs creates DoguDiffs for all dogus in the blueprint and all installed dogus as well. // see determineDoguDiff for more information. func determineDoguDiffs(blueprintDogus []Dogu, installedDogus map[cescommons.SimpleName]*ecosystem.DoguInstallation) []DoguDiff { var doguDiffs = map[cescommons.SimpleName]DoguDiff{} for _, blueprintDogu := range blueprintDogus { installedDogu := installedDogus[blueprintDogu.Name.SimpleName] - doguDiffs[blueprintDogu.Name.SimpleName] = determineDoguDiff(&blueprintDogu, installedDogu) + determinedDoguDiff := determineDoguDiff(&blueprintDogu, installedDogu) + // only add changes to diff + if determinedDoguDiff != nil { + doguDiffs[blueprintDogu.Name.SimpleName] = *determinedDoguDiff + } } for _, installedDogu := range installedDogus { _, found := FindDoguByName(blueprintDogus, installedDogu.Name.SimpleName) // Only create DoguDiff if the installed dogu is not found in the blueprint. // If the installed dogu is in blueprint the DoguDiff was already determined above. if !found { - doguDiffs[installedDogu.Name.SimpleName] = determineDoguDiff(nil, installedDogu) + determinedDoguDiff := determineDoguDiff(nil, installedDogu) + // only add changes to diff + if determinedDoguDiff != nil { + doguDiffs[installedDogu.Name.SimpleName] = *determinedDoguDiff + } } } return maps.Values(doguDiffs) @@ -74,20 +104,19 @@ func determineDoguDiffs(blueprintDogus []Dogu, installedDogus map[cescommons.Sim // if the Dogu is nil (was not in the blueprint), the actual state is also the expected state. // if the installedDogu is nil, it is considered to be not installed currently. // returns a DoguDiff -func determineDoguDiff(blueprintDogu *Dogu, installedDogu *ecosystem.DoguInstallation) DoguDiff { +func determineDoguDiff(blueprintDogu *Dogu, installedDogu *ecosystem.DoguInstallation) *DoguDiff { var expectedState, actualState DoguDiffState var doguName cescommons.SimpleName = "" // either blueprintDogu or installedDogu could be nil if installedDogu == nil { actualState = DoguDiffState{ - InstallationState: TargetStateAbsent, + Absent: true, } } else { doguName = installedDogu.Name.SimpleName actualState = DoguDiffState{ Namespace: installedDogu.Name.Namespace, - Version: installedDogu.Version, - InstallationState: TargetStatePresent, + Version: &installedDogu.Version, MinVolumeSize: installedDogu.MinVolumeSize, ReverseProxyConfig: installedDogu.ReverseProxyConfig, AdditionalMounts: installedDogu.AdditionalMounts, @@ -101,41 +130,41 @@ func determineDoguDiff(blueprintDogu *Dogu, installedDogu *ecosystem.DoguInstall expectedState = DoguDiffState{ Namespace: blueprintDogu.Name.Namespace, Version: blueprintDogu.Version, - InstallationState: blueprintDogu.TargetState, + Absent: blueprintDogu.Absent, MinVolumeSize: blueprintDogu.MinVolumeSize, ReverseProxyConfig: blueprintDogu.ReverseProxyConfig, AdditionalMounts: blueprintDogu.AdditionalMounts, } } - return DoguDiff{ + actions := getNeededDoguActions(expectedState, actualState) + if len(actions) == 0 { + return nil + } + + return &DoguDiff{ DoguName: doguName, Expected: expectedState, Actual: actualState, - NeededActions: getNeededDoguActions(expectedState, actualState), + NeededActions: actions, } } func getNeededDoguActions(expected DoguDiffState, actual DoguDiffState) []Action { - if expected.InstallationState == actual.InstallationState { - switch expected.InstallationState { - case TargetStatePresent: - // dogu should stay installed, but maybe it needs an upgrade, downgrade or a namespace switch? - return getActionsForPresentDoguDiffs(expected, actual) - case TargetStateAbsent: + if expected.Absent == actual.Absent { + if expected.Absent { return []Action{} + } else { + return getActionsForPresentDoguDiffs(expected, actual) } } else { // actual state is always the opposite - switch expected.InstallationState { - case TargetStatePresent: - return []Action{ActionInstall} - case TargetStateAbsent: + if expected.Absent { return []Action{ActionUninstall} + } else { + return []Action{ActionInstall} } } - // all cases should be handled above, but if new fields are added, this is a safe fallback for any bugs. - return []Action{} } func getActionsForPresentDoguDiffs(expected DoguDiffState, actual DoguDiffState) []Action { @@ -145,18 +174,12 @@ func getActionsForPresentDoguDiffs(expected DoguDiffState, actual DoguDiffState) } neededActions = appendActionForMinVolumeSize(neededActions, expected.MinVolumeSize, actual.MinVolumeSize) - neededActions = appendActionForProxyBodySizes(neededActions, expected.ReverseProxyConfig.MaxBodySize, actual.ReverseProxyConfig.MaxBodySize) neededActions = appendActionForAdditionalMounts(neededActions, expected.AdditionalMounts, actual.AdditionalMounts) + neededActions = appendActionForReverseProxyConfig(neededActions, expected, actual) - if expected.ReverseProxyConfig.RewriteTarget != actual.ReverseProxyConfig.RewriteTarget { - neededActions = append(neededActions, ActionUpdateDoguProxyRewriteTarget) - } - if expected.ReverseProxyConfig.AdditionalConfig != actual.ReverseProxyConfig.AdditionalConfig { - neededActions = append(neededActions, ActionUpdateDoguProxyAdditionalConfig) - } - if expected.Version.IsNewerThan(actual.Version) { + if expected.Version != nil && actual.Version != nil && expected.Version.IsNewerThan(*actual.Version) { neededActions = append(neededActions, ActionUpgrade) - } else if actual.Version.IsNewerThan(expected.Version) { + } else if expected.Version != nil && actual.Version.IsNewerThan(*expected.Version) { // if downgrades are allowed is not important here. // Downgrades can be rejected later, so forcing downgrades via a flag can be implemented without changing this code here. neededActions = append(neededActions, ActionDowngrade) @@ -165,22 +188,48 @@ func getActionsForPresentDoguDiffs(expected DoguDiffState, actual DoguDiffState) return neededActions } -func appendActionForMinVolumeSize(actions []Action, expectedSize ecosystem.VolumeSize, actualSize ecosystem.VolumeSize) []Action { +func appendActionForReverseProxyConfig(neededActions []Action, expected DoguDiffState, actual DoguDiffState) []Action { + exp := expected.ReverseProxyConfig + act := actual.ReverseProxyConfig + + // both empty → nothing to do + if exp.IsEmpty() && act.IsEmpty() { + return neededActions + } + + if exp.RewriteTarget != act.RewriteTarget || exp.AdditionalConfig != act.AdditionalConfig { + neededActions = append(neededActions, ActionUpdateDoguReverseProxyConfig) + return neededActions // early return to avoid duplicate actions + } + + return appendActionForProxyBodySizes(neededActions, exp, act) +} + +func appendActionForMinVolumeSize(actions []Action, expectedSize *ecosystem.VolumeSize, actualSize *ecosystem.VolumeSize) []Action { // if expected > actual = update needed - if expectedSize.Cmp(actualSize) > 0 { + if expectedSize == nil { + return actions + } else if actualSize == nil || expectedSize.Cmp(*actualSize) > 0 { return append(actions, ActionUpdateDoguResourceMinVolumeSize) } return actions } -func appendActionForProxyBodySizes(actions []Action, expectedProxyBodySize *ecosystem.BodySize, actualProxyBodySize *ecosystem.BodySize) []Action { +func appendActionForProxyBodySizes( + actions []Action, + expectedReverseProxyConfig ecosystem.ReverseProxyConfig, + actualReverseProxyConfig ecosystem.ReverseProxyConfig, +) []Action { + actualProxyBodySize := actualReverseProxyConfig.MaxBodySize + expectedProxyBodySize := expectedReverseProxyConfig.MaxBodySize + if expectedProxyBodySize == nil && actualProxyBodySize == nil { return actions } else if proxyBodySizeIdentityChanged(expectedProxyBodySize, actualProxyBodySize) { - return append(actions, ActionUpdateDoguProxyBodySize) + return append(actions, ActionUpdateDoguReverseProxyConfig) } else { - if expectedProxyBodySize.Cmp(*actualProxyBodySize) != 0 { - return append(actions, ActionUpdateDoguProxyBodySize) + if expectedProxyBodySize != nil && actualProxyBodySize != nil && expectedProxyBodySize.Cmp(*actualProxyBodySize) != 0 { + return append(actions, ActionUpdateDoguReverseProxyConfig) } } return actions diff --git a/pkg/domain/stateDiffDoguConfig.go b/pkg/domain/stateDiffDoguConfig.go index 3088df47..26d18ffb 100644 --- a/pkg/domain/stateDiffDoguConfig.go +++ b/pkg/domain/stateDiffDoguConfig.go @@ -18,33 +18,26 @@ func (diffs DoguConfigDiffs) HasChanges() bool { return false } -func (diffs SensitiveDoguConfigDiffs) CensorValues() SensitiveDoguConfigDiffs { - var censoredEntries []DoguConfigEntryDiff - for _, entry := range diffs { - actual := entry.Actual - expected := entry.Expected - if len(entry.Actual.Value) > 0 { - actual.Value = censorValue - } - if len(entry.Expected.Value) > 0 { - expected.Value = censorValue - } - censoredEntries = append(censoredEntries, DoguConfigEntryDiff{ - Key: entry.Key, - Actual: actual, - Expected: expected, - NeededAction: entry.NeededAction, - }) - } - return censoredEntries -} - type DoguConfigValueState ConfigValueState type ConfigValueState struct { - Value string + Value *string Exists bool } + +func (a ConfigValueState) Equal(b ConfigValueState) bool { + if a.Exists != b.Exists { + return false + } + if a.Value == b.Value { // covers both nil and same address + return true + } + if a.Value == nil || b.Value == nil { + return false + } + return *a.Value == *b.Value +} + type DoguConfigEntryDiff struct { Key common.DoguConfigKey Actual DoguConfigValueState @@ -55,17 +48,17 @@ type SensitiveDoguConfigEntryDiff = DoguConfigEntryDiff func newDoguConfigEntryDiff( key common.DoguConfigKey, - actualValue config.Value, + actualValue *config.Value, actualExists bool, - expectedValue common.DoguConfigValue, + expectedValue *common.DoguConfigValue, expectedExists bool, ) DoguConfigEntryDiff { actual := DoguConfigValueState{ - Value: string(actualValue), + Value: (*string)(actualValue), Exists: actualExists, } expected := DoguConfigValueState{ - Value: string(expectedValue), + Value: (*string)(expectedValue), Exists: expectedExists, } return DoguConfigEntryDiff{ @@ -76,20 +69,27 @@ func newDoguConfigEntryDiff( } } -func determineDoguConfigDiffs( - wantedConfig DoguConfig, - actualConfig map[cescommons.SimpleName]config.DoguConfig, -) DoguConfigDiffs { +func determineDoguConfigDiffs(doguName cescommons.SimpleName, wantedConfig DoguConfigEntries, actualConfig map[cescommons.SimpleName]config.DoguConfig, isSensitive bool) DoguConfigDiffs { var doguConfigDiff []DoguConfigEntryDiff - // present entries - for key, expectedValue := range wantedConfig.Present { - actualEntry, exists := actualConfig[key.DoguName].Get(key.Key) - doguConfigDiff = append(doguConfigDiff, newDoguConfigEntryDiff(key, actualEntry, exists, expectedValue, true)) - } - // absent entries - for _, key := range wantedConfig.Absent { - actualEntry, exists := actualConfig[key.DoguName].Get(key.Key) - doguConfigDiff = append(doguConfigDiff, newDoguConfigEntryDiff(key, actualEntry, exists, "", false)) + for _, expectedConfig := range wantedConfig { + if expectedConfig.Sensitive != isSensitive { + continue // skip if not match sensitivity + } + + var actualValue *config.Value + actualEntry, exists := actualConfig[doguName].Get(expectedConfig.Key) + if exists { + actualValue = &actualEntry + } + configKey := common.DoguConfigKey{ + DoguName: doguName, + Key: expectedConfig.Key, + } + diff := newDoguConfigEntryDiff(configKey, actualValue, exists, expectedConfig.Value, !expectedConfig.Absent) + // only add diff if there are changes + if diff.NeededAction != ConfigActionNone { + doguConfigDiff = append(doguConfigDiff, diff) + } } return doguConfigDiff } diff --git a/pkg/domain/stateDiffDoguConfig_test.go b/pkg/domain/stateDiffDoguConfig_test.go index 21f9ad77..30f50a01 100644 --- a/pkg/domain/stateDiffDoguConfig_test.go +++ b/pkg/domain/stateDiffDoguConfig_test.go @@ -1,11 +1,13 @@ package domain import ( - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" ) func TestDoguConfigDiffs_HasChanges(t *testing.T) { + testVal := "test" tests := []struct { name string diffs DoguConfigDiffs @@ -36,7 +38,7 @@ func TestDoguConfigDiffs_HasChanges(t *testing.T) { Exists: false, }, Expected: DoguConfigValueState{ - Value: "test", + Value: &testVal, Exists: true, }, NeededAction: ConfigActionSet, @@ -50,7 +52,7 @@ func TestDoguConfigDiffs_HasChanges(t *testing.T) { SensitiveDoguConfigEntryDiff{ Key: dogu1Key1, Actual: DoguConfigValueState{ - Value: "test", + Value: &testVal, Exists: true, }, Expected: DoguConfigValueState{ diff --git a/pkg/domain/stateDiffDogu_test.go b/pkg/domain/stateDiffDogu_test.go index 26fc42ce..f9bbec38 100644 --- a/pkg/domain/stateDiffDogu_test.go +++ b/pkg/domain/stateDiffDogu_test.go @@ -1,11 +1,19 @@ package domain import ( + "testing" + cescommons "github.com/cloudogu/ces-commons-lib/dogu" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" "github.com/stretchr/testify/assert" "k8s.io/apimachinery/pkg/api/resource" - "testing" +) + +var ( + additionalConfig = "additional" + rewriteConfig = "/" + subfolder2 = "secsubfolder" + subfolder3 = "different_subfolder" ) func Test_determineDoguDiff(t *testing.T) { @@ -24,55 +32,42 @@ func Test_determineDoguDiff(t *testing.T) { tests := []struct { name string args args - want DoguDiff + want *DoguDiff }{ { name: "equal, no action", args: args{ blueprintDogu: &Dogu{ - Name: officialNexus, - Version: version3211, - TargetState: TargetStatePresent, + Name: officialNexus, + Version: &version3211, + Absent: false, }, installedDogu: &ecosystem.DoguInstallation{ Name: officialNexus, Version: version3211, }, }, - want: DoguDiff{ - DoguName: "nexus", - Actual: DoguDiffState{ - Namespace: officialNamespace, - Version: version3211, - InstallationState: TargetStatePresent, - }, - Expected: DoguDiffState{ - Namespace: officialNamespace, - Version: version3211, - InstallationState: TargetStatePresent, - }, - NeededActions: nil, - }, + want: nil, }, { name: "install", args: args{ blueprintDogu: &Dogu{ - Name: officialNexus, - Version: version3211, - TargetState: TargetStatePresent, + Name: officialNexus, + Version: &version3211, + Absent: false, }, installedDogu: nil, }, - want: DoguDiff{ + want: &DoguDiff{ DoguName: "nexus", Actual: DoguDiffState{ - InstallationState: TargetStateAbsent, + Absent: true, }, Expected: DoguDiffState{ - Namespace: officialNamespace, - Version: version3211, - InstallationState: TargetStatePresent, + Namespace: officialNamespace, + Version: &version3211, + Absent: false, }, NeededActions: []Action{ActionInstall}, }, @@ -81,24 +76,24 @@ func Test_determineDoguDiff(t *testing.T) { name: "uninstall", args: args{ blueprintDogu: &Dogu{ - Name: officialNexus, - TargetState: TargetStateAbsent, + Name: officialNexus, + Absent: true, }, installedDogu: &ecosystem.DoguInstallation{ Name: officialNexus, Version: version3211, }, }, - want: DoguDiff{ + want: &DoguDiff{ DoguName: "nexus", Actual: DoguDiffState{ - Namespace: officialNamespace, - Version: version3211, - InstallationState: TargetStatePresent, + Namespace: officialNamespace, + Version: &version3211, + Absent: false, }, Expected: DoguDiffState{ - Namespace: officialNamespace, - InstallationState: TargetStateAbsent, + Namespace: officialNamespace, + Absent: true, }, NeededActions: []Action{ActionUninstall}, }, @@ -107,26 +102,26 @@ func Test_determineDoguDiff(t *testing.T) { name: "namespace switch", args: args{ blueprintDogu: &Dogu{ - Name: premiumNexus, - Version: version3211, - TargetState: TargetStatePresent, + Name: premiumNexus, + Version: &version3211, + Absent: false, }, installedDogu: &ecosystem.DoguInstallation{ Name: officialNexus, Version: version3211, }, }, - want: DoguDiff{ + want: &DoguDiff{ DoguName: "nexus", Actual: DoguDiffState{ - Namespace: officialNamespace, - Version: version3211, - InstallationState: TargetStatePresent, + Namespace: officialNamespace, + Version: &version3211, + Absent: false, }, Expected: DoguDiffState{ - Namespace: "premium", - Version: version3211, - InstallationState: TargetStatePresent, + Namespace: "premium", + Version: &version3211, + Absent: false, }, NeededActions: []Action{ActionSwitchDoguNamespace}, }, @@ -135,26 +130,26 @@ func Test_determineDoguDiff(t *testing.T) { name: "upgrade", args: args{ blueprintDogu: &Dogu{ - Name: officialNexus, - Version: version3212, - TargetState: TargetStatePresent, + Name: officialNexus, + Version: &version3212, + Absent: false, }, installedDogu: &ecosystem.DoguInstallation{ Name: officialNexus, Version: version3211, }, }, - want: DoguDiff{ + want: &DoguDiff{ DoguName: "nexus", Actual: DoguDiffState{ - Namespace: officialNamespace, - Version: version3211, - InstallationState: TargetStatePresent, + Namespace: officialNamespace, + Version: &version3211, + Absent: false, }, Expected: DoguDiffState{ - Namespace: officialNamespace, - Version: version3212, - InstallationState: TargetStatePresent, + Namespace: officialNamespace, + Version: &version3212, + Absent: false, }, NeededActions: []Action{ActionUpgrade}, }, @@ -164,25 +159,26 @@ func Test_determineDoguDiff(t *testing.T) { args: args{ blueprintDogu: &Dogu{ Name: officialNexus, - TargetState: TargetStatePresent, - MinVolumeSize: quantity100M, + Version: &version3212, + MinVolumeSize: &quantity100M, }, installedDogu: &ecosystem.DoguInstallation{ Name: officialNexus, - MinVolumeSize: quantity10M, + Version: version3212, + MinVolumeSize: &quantity10M, }, }, - want: DoguDiff{ + want: &DoguDiff{ DoguName: "nexus", Expected: DoguDiffState{ - Namespace: officialNamespace, - InstallationState: TargetStatePresent, - MinVolumeSize: quantity100M, + Namespace: officialNamespace, + Version: &version3212, + MinVolumeSize: &quantity100M, }, Actual: DoguDiffState{ - Namespace: officialNamespace, - InstallationState: TargetStatePresent, - MinVolumeSize: quantity10M, + Namespace: officialNamespace, + Version: &version3212, + MinVolumeSize: &quantity10M, }, NeededActions: []Action{ActionUpdateDoguResourceMinVolumeSize}, }, @@ -192,123 +188,93 @@ func Test_determineDoguDiff(t *testing.T) { args: args{ blueprintDogu: &Dogu{ Name: officialNexus, - TargetState: TargetStatePresent, - MinVolumeSize: quantity100M, + Version: &version3212, + MinVolumeSize: &quantity100M, }, installedDogu: &ecosystem.DoguInstallation{ Name: officialNexus, - MinVolumeSize: quantity100M, + Version: version3212, + MinVolumeSize: &quantity100M, }, }, - want: DoguDiff{ - DoguName: "nexus", - Expected: DoguDiffState{ - Namespace: officialNamespace, - InstallationState: TargetStatePresent, - MinVolumeSize: quantity100M, - }, - Actual: DoguDiffState{ - Namespace: officialNamespace, - InstallationState: TargetStatePresent, - MinVolumeSize: quantity100M, - }, - NeededActions: nil, - }, + want: nil, }, { name: "don't update minVolSize if actual > expected", args: args{ blueprintDogu: &Dogu{ Name: officialNexus, - TargetState: TargetStatePresent, - MinVolumeSize: quantity10M, + Version: &version3212, + MinVolumeSize: &quantity10M, }, installedDogu: &ecosystem.DoguInstallation{ Name: officialNexus, - MinVolumeSize: quantity100M, + Version: version3212, + MinVolumeSize: &quantity100M, }, }, - want: DoguDiff{ - DoguName: "nexus", - Expected: DoguDiffState{ - Namespace: officialNamespace, - InstallationState: TargetStatePresent, - MinVolumeSize: quantity10M, - }, - Actual: DoguDiffState{ - Namespace: officialNamespace, - InstallationState: TargetStatePresent, - MinVolumeSize: quantity100M, - }, - NeededActions: nil, - }, + want: nil, }, { name: "multiple update actions", args: args{ blueprintDogu: &Dogu{ - Name: officialNexus, - Version: version3212, - TargetState: TargetStatePresent, + Name: officialNexus, + Version: &version3212, ReverseProxyConfig: ecosystem.ReverseProxyConfig{ MaxBodySize: &proxyBodySize, - AdditionalConfig: "additional", - RewriteTarget: "/", + AdditionalConfig: ecosystem.AdditionalConfig(additionalConfig), + RewriteTarget: ecosystem.RewriteTarget(rewriteConfig), }, - MinVolumeSize: volumeSize2, + MinVolumeSize: &volumeSize2, }, installedDogu: &ecosystem.DoguInstallation{ Name: officialNexus, Version: version3211, - MinVolumeSize: volumeSize1, + MinVolumeSize: &volumeSize1, }, }, - want: DoguDiff{ + want: &DoguDiff{ DoguName: "nexus", Actual: DoguDiffState{ - Namespace: officialNamespace, - Version: version3211, - InstallationState: TargetStatePresent, - MinVolumeSize: volumeSize1, + Namespace: officialNamespace, + Version: &version3211, + MinVolumeSize: &volumeSize1, }, Expected: DoguDiffState{ - Namespace: officialNamespace, - Version: version3212, - InstallationState: TargetStatePresent, + Namespace: officialNamespace, + Version: &version3212, ReverseProxyConfig: ecosystem.ReverseProxyConfig{ MaxBodySize: &proxyBodySize, - AdditionalConfig: "additional", - RewriteTarget: "/", + AdditionalConfig: ecosystem.AdditionalConfig(additionalConfig), + RewriteTarget: ecosystem.RewriteTarget(rewriteConfig), }, - MinVolumeSize: volumeSize2, + MinVolumeSize: &volumeSize2, }, - NeededActions: []Action{ActionUpdateDoguResourceMinVolumeSize, ActionUpdateDoguProxyBodySize, ActionUpdateDoguProxyRewriteTarget, ActionUpdateDoguProxyAdditionalConfig, ActionUpgrade}, + NeededActions: []Action{ActionUpdateDoguResourceMinVolumeSize, ActionUpdateDoguReverseProxyConfig, ActionUpgrade}, }, }, { name: "downgrade", args: args{ blueprintDogu: &Dogu{ - Name: officialNexus, - Version: version3211, - TargetState: TargetStatePresent, + Name: officialNexus, + Version: &version3211, }, installedDogu: &ecosystem.DoguInstallation{ Name: officialNexus, Version: version3212, }, }, - want: DoguDiff{ + want: &DoguDiff{ DoguName: "nexus", Actual: DoguDiffState{ - Namespace: officialNamespace, - Version: version3212, - InstallationState: TargetStatePresent, + Namespace: officialNamespace, + Version: &version3212, }, Expected: DoguDiffState{ - Namespace: officialNamespace, - Version: version3211, - InstallationState: TargetStatePresent, + Namespace: officialNamespace, + Version: &version3211, }, NeededActions: []Action{ActionDowngrade}, }, @@ -322,20 +288,7 @@ func Test_determineDoguDiff(t *testing.T) { Version: version3211, }, }, - want: DoguDiff{ - DoguName: "nexus", - Actual: DoguDiffState{ - Namespace: officialNamespace, - Version: version3211, - InstallationState: TargetStatePresent, - }, - Expected: DoguDiffState{ - Namespace: officialNamespace, - Version: version3211, - InstallationState: TargetStatePresent, - }, - NeededActions: nil, - }, + want: nil, }, { name: "should stay absent, no action", @@ -343,24 +296,14 @@ func Test_determineDoguDiff(t *testing.T) { blueprintDogu: nil, installedDogu: nil, }, - want: DoguDiff{ - DoguName: "", - Actual: DoguDiffState{ - InstallationState: TargetStateAbsent, - }, - Expected: DoguDiffState{ - InstallationState: TargetStateAbsent, - }, - NeededActions: []Action{}, - }, + want: nil, }, { name: "update proxy body size", args: args{ blueprintDogu: &Dogu{ - Name: officialNexus, - Version: version3212, - TargetState: TargetStatePresent, + Name: officialNexus, + Version: &version3212, ReverseProxyConfig: ecosystem.ReverseProxyConfig{ MaxBodySize: quantity100MPtr, }, @@ -373,34 +316,31 @@ func Test_determineDoguDiff(t *testing.T) { }, }, }, - want: DoguDiff{ + want: &DoguDiff{ DoguName: "nexus", Actual: DoguDiffState{ - Namespace: officialNamespace, - Version: version3212, - InstallationState: TargetStatePresent, + Namespace: officialNamespace, + Version: &version3212, ReverseProxyConfig: ecosystem.ReverseProxyConfig{ MaxBodySize: nil, }, }, Expected: DoguDiffState{ - Namespace: officialNamespace, - Version: version3212, - InstallationState: TargetStatePresent, + Namespace: officialNamespace, + Version: &version3212, ReverseProxyConfig: ecosystem.ReverseProxyConfig{ MaxBodySize: quantity100MPtr, }, }, - NeededActions: []Action{ActionUpdateDoguProxyBodySize}, + NeededActions: []Action{ActionUpdateDoguReverseProxyConfig}, }, }, { name: "update if proxy body size changed", args: args{ blueprintDogu: &Dogu{ - Name: officialNexus, - Version: version3212, - TargetState: TargetStatePresent, + Name: officialNexus, + Version: &version3212, ReverseProxyConfig: ecosystem.ReverseProxyConfig{ MaxBodySize: quantity100MPtr, }, @@ -413,34 +353,31 @@ func Test_determineDoguDiff(t *testing.T) { }, }, }, - want: DoguDiff{ + want: &DoguDiff{ DoguName: "nexus", Actual: DoguDiffState{ - Namespace: officialNamespace, - Version: version3212, - InstallationState: TargetStatePresent, + Namespace: officialNamespace, + Version: &version3212, ReverseProxyConfig: ecosystem.ReverseProxyConfig{ MaxBodySize: quantity10MPtr, }, }, Expected: DoguDiffState{ - Namespace: officialNamespace, - Version: version3212, - InstallationState: TargetStatePresent, + Namespace: officialNamespace, + Version: &version3212, ReverseProxyConfig: ecosystem.ReverseProxyConfig{ MaxBodySize: quantity100MPtr, }, }, - NeededActions: []Action{ActionUpdateDoguProxyBodySize}, + NeededActions: []Action{ActionUpdateDoguReverseProxyConfig}, }, }, { name: "no update if body sizes are nil", args: args{ blueprintDogu: &Dogu{ - Name: officialNexus, - Version: version3212, - TargetState: TargetStatePresent, + Name: officialNexus, + Version: &version3212, ReverseProxyConfig: ecosystem.ReverseProxyConfig{ MaxBodySize: nil, }, @@ -453,249 +390,155 @@ func Test_determineDoguDiff(t *testing.T) { }, }, }, - want: DoguDiff{ - DoguName: "nexus", - Actual: DoguDiffState{ - Namespace: officialNamespace, - Version: version3212, - InstallationState: TargetStatePresent, - ReverseProxyConfig: ecosystem.ReverseProxyConfig{ - MaxBodySize: nil, - }, - }, - Expected: DoguDiffState{ - Namespace: officialNamespace, - Version: version3212, - InstallationState: TargetStatePresent, - ReverseProxyConfig: ecosystem.ReverseProxyConfig{ - MaxBodySize: nil, - }, - }, - NeededActions: nil, - }, + want: nil, }, { name: "no action if additional mounts are equal", args: args{ blueprintDogu: &Dogu{ - Name: officialNexus, - TargetState: TargetStatePresent, + Name: officialNexus, + Version: &version3212, AdditionalMounts: []ecosystem.AdditionalMount{ { SourceType: ecosystem.DataSourceConfigMap, Name: "configmap", Volume: "volume", - Subfolder: "subfolder", + Subfolder: subfolder, }, { SourceType: ecosystem.DataSourceSecret, Name: "secret", Volume: "secvolume", - Subfolder: "secsubfolder", + Subfolder: subfolder2, }, }, }, installedDogu: &ecosystem.DoguInstallation{ - Name: officialNexus, - AdditionalMounts: []ecosystem.AdditionalMount{ - { - SourceType: ecosystem.DataSourceConfigMap, - Name: "configmap", - Volume: "volume", - Subfolder: "subfolder", - }, - { - SourceType: ecosystem.DataSourceSecret, - Name: "secret", - Volume: "secvolume", - Subfolder: "secsubfolder", - }, - }, - }, - }, - want: DoguDiff{ - DoguName: "nexus", - Actual: DoguDiffState{ - Namespace: officialNamespace, - InstallationState: TargetStatePresent, - AdditionalMounts: []ecosystem.AdditionalMount{ - { - SourceType: ecosystem.DataSourceConfigMap, - Name: "configmap", - Volume: "volume", - Subfolder: "subfolder", - }, - { - SourceType: ecosystem.DataSourceSecret, - Name: "secret", - Volume: "secvolume", - Subfolder: "secsubfolder", - }, - }, - }, - Expected: DoguDiffState{ - Namespace: officialNamespace, - InstallationState: TargetStatePresent, + Name: officialNexus, + Version: version3212, AdditionalMounts: []ecosystem.AdditionalMount{ { SourceType: ecosystem.DataSourceConfigMap, Name: "configmap", Volume: "volume", - Subfolder: "subfolder", + Subfolder: subfolder, }, { SourceType: ecosystem.DataSourceSecret, Name: "secret", Volume: "secvolume", - Subfolder: "secsubfolder", + Subfolder: subfolder2, }, }, }, - NeededActions: nil, }, + want: nil, }, { name: "no action if additional mounts are equal but order is different", args: args{ blueprintDogu: &Dogu{ - Name: officialNexus, - TargetState: TargetStatePresent, + Name: officialNexus, + Version: &version3212, AdditionalMounts: []ecosystem.AdditionalMount{ { SourceType: ecosystem.DataSourceSecret, Name: "secret", Volume: "secvolume", - Subfolder: "secsubfolder", + Subfolder: subfolder2, }, { SourceType: ecosystem.DataSourceConfigMap, Name: "configmap", Volume: "volume", - Subfolder: "subfolder", + Subfolder: subfolder, }, }, }, installedDogu: &ecosystem.DoguInstallation{ - Name: officialNexus, - AdditionalMounts: []ecosystem.AdditionalMount{ - { - SourceType: ecosystem.DataSourceConfigMap, - Name: "configmap", - Volume: "volume", - Subfolder: "subfolder", - }, - { - SourceType: ecosystem.DataSourceSecret, - Name: "secret", - Volume: "secvolume", - Subfolder: "secsubfolder", - }, - }, - }, - }, - want: DoguDiff{ - DoguName: "nexus", - Actual: DoguDiffState{ - Namespace: officialNamespace, - InstallationState: TargetStatePresent, + Name: officialNexus, + Version: version3212, AdditionalMounts: []ecosystem.AdditionalMount{ { SourceType: ecosystem.DataSourceConfigMap, Name: "configmap", Volume: "volume", - Subfolder: "subfolder", + Subfolder: subfolder, }, { SourceType: ecosystem.DataSourceSecret, Name: "secret", Volume: "secvolume", - Subfolder: "secsubfolder", + Subfolder: subfolder2, }, }, }, - Expected: DoguDiffState{ - Namespace: officialNamespace, - InstallationState: TargetStatePresent, - AdditionalMounts: []ecosystem.AdditionalMount{ - { - SourceType: ecosystem.DataSourceSecret, - Name: "secret", - Volume: "secvolume", - Subfolder: "secsubfolder", - }, - { - SourceType: ecosystem.DataSourceConfigMap, - Name: "configmap", - Volume: "volume", - Subfolder: "subfolder", - }, - }, - }, - NeededActions: nil, }, + want: nil, }, { name: "needs update action for additional mounts if the size is different", args: args{ blueprintDogu: &Dogu{ - Name: officialNexus, - TargetState: TargetStatePresent, + Name: officialNexus, + Version: &version3212, AdditionalMounts: []ecosystem.AdditionalMount{ { SourceType: ecosystem.DataSourceConfigMap, Name: "configmap", Volume: "volume", - Subfolder: "subfolder", + Subfolder: subfolder, }, }, }, installedDogu: &ecosystem.DoguInstallation{ - Name: officialNexus, + Name: officialNexus, + Version: version3212, AdditionalMounts: []ecosystem.AdditionalMount{ { SourceType: ecosystem.DataSourceConfigMap, Name: "configmap", Volume: "volume", - Subfolder: "subfolder", + Subfolder: subfolder, }, { SourceType: ecosystem.DataSourceSecret, Name: "secret", Volume: "secvolume", - Subfolder: "secsubfolder", + Subfolder: subfolder2, }, }, }, }, - want: DoguDiff{ + want: &DoguDiff{ DoguName: "nexus", Actual: DoguDiffState{ - Namespace: officialNamespace, - InstallationState: TargetStatePresent, + Namespace: officialNamespace, + Version: &version3212, AdditionalMounts: []ecosystem.AdditionalMount{ { SourceType: ecosystem.DataSourceConfigMap, Name: "configmap", Volume: "volume", - Subfolder: "subfolder", + Subfolder: subfolder, }, { SourceType: ecosystem.DataSourceSecret, Name: "secret", Volume: "secvolume", - Subfolder: "secsubfolder", + Subfolder: subfolder2, }, }, }, Expected: DoguDiffState{ - Namespace: officialNamespace, - InstallationState: TargetStatePresent, + Namespace: officialNamespace, + Version: &version3212, AdditionalMounts: []ecosystem.AdditionalMount{ { SourceType: ecosystem.DataSourceConfigMap, Name: "configmap", Volume: "volume", - Subfolder: "subfolder", + Subfolder: subfolder, }, }, }, @@ -706,76 +549,77 @@ func Test_determineDoguDiff(t *testing.T) { name: "needs update action for additional mounts if an element is different", args: args{ blueprintDogu: &Dogu{ - Name: officialNexus, - TargetState: TargetStatePresent, + Name: officialNexus, + Version: &version3212, AdditionalMounts: []ecosystem.AdditionalMount{ { SourceType: ecosystem.DataSourceConfigMap, Name: "configmap", Volume: "volume", - Subfolder: "different_subfolder", + Subfolder: subfolder3, }, { SourceType: ecosystem.DataSourceSecret, Name: "secret", Volume: "secvolume", - Subfolder: "secsubfolder", + Subfolder: subfolder2, }, }, }, installedDogu: &ecosystem.DoguInstallation{ - Name: officialNexus, + Name: officialNexus, + Version: version3212, AdditionalMounts: []ecosystem.AdditionalMount{ { SourceType: ecosystem.DataSourceConfigMap, Name: "configmap", Volume: "volume", - Subfolder: "subfolder", + Subfolder: subfolder, }, { SourceType: ecosystem.DataSourceSecret, Name: "secret", Volume: "secvolume", - Subfolder: "secsubfolder", + Subfolder: subfolder2, }, }, }, }, - want: DoguDiff{ + want: &DoguDiff{ DoguName: "nexus", Actual: DoguDiffState{ - Namespace: officialNamespace, - InstallationState: TargetStatePresent, + Namespace: officialNamespace, + Version: &version3212, AdditionalMounts: []ecosystem.AdditionalMount{ { SourceType: ecosystem.DataSourceConfigMap, Name: "configmap", Volume: "volume", - Subfolder: "subfolder", + Subfolder: subfolder, }, { SourceType: ecosystem.DataSourceSecret, Name: "secret", Volume: "secvolume", - Subfolder: "secsubfolder", + Subfolder: subfolder2, }, }, }, Expected: DoguDiffState{ - Namespace: officialNamespace, - InstallationState: TargetStatePresent, + Namespace: officialNamespace, + Version: &version3212, AdditionalMounts: []ecosystem.AdditionalMount{ { SourceType: ecosystem.DataSourceConfigMap, Name: "configmap", Volume: "volume", - Subfolder: "different_subfolder", + Subfolder: subfolder3, }, { SourceType: ecosystem.DataSourceSecret, Name: "secret", Volume: "secvolume", - Subfolder: "secsubfolder", + Subfolder: subfolder2, }, }, }, @@ -813,9 +657,9 @@ func Test_determineDoguDiffs(t *testing.T) { args: args{ blueprintDogus: []Dogu{ { - Name: officialNexus, - Version: version3211, - TargetState: TargetStatePresent, + Name: officialNexus, + Version: &version3211, + Absent: false, }, }, installedDogus: nil, @@ -824,12 +668,12 @@ func Test_determineDoguDiffs(t *testing.T) { { DoguName: "nexus", Actual: DoguDiffState{ - InstallationState: TargetStateAbsent, + Absent: true, }, Expected: DoguDiffState{ - Namespace: officialNamespace, - Version: version3211, - InstallationState: TargetStatePresent, + Namespace: officialNamespace, + Version: &version3211, + Absent: false, }, NeededActions: []Action{ActionInstall}, }, @@ -846,31 +690,16 @@ func Test_determineDoguDiffs(t *testing.T) { }, }, }, - want: []DoguDiff{ - { - DoguName: "nexus", - Actual: DoguDiffState{ - Namespace: officialNamespace, - Version: version3211, - InstallationState: TargetStatePresent, - }, - Expected: DoguDiffState{ - Namespace: officialNamespace, - Version: version3211, - InstallationState: TargetStatePresent, - }, - NeededActions: nil, - }, - }, + want: []DoguDiff{}, }, { name: "an installed dogu which is also in the blueprint", args: args{ blueprintDogus: []Dogu{ { - Name: officialNexus, - Version: version3212, - TargetState: TargetStatePresent, + Name: officialNexus, + Version: &version3212, + Absent: false, }, }, installedDogus: map[cescommons.SimpleName]*ecosystem.DoguInstallation{ @@ -884,14 +713,14 @@ func Test_determineDoguDiffs(t *testing.T) { { DoguName: "nexus", Actual: DoguDiffState{ - Namespace: officialNamespace, - Version: version3211, - InstallationState: TargetStatePresent, + Namespace: officialNamespace, + Version: &version3211, + Absent: false, }, Expected: DoguDiffState{ - Namespace: officialNamespace, - Version: version3212, - InstallationState: TargetStatePresent, + Namespace: officialNamespace, + Version: &version3212, + Absent: false, }, NeededActions: []Action{ActionUpgrade}, }, @@ -907,14 +736,14 @@ func Test_determineDoguDiffs(t *testing.T) { func TestDoguDiff_String(t *testing.T) { actual := DoguDiffState{ - Namespace: "official", - Version: version3211, - InstallationState: TargetStatePresent, + Namespace: "official", + Version: &version3211, + Absent: false, } expected := DoguDiffState{ - Namespace: "premium", - Version: version3212, - InstallationState: TargetStatePresent, + Namespace: "premium", + Version: &version3212, + Absent: false, } diff := &DoguDiff{ DoguName: "postgresql", @@ -925,17 +754,17 @@ func TestDoguDiff_String(t *testing.T) { assert.Equal(t, "{"+ "DoguName: \"postgresql\", "+ - "Actual: {Version: \"3.2.1-1\", Namespace: \"official\", InstallationState: \"present\"}, "+ - "Expected: {Version: \"3.2.1-2\", Namespace: \"premium\", InstallationState: \"present\"}, "+ + "Actual: {Version: \"3.2.1-1\", Namespace: \"official\", Absent: false}, "+ + "Expected: {Version: \"3.2.1-2\", Namespace: \"premium\", Absent: false}, "+ "NeededActions: [\"upgrade\" \"update resource minimum volume size\"]"+ "}", diff.String()) } func TestDoguDiffState_String(t *testing.T) { diff := &DoguDiffState{ - Namespace: "official", - Version: version3211, - InstallationState: TargetStatePresent, + Namespace: "official", + Version: &version3211, + Absent: false, } - assert.Equal(t, "{Version: \"3.2.1-1\", Namespace: \"official\", InstallationState: \"present\"}", diff.String()) + assert.Equal(t, "{Version: \"3.2.1-1\", Namespace: \"official\", Absent: false}", diff.String()) } diff --git a/pkg/domain/stateDiffGlobalConfig.go b/pkg/domain/stateDiffGlobalConfig.go index 37c62473..6455c2e4 100644 --- a/pkg/domain/stateDiffGlobalConfig.go +++ b/pkg/domain/stateDiffGlobalConfig.go @@ -41,17 +41,17 @@ func (diffs GlobalConfigDiffs) countByAction() map[ConfigAction]int { func newGlobalConfigEntryDiff( key common.GlobalConfigKey, - actualValue common.GlobalConfigValue, + actualValue *common.GlobalConfigValue, actualExists bool, - expectedValue common.GlobalConfigValue, + expectedValue *common.GlobalConfigValue, expectedExists bool, ) GlobalConfigEntryDiff { actual := GlobalConfigValueState{ - Value: string(actualValue), + Value: (*string)(actualValue), Exists: actualExists, } expected := GlobalConfigValueState{ - Value: string(expectedValue), + Value: (*string)(expectedValue), Exists: expectedExists, } return GlobalConfigEntryDiff{ @@ -63,20 +63,22 @@ func newGlobalConfigEntryDiff( } func determineGlobalConfigDiffs( - config GlobalConfig, + config GlobalConfigEntries, actualConfig config.GlobalConfig, ) GlobalConfigDiffs { var configDiffs []GlobalConfigEntryDiff - // present entries - for key, expectedValue := range config.Present { - actualEntry, actualExists := actualConfig.Get(key) - configDiffs = append(configDiffs, newGlobalConfigEntryDiff(key, actualEntry, actualExists, expectedValue, true)) - } - // absent entries - for _, key := range config.Absent { - actualEntry, actualExists := actualConfig.Get(key) - configDiffs = append(configDiffs, newGlobalConfigEntryDiff(key, actualEntry, actualExists, "", false)) + for _, expectedConfig := range config { + var actualValue *common.GlobalConfigValue + actualEntry, actualExists := actualConfig.Get(expectedConfig.Key) + if actualExists { + actualValue = &actualEntry + } + diff := newGlobalConfigEntryDiff(expectedConfig.Key, actualValue, actualExists, expectedConfig.Value, !expectedConfig.Absent) + // only add diff if there are changes + if diff.NeededAction != ConfigActionNone { + configDiffs = append(configDiffs, diff) + } } return configDiffs } diff --git a/pkg/domain/stateDiffGlobalConfig_test.go b/pkg/domain/stateDiffGlobalConfig_test.go index 2b3276d2..4b14e24f 100644 --- a/pkg/domain/stateDiffGlobalConfig_test.go +++ b/pkg/domain/stateDiffGlobalConfig_test.go @@ -1,11 +1,14 @@ package domain import ( - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" ) func TestGlobalConfigDiffs_HasChanges(t *testing.T) { + valChanged := "changed" + valInitial := "initial" tests := []struct { name string diffs GlobalConfigDiffs @@ -21,8 +24,8 @@ func TestGlobalConfigDiffs_HasChanges(t *testing.T) { diffs: []GlobalConfigEntryDiff{ { Key: "testkey", - Actual: GlobalConfigValueState{Value: "changed", Exists: true}, - Expected: GlobalConfigValueState{Value: "initial", Exists: true}, + Actual: GlobalConfigValueState{Value: &valChanged, Exists: true}, + Expected: GlobalConfigValueState{Value: &valInitial, Exists: true}, NeededAction: ConfigActionSet, }, }, @@ -33,8 +36,8 @@ func TestGlobalConfigDiffs_HasChanges(t *testing.T) { diffs: []GlobalConfigEntryDiff{ { Key: "testkey", - Actual: GlobalConfigValueState{Value: "changed", Exists: true}, - Expected: GlobalConfigValueState{Value: "initial", Exists: true}, + Actual: GlobalConfigValueState{Value: &valChanged, Exists: true}, + Expected: GlobalConfigValueState{Value: &valInitial, Exists: true}, NeededAction: ConfigActionNone, }, }, diff --git a/pkg/domain/targetState.go b/pkg/domain/targetState.go deleted file mode 100644 index 70edc2c8..00000000 --- a/pkg/domain/targetState.go +++ /dev/null @@ -1,27 +0,0 @@ -package domain - -// TargetState defines an enum of values that determines a state of installation. -type TargetState int - -const ( - // TargetStatePresent is the default state. If selected the chosen item must be present after the blueprint was - // applied. - TargetStatePresent = iota - // TargetStateAbsent sets the state of the item to absent. If selected the chosen item must be absent after the - // blueprint was applied. - TargetStateAbsent -) - -var PossibleTargetStates = []TargetState{ - TargetStatePresent, TargetStateAbsent, -} - -// String returns a string representation of the given TargetState enum value. -func (state TargetState) String() string { - return toString[state] -} - -var toString = map[TargetState]string{ - TargetStatePresent: "present", - TargetStateAbsent: "absent", -} diff --git a/pkg/domain/targetState_test.go b/pkg/domain/targetState_test.go deleted file mode 100644 index 3d3ba131..00000000 --- a/pkg/domain/targetState_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package domain - -import ( - "testing" -) - -func TestTargetState_String(t *testing.T) { - tests := []struct { - name string - state TargetState - want string - }{ - { - "map present enum value to string", - TargetStatePresent, - "present", - }, - { - "map absent enum value to string", - TargetStateAbsent, - "absent", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := tt.state.String(); got != tt.want { - t.Errorf("TargetState.String() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/pkg/domainservice/adapterInterfaces.go b/pkg/domainservice/adapterInterfaces.go index 66c847c0..7f233a75 100644 --- a/pkg/domainservice/adapterInterfaces.go +++ b/pkg/domainservice/adapterInterfaces.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + cescommons "github.com/cloudogu/ces-commons-lib/dogu" "github.com/cloudogu/cesapp-lib/core" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/common" @@ -37,41 +38,18 @@ type DoguInstallationRepository interface { Delete(ctx context.Context, doguName cescommons.SimpleName) error } -type ComponentInstallationRepository interface { - // GetByName loads an installed component from the ecosystem and returns - // - the ecosystem.ComponentInstallation or - // - a NotFoundError if the component is not installed or - // - an InternalError if there is any other error. - GetByName(ctx context.Context, componentName common.SimpleComponentName) (*ecosystem.ComponentInstallation, error) - // GetAll returns - // - the installation info of all installed components or - // - an InternalError if there is any other error. - GetAll(ctx context.Context) (map[common.SimpleComponentName]*ecosystem.ComponentInstallation, error) - // Delete deletes the component by name from the ecosystem. - // returns an InternalError if there is an error. - Delete(ctx context.Context, componentName common.SimpleComponentName) error - // Create creates the ecosystem.ComponentInstallation in the ecosystem. - // returns an InternalError if there is an error. - Create(ctx context.Context, component *ecosystem.ComponentInstallation) error - // Update updates the ecosystem.ComponentInstallation in the ecosystem. - // returns an InternalError if anything went wrong. - Update(ctx context.Context, component *ecosystem.ComponentInstallation) error -} - -type RequiredComponentsProvider interface { - GetRequiredComponents(ctx context.Context) ([]ecosystem.RequiredComponent, error) -} - -type HealthWaitConfigProvider interface { - GetWaitConfig(ctx context.Context) (ecosystem.WaitConfig, error) -} - type BlueprintSpecRepository interface { // GetById returns a BlueprintSpec identified by its ID or // a NotFoundError if the BlueprintSpec was not found or // a domain.InvalidBlueprintError together with a BlueprintSpec without blueprint and mask if the BlueprintSpec could not be parsed or // an InternalError if there is any other error. GetById(ctx context.Context, blueprintId string) (*domain.BlueprintSpec, error) + + // CheckSingleton checks if there is indeed only a single Blueprint-resource in the namespace of the repository or + // a domain.MultipleBlueprintsError if there are at least two Blueprint-resources or + // an InternalError if there is any other error. + CheckSingleton(ctx context.Context) error + // Update updates a given BlueprintSpec. // returns a ConflictError if there were changes on the BlueprintSpec in the meantime or // returns an InternalError if there is any other error @@ -95,11 +73,6 @@ type DoguToLoad struct { Version string } -type DoguRestartRepository interface { - // RestartAll restarts all provided Dogus - RestartAll(context.Context, []cescommons.SimpleName) error -} - // GlobalConfigRepository is used to get the whole global config of the ecosystem to make changes and persist it as a whole. type GlobalConfigRepository interface { // Get retrieves the whole global config. @@ -165,9 +138,9 @@ type SensitiveConfigRefReader interface { // - InternalError if any other error happens. GetValues( ctx context.Context, - refs map[common.SensitiveDoguConfigKey]domain.SensitiveValueRef, + refs map[common.DoguConfigKey]domain.SensitiveValueRef, ) ( - map[common.SensitiveDoguConfigKey]common.SensitiveDoguConfigValue, + map[common.DoguConfigKey]common.SensitiveDoguConfigValue, error, ) } @@ -182,6 +155,7 @@ func NewNotFoundError(wrappedError error, message string, msgArgs ...any) *NotFo type NotFoundError struct { WrappedError error Message string + DoNotRetry bool } // Error marks the struct as an error. diff --git a/pkg/domainservice/mock_BlueprintSpecRepository_test.go b/pkg/domainservice/mock_BlueprintSpecRepository_test.go index 70d6e932..dc8384d6 100644 --- a/pkg/domainservice/mock_BlueprintSpecRepository_test.go +++ b/pkg/domainservice/mock_BlueprintSpecRepository_test.go @@ -22,6 +22,52 @@ func (_m *MockBlueprintSpecRepository) EXPECT() *MockBlueprintSpecRepository_Exp return &MockBlueprintSpecRepository_Expecter{mock: &_m.Mock} } +// CheckSingleton provides a mock function with given fields: ctx +func (_m *MockBlueprintSpecRepository) CheckSingleton(ctx context.Context) error { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for CheckSingleton") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(ctx) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockBlueprintSpecRepository_CheckSingleton_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CheckSingleton' +type MockBlueprintSpecRepository_CheckSingleton_Call struct { + *mock.Call +} + +// CheckSingleton is a helper method to define mock.On call +// - ctx context.Context +func (_e *MockBlueprintSpecRepository_Expecter) CheckSingleton(ctx interface{}) *MockBlueprintSpecRepository_CheckSingleton_Call { + return &MockBlueprintSpecRepository_CheckSingleton_Call{Call: _e.mock.On("CheckSingleton", ctx)} +} + +func (_c *MockBlueprintSpecRepository_CheckSingleton_Call) Run(run func(ctx context.Context)) *MockBlueprintSpecRepository_CheckSingleton_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *MockBlueprintSpecRepository_CheckSingleton_Call) Return(_a0 error) *MockBlueprintSpecRepository_CheckSingleton_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockBlueprintSpecRepository_CheckSingleton_Call) RunAndReturn(run func(context.Context) error) *MockBlueprintSpecRepository_CheckSingleton_Call { + _c.Call.Return(run) + return _c +} + // GetById provides a mock function with given fields: ctx, blueprintId func (_m *MockBlueprintSpecRepository) GetById(ctx context.Context, blueprintId string) (*domain.BlueprintSpec, error) { ret := _m.Called(ctx, blueprintId) diff --git a/pkg/domainservice/mock_ComponentInstallationRepository_test.go b/pkg/domainservice/mock_ComponentInstallationRepository_test.go deleted file mode 100644 index d269ae54..00000000 --- a/pkg/domainservice/mock_ComponentInstallationRepository_test.go +++ /dev/null @@ -1,298 +0,0 @@ -// Code generated by mockery v2.53.3. DO NOT EDIT. - -package domainservice - -import ( - context "context" - - common "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/common" - - ecosystem "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" - - mock "github.com/stretchr/testify/mock" -) - -// MockComponentInstallationRepository is an autogenerated mock type for the ComponentInstallationRepository type -type MockComponentInstallationRepository struct { - mock.Mock -} - -type MockComponentInstallationRepository_Expecter struct { - mock *mock.Mock -} - -func (_m *MockComponentInstallationRepository) EXPECT() *MockComponentInstallationRepository_Expecter { - return &MockComponentInstallationRepository_Expecter{mock: &_m.Mock} -} - -// Create provides a mock function with given fields: ctx, component -func (_m *MockComponentInstallationRepository) Create(ctx context.Context, component *ecosystem.ComponentInstallation) error { - ret := _m.Called(ctx, component) - - if len(ret) == 0 { - panic("no return value specified for Create") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *ecosystem.ComponentInstallation) error); ok { - r0 = rf(ctx, component) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// MockComponentInstallationRepository_Create_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Create' -type MockComponentInstallationRepository_Create_Call struct { - *mock.Call -} - -// Create is a helper method to define mock.On call -// - ctx context.Context -// - component *ecosystem.ComponentInstallation -func (_e *MockComponentInstallationRepository_Expecter) Create(ctx interface{}, component interface{}) *MockComponentInstallationRepository_Create_Call { - return &MockComponentInstallationRepository_Create_Call{Call: _e.mock.On("Create", ctx, component)} -} - -func (_c *MockComponentInstallationRepository_Create_Call) Run(run func(ctx context.Context, component *ecosystem.ComponentInstallation)) *MockComponentInstallationRepository_Create_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(*ecosystem.ComponentInstallation)) - }) - return _c -} - -func (_c *MockComponentInstallationRepository_Create_Call) Return(_a0 error) *MockComponentInstallationRepository_Create_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockComponentInstallationRepository_Create_Call) RunAndReturn(run func(context.Context, *ecosystem.ComponentInstallation) error) *MockComponentInstallationRepository_Create_Call { - _c.Call.Return(run) - return _c -} - -// Delete provides a mock function with given fields: ctx, componentName -func (_m *MockComponentInstallationRepository) Delete(ctx context.Context, componentName common.SimpleComponentName) error { - ret := _m.Called(ctx, componentName) - - if len(ret) == 0 { - panic("no return value specified for Delete") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, common.SimpleComponentName) error); ok { - r0 = rf(ctx, componentName) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// MockComponentInstallationRepository_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete' -type MockComponentInstallationRepository_Delete_Call struct { - *mock.Call -} - -// Delete is a helper method to define mock.On call -// - ctx context.Context -// - componentName common.SimpleComponentName -func (_e *MockComponentInstallationRepository_Expecter) Delete(ctx interface{}, componentName interface{}) *MockComponentInstallationRepository_Delete_Call { - return &MockComponentInstallationRepository_Delete_Call{Call: _e.mock.On("Delete", ctx, componentName)} -} - -func (_c *MockComponentInstallationRepository_Delete_Call) Run(run func(ctx context.Context, componentName common.SimpleComponentName)) *MockComponentInstallationRepository_Delete_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(common.SimpleComponentName)) - }) - return _c -} - -func (_c *MockComponentInstallationRepository_Delete_Call) Return(_a0 error) *MockComponentInstallationRepository_Delete_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockComponentInstallationRepository_Delete_Call) RunAndReturn(run func(context.Context, common.SimpleComponentName) error) *MockComponentInstallationRepository_Delete_Call { - _c.Call.Return(run) - return _c -} - -// GetAll provides a mock function with given fields: ctx -func (_m *MockComponentInstallationRepository) GetAll(ctx context.Context) (map[common.SimpleComponentName]*ecosystem.ComponentInstallation, error) { - ret := _m.Called(ctx) - - if len(ret) == 0 { - panic("no return value specified for GetAll") - } - - var r0 map[common.SimpleComponentName]*ecosystem.ComponentInstallation - var r1 error - if rf, ok := ret.Get(0).(func(context.Context) (map[common.SimpleComponentName]*ecosystem.ComponentInstallation, error)); ok { - return rf(ctx) - } - if rf, ok := ret.Get(0).(func(context.Context) map[common.SimpleComponentName]*ecosystem.ComponentInstallation); ok { - r0 = rf(ctx) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(map[common.SimpleComponentName]*ecosystem.ComponentInstallation) - } - } - - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(ctx) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockComponentInstallationRepository_GetAll_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAll' -type MockComponentInstallationRepository_GetAll_Call struct { - *mock.Call -} - -// GetAll is a helper method to define mock.On call -// - ctx context.Context -func (_e *MockComponentInstallationRepository_Expecter) GetAll(ctx interface{}) *MockComponentInstallationRepository_GetAll_Call { - return &MockComponentInstallationRepository_GetAll_Call{Call: _e.mock.On("GetAll", ctx)} -} - -func (_c *MockComponentInstallationRepository_GetAll_Call) Run(run func(ctx context.Context)) *MockComponentInstallationRepository_GetAll_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context)) - }) - return _c -} - -func (_c *MockComponentInstallationRepository_GetAll_Call) Return(_a0 map[common.SimpleComponentName]*ecosystem.ComponentInstallation, _a1 error) *MockComponentInstallationRepository_GetAll_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockComponentInstallationRepository_GetAll_Call) RunAndReturn(run func(context.Context) (map[common.SimpleComponentName]*ecosystem.ComponentInstallation, error)) *MockComponentInstallationRepository_GetAll_Call { - _c.Call.Return(run) - return _c -} - -// GetByName provides a mock function with given fields: ctx, componentName -func (_m *MockComponentInstallationRepository) GetByName(ctx context.Context, componentName common.SimpleComponentName) (*ecosystem.ComponentInstallation, error) { - ret := _m.Called(ctx, componentName) - - if len(ret) == 0 { - panic("no return value specified for GetByName") - } - - var r0 *ecosystem.ComponentInstallation - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, common.SimpleComponentName) (*ecosystem.ComponentInstallation, error)); ok { - return rf(ctx, componentName) - } - if rf, ok := ret.Get(0).(func(context.Context, common.SimpleComponentName) *ecosystem.ComponentInstallation); ok { - r0 = rf(ctx, componentName) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*ecosystem.ComponentInstallation) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, common.SimpleComponentName) error); ok { - r1 = rf(ctx, componentName) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockComponentInstallationRepository_GetByName_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetByName' -type MockComponentInstallationRepository_GetByName_Call struct { - *mock.Call -} - -// GetByName is a helper method to define mock.On call -// - ctx context.Context -// - componentName common.SimpleComponentName -func (_e *MockComponentInstallationRepository_Expecter) GetByName(ctx interface{}, componentName interface{}) *MockComponentInstallationRepository_GetByName_Call { - return &MockComponentInstallationRepository_GetByName_Call{Call: _e.mock.On("GetByName", ctx, componentName)} -} - -func (_c *MockComponentInstallationRepository_GetByName_Call) Run(run func(ctx context.Context, componentName common.SimpleComponentName)) *MockComponentInstallationRepository_GetByName_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(common.SimpleComponentName)) - }) - return _c -} - -func (_c *MockComponentInstallationRepository_GetByName_Call) Return(_a0 *ecosystem.ComponentInstallation, _a1 error) *MockComponentInstallationRepository_GetByName_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockComponentInstallationRepository_GetByName_Call) RunAndReturn(run func(context.Context, common.SimpleComponentName) (*ecosystem.ComponentInstallation, error)) *MockComponentInstallationRepository_GetByName_Call { - _c.Call.Return(run) - return _c -} - -// Update provides a mock function with given fields: ctx, component -func (_m *MockComponentInstallationRepository) Update(ctx context.Context, component *ecosystem.ComponentInstallation) error { - ret := _m.Called(ctx, component) - - if len(ret) == 0 { - panic("no return value specified for Update") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *ecosystem.ComponentInstallation) error); ok { - r0 = rf(ctx, component) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// MockComponentInstallationRepository_Update_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Update' -type MockComponentInstallationRepository_Update_Call struct { - *mock.Call -} - -// Update is a helper method to define mock.On call -// - ctx context.Context -// - component *ecosystem.ComponentInstallation -func (_e *MockComponentInstallationRepository_Expecter) Update(ctx interface{}, component interface{}) *MockComponentInstallationRepository_Update_Call { - return &MockComponentInstallationRepository_Update_Call{Call: _e.mock.On("Update", ctx, component)} -} - -func (_c *MockComponentInstallationRepository_Update_Call) Run(run func(ctx context.Context, component *ecosystem.ComponentInstallation)) *MockComponentInstallationRepository_Update_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(*ecosystem.ComponentInstallation)) - }) - return _c -} - -func (_c *MockComponentInstallationRepository_Update_Call) Return(_a0 error) *MockComponentInstallationRepository_Update_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockComponentInstallationRepository_Update_Call) RunAndReturn(run func(context.Context, *ecosystem.ComponentInstallation) error) *MockComponentInstallationRepository_Update_Call { - _c.Call.Return(run) - return _c -} - -// NewMockComponentInstallationRepository creates a new instance of MockComponentInstallationRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewMockComponentInstallationRepository(t interface { - mock.TestingT - Cleanup(func()) -}) *MockComponentInstallationRepository { - mock := &MockComponentInstallationRepository{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/domainservice/mock_DoguRestartRepository_test.go b/pkg/domainservice/mock_DoguRestartRepository_test.go deleted file mode 100644 index b5f5dd47..00000000 --- a/pkg/domainservice/mock_DoguRestartRepository_test.go +++ /dev/null @@ -1,84 +0,0 @@ -// Code generated by mockery v2.53.3. DO NOT EDIT. - -package domainservice - -import ( - context "context" - - dogu "github.com/cloudogu/ces-commons-lib/dogu" - mock "github.com/stretchr/testify/mock" -) - -// MockDoguRestartRepository is an autogenerated mock type for the DoguRestartRepository type -type MockDoguRestartRepository struct { - mock.Mock -} - -type MockDoguRestartRepository_Expecter struct { - mock *mock.Mock -} - -func (_m *MockDoguRestartRepository) EXPECT() *MockDoguRestartRepository_Expecter { - return &MockDoguRestartRepository_Expecter{mock: &_m.Mock} -} - -// RestartAll provides a mock function with given fields: _a0, _a1 -func (_m *MockDoguRestartRepository) RestartAll(_a0 context.Context, _a1 []dogu.SimpleName) error { - ret := _m.Called(_a0, _a1) - - if len(ret) == 0 { - panic("no return value specified for RestartAll") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, []dogu.SimpleName) error); ok { - r0 = rf(_a0, _a1) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// MockDoguRestartRepository_RestartAll_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RestartAll' -type MockDoguRestartRepository_RestartAll_Call struct { - *mock.Call -} - -// RestartAll is a helper method to define mock.On call -// - _a0 context.Context -// - _a1 []dogu.SimpleName -func (_e *MockDoguRestartRepository_Expecter) RestartAll(_a0 interface{}, _a1 interface{}) *MockDoguRestartRepository_RestartAll_Call { - return &MockDoguRestartRepository_RestartAll_Call{Call: _e.mock.On("RestartAll", _a0, _a1)} -} - -func (_c *MockDoguRestartRepository_RestartAll_Call) Run(run func(_a0 context.Context, _a1 []dogu.SimpleName)) *MockDoguRestartRepository_RestartAll_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].([]dogu.SimpleName)) - }) - return _c -} - -func (_c *MockDoguRestartRepository_RestartAll_Call) Return(_a0 error) *MockDoguRestartRepository_RestartAll_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockDoguRestartRepository_RestartAll_Call) RunAndReturn(run func(context.Context, []dogu.SimpleName) error) *MockDoguRestartRepository_RestartAll_Call { - _c.Call.Return(run) - return _c -} - -// NewMockDoguRestartRepository creates a new instance of MockDoguRestartRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewMockDoguRestartRepository(t interface { - mock.TestingT - Cleanup(func()) -}) *MockDoguRestartRepository { - mock := &MockDoguRestartRepository{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/domainservice/mock_HealthWaitConfigProvider_test.go b/pkg/domainservice/mock_HealthWaitConfigProvider_test.go deleted file mode 100644 index b3e12ccc..00000000 --- a/pkg/domainservice/mock_HealthWaitConfigProvider_test.go +++ /dev/null @@ -1,93 +0,0 @@ -// Code generated by mockery v2.53.3. DO NOT EDIT. - -package domainservice - -import ( - context "context" - - ecosystem "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" - mock "github.com/stretchr/testify/mock" -) - -// MockHealthWaitConfigProvider is an autogenerated mock type for the HealthWaitConfigProvider type -type MockHealthWaitConfigProvider struct { - mock.Mock -} - -type MockHealthWaitConfigProvider_Expecter struct { - mock *mock.Mock -} - -func (_m *MockHealthWaitConfigProvider) EXPECT() *MockHealthWaitConfigProvider_Expecter { - return &MockHealthWaitConfigProvider_Expecter{mock: &_m.Mock} -} - -// GetWaitConfig provides a mock function with given fields: ctx -func (_m *MockHealthWaitConfigProvider) GetWaitConfig(ctx context.Context) (ecosystem.WaitConfig, error) { - ret := _m.Called(ctx) - - if len(ret) == 0 { - panic("no return value specified for GetWaitConfig") - } - - var r0 ecosystem.WaitConfig - var r1 error - if rf, ok := ret.Get(0).(func(context.Context) (ecosystem.WaitConfig, error)); ok { - return rf(ctx) - } - if rf, ok := ret.Get(0).(func(context.Context) ecosystem.WaitConfig); ok { - r0 = rf(ctx) - } else { - r0 = ret.Get(0).(ecosystem.WaitConfig) - } - - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(ctx) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockHealthWaitConfigProvider_GetWaitConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetWaitConfig' -type MockHealthWaitConfigProvider_GetWaitConfig_Call struct { - *mock.Call -} - -// GetWaitConfig is a helper method to define mock.On call -// - ctx context.Context -func (_e *MockHealthWaitConfigProvider_Expecter) GetWaitConfig(ctx interface{}) *MockHealthWaitConfigProvider_GetWaitConfig_Call { - return &MockHealthWaitConfigProvider_GetWaitConfig_Call{Call: _e.mock.On("GetWaitConfig", ctx)} -} - -func (_c *MockHealthWaitConfigProvider_GetWaitConfig_Call) Run(run func(ctx context.Context)) *MockHealthWaitConfigProvider_GetWaitConfig_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context)) - }) - return _c -} - -func (_c *MockHealthWaitConfigProvider_GetWaitConfig_Call) Return(_a0 ecosystem.WaitConfig, _a1 error) *MockHealthWaitConfigProvider_GetWaitConfig_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockHealthWaitConfigProvider_GetWaitConfig_Call) RunAndReturn(run func(context.Context) (ecosystem.WaitConfig, error)) *MockHealthWaitConfigProvider_GetWaitConfig_Call { - _c.Call.Return(run) - return _c -} - -// NewMockHealthWaitConfigProvider creates a new instance of MockHealthWaitConfigProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewMockHealthWaitConfigProvider(t interface { - mock.TestingT - Cleanup(func()) -}) *MockHealthWaitConfigProvider { - mock := &MockHealthWaitConfigProvider{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/domainservice/mock_RequiredComponentsProvider_test.go b/pkg/domainservice/mock_RequiredComponentsProvider_test.go deleted file mode 100644 index c4b4c949..00000000 --- a/pkg/domainservice/mock_RequiredComponentsProvider_test.go +++ /dev/null @@ -1,95 +0,0 @@ -// Code generated by mockery v2.53.3. DO NOT EDIT. - -package domainservice - -import ( - context "context" - - ecosystem "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" - mock "github.com/stretchr/testify/mock" -) - -// MockRequiredComponentsProvider is an autogenerated mock type for the RequiredComponentsProvider type -type MockRequiredComponentsProvider struct { - mock.Mock -} - -type MockRequiredComponentsProvider_Expecter struct { - mock *mock.Mock -} - -func (_m *MockRequiredComponentsProvider) EXPECT() *MockRequiredComponentsProvider_Expecter { - return &MockRequiredComponentsProvider_Expecter{mock: &_m.Mock} -} - -// GetRequiredComponents provides a mock function with given fields: ctx -func (_m *MockRequiredComponentsProvider) GetRequiredComponents(ctx context.Context) ([]ecosystem.RequiredComponent, error) { - ret := _m.Called(ctx) - - if len(ret) == 0 { - panic("no return value specified for GetRequiredComponents") - } - - var r0 []ecosystem.RequiredComponent - var r1 error - if rf, ok := ret.Get(0).(func(context.Context) ([]ecosystem.RequiredComponent, error)); ok { - return rf(ctx) - } - if rf, ok := ret.Get(0).(func(context.Context) []ecosystem.RequiredComponent); ok { - r0 = rf(ctx) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]ecosystem.RequiredComponent) - } - } - - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(ctx) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockRequiredComponentsProvider_GetRequiredComponents_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRequiredComponents' -type MockRequiredComponentsProvider_GetRequiredComponents_Call struct { - *mock.Call -} - -// GetRequiredComponents is a helper method to define mock.On call -// - ctx context.Context -func (_e *MockRequiredComponentsProvider_Expecter) GetRequiredComponents(ctx interface{}) *MockRequiredComponentsProvider_GetRequiredComponents_Call { - return &MockRequiredComponentsProvider_GetRequiredComponents_Call{Call: _e.mock.On("GetRequiredComponents", ctx)} -} - -func (_c *MockRequiredComponentsProvider_GetRequiredComponents_Call) Run(run func(ctx context.Context)) *MockRequiredComponentsProvider_GetRequiredComponents_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context)) - }) - return _c -} - -func (_c *MockRequiredComponentsProvider_GetRequiredComponents_Call) Return(_a0 []ecosystem.RequiredComponent, _a1 error) *MockRequiredComponentsProvider_GetRequiredComponents_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockRequiredComponentsProvider_GetRequiredComponents_Call) RunAndReturn(run func(context.Context) ([]ecosystem.RequiredComponent, error)) *MockRequiredComponentsProvider_GetRequiredComponents_Call { - _c.Call.Return(run) - return _c -} - -// NewMockRequiredComponentsProvider creates a new instance of MockRequiredComponentsProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewMockRequiredComponentsProvider(t interface { - mock.TestingT - Cleanup(func()) -}) *MockRequiredComponentsProvider { - mock := &MockRequiredComponentsProvider{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/domainservice/util.go b/pkg/domainservice/util.go index d1fffd50..a85a523c 100644 --- a/pkg/domainservice/util.go +++ b/pkg/domainservice/util.go @@ -3,6 +3,7 @@ package domainservice import ( "context" "errors" + cescommons "github.com/cloudogu/ces-commons-lib/dogu" "github.com/cloudogu/cesapp-lib/core" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" @@ -11,9 +12,13 @@ import ( func loadDoguSpecifications(ctx context.Context, remoteDoguRegistry RemoteDoguRegistry, wantedDogus []domain.Dogu) (map[cescommons.QualifiedName]*core.Dogu, error) { dogusToLoad := util.Map(wantedDogus, func(dogu domain.Dogu) cescommons.QualifiedVersion { + doguVersion := core.Version{} + if dogu.Version != nil { + doguVersion = *dogu.Version + } return cescommons.QualifiedVersion{ Name: dogu.Name, - Version: dogu.Version, + Version: doguVersion, } }) doguSpecsOfWantedDogus, err := remoteDoguRegistry.GetDogus(ctx, dogusToLoad) diff --git a/pkg/domainservice/validateAdditionalMountsDomainUseCase.go b/pkg/domainservice/validateAdditionalMountsDomainUseCase.go index efc1c9ef..5cbfde08 100644 --- a/pkg/domainservice/validateAdditionalMountsDomainUseCase.go +++ b/pkg/domainservice/validateAdditionalMountsDomainUseCase.go @@ -4,11 +4,12 @@ import ( "context" "errors" "fmt" + "slices" + "github.com/cloudogu/cesapp-lib/core" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/util" "sigs.k8s.io/controller-runtime/pkg/log" - "slices" ) type ValidateAdditionalMountsDomainUseCase struct { @@ -30,15 +31,15 @@ func (useCase *ValidateAdditionalMountsDomainUseCase) ValidateAdditionalMounts(c logger := log.FromContext(ctx).WithName("ValidateAdditionalMountsDomainUseCase.ValidateAdditionalMounts") dogusWithMounts := filterDogusWithAdditionalMounts(effectiveBlueprint.GetWantedDogus()) if len(dogusWithMounts) == 0 { - logger.Info("skip additional mounts validation as no dogus have additional mounts") + logger.V(2).Info("skip additional mounts validation as no dogus have additional mounts") return nil } - logger.Info("load dogu specifications...", "dogusWithMounts", dogusWithMounts) + logger.V(2).Info("load dogu specifications...", "dogusWithMounts", dogusWithMounts) doguSpecs, err := loadDoguSpecifications(ctx, useCase.remoteDoguRegistry, dogusWithMounts) if err != nil { return err } - logger.Info("dogu specifications loaded", "specs", doguSpecs) + logger.V(2).Info("dogu specifications loaded", "specs", doguSpecs) var errorList []error for _, wantedDogu := range dogusWithMounts { diff --git a/pkg/domainservice/validateAdditionalMountsDomainUseCase_test.go b/pkg/domainservice/validateAdditionalMountsDomainUseCase_test.go index d6011cc5..dfa074e0 100644 --- a/pkg/domainservice/validateAdditionalMountsDomainUseCase_test.go +++ b/pkg/domainservice/validateAdditionalMountsDomainUseCase_test.go @@ -2,13 +2,14 @@ package domainservice import ( _ "embed" + "testing" + "github.com/cloudogu/ces-commons-lib/dogu" "github.com/cloudogu/cesapp-lib/core" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "testing" ) //go:embed testdata/k8s-nginx-static-1-26-3-2.json @@ -35,9 +36,9 @@ func TestValidateAdditionalMountsDomainUseCase_ValidateAdditionalMounts(t *testi blueprint := domain.EffectiveBlueprint{ Dogus: []domain.Dogu{ { - Name: k8sNginxStatic, - Version: version1_26_3_2, - TargetState: domain.TargetStatePresent, + Name: k8sNginxStatic, + Version: &version1_26_3_2, + Absent: false, }, }, } @@ -54,9 +55,9 @@ func TestValidateAdditionalMountsDomainUseCase_ValidateAdditionalMounts(t *testi blueprint := domain.EffectiveBlueprint{ Dogus: []domain.Dogu{ { - Name: k8sNginxStatic, - Version: version1_26_3_2, - TargetState: domain.TargetStatePresent, + Name: k8sNginxStatic, + Version: &version1_26_3_2, + Absent: false, AdditionalMounts: []ecosystem.AdditionalMount{ { SourceType: ecosystem.DataSourceConfigMap, @@ -87,9 +88,9 @@ func TestValidateAdditionalMountsDomainUseCase_ValidateAdditionalMounts(t *testi blueprint := domain.EffectiveBlueprint{ Dogus: []domain.Dogu{ { - Name: k8sNginxStatic, - Version: version1_26_3_2, - TargetState: domain.TargetStatePresent, + Name: k8sNginxStatic, + Version: &version1_26_3_2, + Absent: false, AdditionalMounts: []ecosystem.AdditionalMount{ { SourceType: ecosystem.DataSourceConfigMap, @@ -123,9 +124,9 @@ func TestValidateAdditionalMountsDomainUseCase_ValidateAdditionalMounts(t *testi blueprint := domain.EffectiveBlueprint{ Dogus: []domain.Dogu{ { - Name: k8sNginxStatic, - Version: version1_26_3_2, - TargetState: domain.TargetStatePresent, + Name: k8sNginxStatic, + Version: &version1_26_3_2, + Absent: false, AdditionalMounts: []ecosystem.AdditionalMount{ { SourceType: ecosystem.DataSourceConfigMap, @@ -160,9 +161,9 @@ func TestValidateAdditionalMountsDomainUseCase_ValidateAdditionalMounts(t *testi blueprint := domain.EffectiveBlueprint{ Dogus: []domain.Dogu{ { - Name: k8sNginxStatic, - Version: version1_26_3_2, - TargetState: domain.TargetStatePresent, + Name: k8sNginxStatic, + Version: &version1_26_3_2, + Absent: false, AdditionalMounts: []ecosystem.AdditionalMount{ { SourceType: ecosystem.DataSourceConfigMap, diff --git a/pkg/domainservice/validateDependenciesDomainUseCase.go b/pkg/domainservice/validateDependenciesDomainUseCase.go index 13942864..b66ff44d 100644 --- a/pkg/domainservice/validateDependenciesDomainUseCase.go +++ b/pkg/domainservice/validateDependenciesDomainUseCase.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + cescommons "github.com/cloudogu/ces-commons-lib/dogu" "github.com/cloudogu/cesapp-lib/core" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" @@ -12,10 +13,8 @@ import ( ) const ( - nginxDependencyName = "nginx" - nginxStaticDependencyName = "nginx-static" - nginxIngressDependencyName = "nginx-ingress" - registratorDependencyName = "registrator" + nginxDependencyName = "nginx" + registratorDependencyName = "registrator" ) type ValidateDependenciesDomainUseCase struct { @@ -37,12 +36,16 @@ func (useCase *ValidateDependenciesDomainUseCase) ValidateDependenciesForAllDogu logger := log.FromContext(ctx).WithName("ValidateDependenciesDomainUseCase.ValidateDependenciesForAllDogus") wantedDogus := effectiveBlueprint.GetWantedDogus() dogusToLoad := util.Map(wantedDogus, func(dogu domain.Dogu) cescommons.QualifiedVersion { + doguVersion := core.Version{} + if dogu.Version != nil { + doguVersion = *dogu.Version + } return cescommons.QualifiedVersion{ Name: dogu.Name, - Version: dogu.Version, + Version: doguVersion, } }) - logger.Info("load dogu specifications...", "wantedDogus", wantedDogus) + logger.V(2).Info("load dogu specifications...", "wantedDogus", wantedDogus) doguSpecsOfWantedDogus, err := useCase.remoteDoguRegistry.GetDogus(ctx, dogusToLoad) if err != nil { var notFoundError *NotFoundError @@ -52,12 +55,12 @@ func (useCase *ValidateDependenciesDomainUseCase) ValidateDependenciesForAllDogu return &InternalError{WrappedError: err, Message: "cannot load dogu specifications from remote registry for dogu dependency validation"} } } - logger.Info("dogu specifications loaded", "specs", doguSpecsOfWantedDogus) + logger.V(2).Info("dogu specifications loaded", "specs", doguSpecsOfWantedDogus) var errorList []error for _, wantedDogu := range wantedDogus { dependencyDoguSpec := doguSpecsOfWantedDogus[wantedDogu.Name] - logger.Info(fmt.Sprintf("check dependencies of %q in version %q", wantedDogu.Name, wantedDogu.Version.Raw)) + logger.V(2).Info(fmt.Sprintf("check dependencies of %q in version %q", wantedDogu.Name, wantedDogu.Version.Raw)) err = useCase.checkDoguDependencies(ctx, wantedDogus, doguSpecsOfWantedDogus, dependencyDoguSpec.Dependencies) if err != nil { errorList = append(errorList, fmt.Errorf("dependencies for dogu '%s' are not satisfied in blueprint: %w", wantedDogu.Name, err)) @@ -83,9 +86,15 @@ func (useCase *ValidateDependenciesDomainUseCase) checkDoguDependencies( var problems []error for _, dependencyOfWantedDogu := range dependenciesOfWantedDogu { - logger.Info(fmt.Sprintf("check dependency %q in version %q...", dependencyOfWantedDogu.Name, dependencyOfWantedDogu.Version)) + logger.V(2).Info(fmt.Sprintf( + "check dependency %q in version %q...", + dependencyOfWantedDogu.Name, dependencyOfWantedDogu.Version, + )) if dependencyOfWantedDogu.Type != core.DependencyTypeDogu { - logger.Info(fmt.Sprintf("dogu has a dependency %q of type %q. At the moment only dogu dependencies are validated.", dependencyOfWantedDogu.Name, dependencyOfWantedDogu.Type)) + logger.V(1).Info(fmt.Sprintf( + "dogu has a dependency %q of type %q. At the moment only dogu dependencies are validated.", + dependencyOfWantedDogu.Name, dependencyOfWantedDogu.Type, + )) continue } @@ -95,12 +104,8 @@ func (useCase *ValidateDependenciesDomainUseCase) checkDoguDependencies( } // Exception for the old nginx dependency from the single node Cloudogu EcoSystem. - // We only have to check if nginx-static and nginx-ingress are present. + // The nginx dependency was replaced by a the k8s-ces-gateway and k8s-ces-assets component if dependencyOfWantedDogu.Name == nginxDependencyName { - if !checkNginxIngressAndStatic(wantedDogus) { - problems = append(problems, fmt.Errorf("dogu has %q dependency but %q and %q are missing in the effective blueprint", nginxDependencyName, nginxIngressDependencyName, nginxStaticDependencyName)) - } - logger.Info(fmt.Sprintf("dogu has dependency %q. %q and %q are available.", nginxDependencyName, nginxIngressDependencyName, nginxStaticDependencyName)) continue } @@ -112,23 +117,6 @@ func (useCase *ValidateDependenciesDomainUseCase) checkDoguDependencies( return err } -func checkNginxIngressAndStatic(wantedDogus []domain.Dogu) bool { - foundNginxIngress := isDoguInSlice(wantedDogus, nginxIngressDependencyName) - foundNginxStatic := isDoguInSlice(wantedDogus, nginxStaticDependencyName) - - return foundNginxIngress && foundNginxStatic -} - -func isDoguInSlice(dogus []domain.Dogu, name cescommons.SimpleName) bool { - for _, dogu := range dogus { - if dogu.Name.SimpleName == name { - return true - } - } - - return false -} - func checkDoguDependency( dependencyOfWantedDogu core.Dependency, wantedDogus []domain.Dogu, @@ -152,7 +140,11 @@ func checkDependencyVersion(doguInBlueprint domain.Dogu, expectedVersion string) if err != nil { return fmt.Errorf("failed to parse version comparator of version %s for dogu dependency %s: %w", expectedVersion, doguInBlueprint.Name, err) } - allows, err := comparator.Allows(doguInBlueprint.Version) + doguInBlueprintVersion := core.Version{} + if doguInBlueprint.Version != nil { + doguInBlueprintVersion = *doguInBlueprint.Version + } + allows, err := comparator.Allows(doguInBlueprintVersion) if err != nil { return fmt.Errorf("an error occurred when comparing the versions: %w", err) } diff --git a/pkg/domainservice/validateDependenciesDomainUseCase_test.go b/pkg/domainservice/validateDependenciesDomainUseCase_test.go index 9e8af9df..dfb84b98 100644 --- a/pkg/domainservice/validateDependenciesDomainUseCase_test.go +++ b/pkg/domainservice/validateDependenciesDomainUseCase_test.go @@ -2,13 +2,14 @@ package domainservice import ( "context" + "testing" + cescommons "github.com/cloudogu/ces-commons-lib/dogu" "github.com/cloudogu/cesapp-lib/core" "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "testing" ) var ( @@ -49,14 +50,14 @@ func Test_checkDependencyVersion(t *testing.T) { args args wantErr bool }{ - {name: "exact version", args: args{doguInBlueprint: domain.Dogu{Name: officialNginx, Version: version2_0_0_1}, expectedVersion: "2.0.0-1"}, wantErr: false}, - {name: "has lower version", args: args{doguInBlueprint: domain.Dogu{Name: officialNginx, Version: version2_0_0_1}, expectedVersion: ">=2.0.0-2"}, wantErr: true}, - {name: "has higher version", args: args{doguInBlueprint: domain.Dogu{Name: officialNginx, Version: version2_0_0_3}, expectedVersion: ">=2.0.0-2"}, wantErr: false}, - {name: "needs lower version", args: args{doguInBlueprint: domain.Dogu{Name: officialNginx, Version: version2_0_0_3}, expectedVersion: "<=2.0.0-2"}, wantErr: true}, - {name: "needs higher version", args: args{doguInBlueprint: domain.Dogu{Name: officialNginx, Version: version2_0_0_1}, expectedVersion: ">2.0.0-1"}, wantErr: true}, - {name: "no constraint", args: args{doguInBlueprint: domain.Dogu{Name: officialNginx, Version: version2_0_0_1}, expectedVersion: ""}, wantErr: false}, - {name: "not parsable expected version", args: args{doguInBlueprint: domain.Dogu{Name: officialNginx, Version: version2_0_0_1}, expectedVersion: "abc"}, wantErr: true}, - {name: "not parsable actual version", args: args{doguInBlueprint: domain.Dogu{Name: officialNginx, Version: core.Version{Raw: "abc"}}, expectedVersion: "2.0.0-1"}, wantErr: true}, + {name: "exact version", args: args{doguInBlueprint: domain.Dogu{Name: officialNginx, Version: &version2_0_0_1}, expectedVersion: "2.0.0-1"}, wantErr: false}, + {name: "has lower version", args: args{doguInBlueprint: domain.Dogu{Name: officialNginx, Version: &version2_0_0_1}, expectedVersion: ">=2.0.0-2"}, wantErr: true}, + {name: "has higher version", args: args{doguInBlueprint: domain.Dogu{Name: officialNginx, Version: &version2_0_0_3}, expectedVersion: ">=2.0.0-2"}, wantErr: false}, + {name: "needs lower version", args: args{doguInBlueprint: domain.Dogu{Name: officialNginx, Version: &version2_0_0_3}, expectedVersion: "<=2.0.0-2"}, wantErr: true}, + {name: "needs higher version", args: args{doguInBlueprint: domain.Dogu{Name: officialNginx, Version: &version2_0_0_1}, expectedVersion: ">2.0.0-1"}, wantErr: true}, + {name: "no constraint", args: args{doguInBlueprint: domain.Dogu{Name: officialNginx, Version: &version2_0_0_1}, expectedVersion: ""}, wantErr: false}, + {name: "not parsable expected version", args: args{doguInBlueprint: domain.Dogu{Name: officialNginx, Version: &version2_0_0_1}, expectedVersion: "abc"}, wantErr: true}, + {name: "not parsable actual version", args: args{doguInBlueprint: domain.Dogu{Name: officialNginx, Version: &core.Version{Raw: "abc"}}, expectedVersion: "2.0.0-1"}, wantErr: true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -82,8 +83,8 @@ func TestValidateDependenciesDomainUseCase_ValidateDependenciesForAllDogus(t *te args: args{ effectiveBlueprint: domain.EffectiveBlueprint{ Dogus: []domain.Dogu{ - {Name: officialRedmine, Version: version1_0_0_1}, - {Name: officialPostgres, Version: version1_0_0_1}, + {Name: officialRedmine, Version: &version1_0_0_1}, + {Name: officialPostgres, Version: &version1_0_0_1}, }, }, }, @@ -94,8 +95,8 @@ func TestValidateDependenciesDomainUseCase_ValidateDependenciesForAllDogus(t *te args: args{ effectiveBlueprint: domain.EffectiveBlueprint{ Dogus: []domain.Dogu{ - {Name: officialRedmine, Version: version1_0_0_1}, - {Name: officialPostgres, Version: version1_0_0_1}, + {Name: officialRedmine, Version: &version1_0_0_1}, + {Name: officialPostgres, Version: &version1_0_0_1}, }, }, }, @@ -106,7 +107,7 @@ func TestValidateDependenciesDomainUseCase_ValidateDependenciesForAllDogus(t *te args: args{ effectiveBlueprint: domain.EffectiveBlueprint{ Dogus: []domain.Dogu{ - {Name: officialRedmine, Version: version1_0_0_1}, + {Name: officialRedmine, Version: &version1_0_0_1}, }, }, }, @@ -118,7 +119,7 @@ func TestValidateDependenciesDomainUseCase_ValidateDependenciesForAllDogus(t *te args: args{ effectiveBlueprint: domain.EffectiveBlueprint{ Dogus: []domain.Dogu{ - {Name: officialRedmine2, Version: version1_0_0_1}, + {Name: officialRedmine2, Version: &version1_0_0_1}, }, }, }, @@ -130,7 +131,7 @@ func TestValidateDependenciesDomainUseCase_ValidateDependenciesForAllDogus(t *te args: args{ effectiveBlueprint: domain.EffectiveBlueprint{ Dogus: []domain.Dogu{ - {Name: officialK8sCesControl, Version: version1_0_0_1}, + {Name: officialK8sCesControl, Version: &version1_0_0_1}, }, }, }, @@ -140,7 +141,7 @@ func TestValidateDependenciesDomainUseCase_ValidateDependenciesForAllDogus(t *te name: "missing nginx-static and nginx ingress on nginx dependency", args: args{effectiveBlueprint: domain.EffectiveBlueprint{ Dogus: []domain.Dogu{ - {Name: officialScm, Version: version1_0_0_1}, + {Name: officialScm, Version: &version1_0_0_1}, }, }}, wantErr: true, @@ -149,9 +150,9 @@ func TestValidateDependenciesDomainUseCase_ValidateDependenciesForAllDogus(t *te name: "ok with nginx dependency", args: args{effectiveBlueprint: domain.EffectiveBlueprint{ Dogus: []domain.Dogu{ - {Name: officialPlantuml, Version: version1_0_0_1}, - {Name: k8sNginxStatic, Version: version1_0_0_1}, - {Name: k8sNginxIngress, Version: version1_0_0_1}, + {Name: officialPlantuml, Version: &version1_0_0_1}, + {Name: k8sNginxStatic, Version: &version1_0_0_1}, + {Name: k8sNginxIngress, Version: &version1_0_0_1}, }, }}, wantErr: false, @@ -160,7 +161,7 @@ func TestValidateDependenciesDomainUseCase_ValidateDependenciesForAllDogus(t *te name: "registrator should be ignored", args: args{effectiveBlueprint: domain.EffectiveBlueprint{ Dogus: []domain.Dogu{ - {Name: ldapMapper, Version: version1_0_0_1}, + {Name: ldapMapper, Version: &version1_0_0_1}, }, }}, wantErr: false, @@ -189,7 +190,7 @@ func TestValidateDependenciesDomainUseCase_ValidateDependenciesForAllDogus_NotFo // when err := useCase.ValidateDependenciesForAllDogus(ctx, domain.EffectiveBlueprint{ Dogus: []domain.Dogu{ - {Name: officialUnknownDogu, Version: version1_0_0_1}, + {Name: officialUnknownDogu, Version: &version1_0_0_1}, }, }) // then @@ -218,8 +219,8 @@ func TestValidateDependenciesDomainUseCase_ValidateDependenciesForAllDogus_colle // when err := useCase.ValidateDependenciesForAllDogus(ctx, domain.EffectiveBlueprint{ Dogus: []domain.Dogu{ - {Name: officialRedmine, Version: version1_0_0_1}, - {Name: helloworldBluespice, Version: version1_0_0_1}, + {Name: officialRedmine, Version: &version1_0_0_1}, + {Name: helloworldBluespice, Version: &version1_0_0_1}, }, }) // then