From 9ad0e9a1675af3dde613cdfb60c370d5ca6c128f Mon Sep 17 00:00:00 2001 From: Pavan Date: Fri, 18 Aug 2023 11:40:57 +0200 Subject: [PATCH] CAP Operator: Initial open source version created Initial Open Source version Change-Id: I51258701067d3b05b9a79dbff0d37101c2dac1e8 --- .dockerignore | 4 + .github/workflows/go.yml | 32 + .github/workflows/publish-website.yaml | 79 + .gitignore | 24 + .reuse/dep5 | 20 +- .vscode/launch.json | 15 + LICENSE | 27 +- README.md | 32 +- build/controller/Dockerfile | 15 + build/mtx-job/Dockerfile | 15 + build/server/Dockerfile | 15 + build/web-hooks/Dockerfile | 15 + cmd/controller/main.go | 129 + cmd/controller/temp.go | 371 +++ cmd/mtx-job/main.go | 543 ++++ cmd/mtx-job/main_test.go | 510 ++++ cmd/server/internal/client.go | 19 + cmd/server/internal/handler.go | 585 +++++ cmd/server/internal/handler_test.go | 663 +++++ cmd/server/internal/jwt.go | 235 ++ cmd/server/internal/jwt_test.go | 360 +++ .../internal/testdata/auth.service.local.crt | 30 + .../internal/testdata/auth.service.local.key | 28 + cmd/server/internal/testdata/rootCA.pem | 23 + cmd/server/server.go | 71 + cmd/web-hooks/internal/handler/handler.go | 629 +++++ .../internal/handler/handler_test.go | 1164 +++++++++ cmd/web-hooks/main.go | 77 + crds/capapplication.yaml | 133 + crds/capapplicationversion.yaml | 755 ++++++ crds/captenant.yaml | 95 + crds/captenantoperation.yaml | 106 + go.mod | 79 + go.sum | 251 ++ hack/LICENSE_BOILERPLATE.txt | 4 + hack/api-reference/config.json | 20 + hack/api-reference/generate.sh | 14 + hack/api-reference/template/members.tpl | 48 + hack/api-reference/template/pkg.tpl | 49 + hack/api-reference/template/placeholder.go | 2 + hack/api-reference/template/type.tpl | 81 + hack/generate.sh | 65 + hack/helm-reference/generate.sh | 22 + hack/tools.go | 8 + internal/controller/common_test.go | 638 +++++ internal/controller/controller.go | 281 +++ internal/controller/controller_test.go | 292 +++ internal/controller/informers.go | 244 ++ internal/controller/informers_test.go | 149 ++ .../controller/reconcile-capapplication.go | 408 +++ .../reconcile-capapplication_test.go | 1128 +++++++++ .../reconcile-capapplicationversion.go | 838 +++++++ .../reconcile-capapplicationversion_test.go | 540 ++++ internal/controller/reconcile-captenant.go | 697 +++++ .../controller/reconcile-captenant_test.go | 595 +++++ .../reconcile-captenantoperation.go | 682 +++++ .../reconcile-captenantoperation_test.go | 655 +++++ internal/controller/reconcile-domains.go | 1274 ++++++++++ internal/controller/reconcile-domains_test.go | 284 +++ internal/controller/reconcile.go | 523 ++++ internal/controller/reconcile_test.go | 585 +++++ internal/controller/reconciliation-result.go | 46 + .../capapplication/ca-01.expected.yaml | 42 + .../capapplication/ca-01.initial.yaml | 38 + .../capapplication/ca-02.expected.yaml | 49 + .../capapplication/ca-02.initial.yaml | 42 + .../capapplication/ca-03.expected.yaml | 52 + .../capapplication/ca-03.initial.yaml | 42 + .../capapplication/ca-04.expected.yaml | 50 + .../capapplication/ca-04.initial.yaml | 43 + .../capapplication/ca-05.expected.yaml | 52 + .../capapplication/ca-05.initial.yaml | 42 + .../capapplication/ca-06.expected.yaml | 57 + .../capapplication/ca-06.initial.yaml | 43 + .../capapplication/ca-07.expected.yaml | 49 + .../capapplication/ca-07.initial.yaml | 42 + .../capapplication/ca-08.expected.yaml | 49 + .../capapplication/ca-08.initial.yaml | 42 + .../capapplication/ca-09.expected.yaml | 50 + .../capapplication/ca-09.initial.yaml | 43 + .../capapplication/ca-10.expected.yaml | 50 + .../capapplication/ca-10.initial.yaml | 43 + .../capapplication/ca-11.expected.yaml | 50 + .../capapplication/ca-11.initial.yaml | 43 + .../capapplication/ca-12.expected.yaml | 50 + .../capapplication/ca-12.initial.yaml | 43 + .../capapplication/ca-13.expected.yaml | 42 + .../capapplication/ca-13.initial.yaml | 40 + .../capapplication/ca-14.expected.yaml | 41 + .../capapplication/ca-14.initial.yaml | 41 + .../capapplication/ca-15.expected.yaml | 50 + .../capapplication/ca-15.initial.yaml | 43 + .../capapplication/ca-16.expected.yaml | 46 + .../capapplication/ca-16.initial.yaml | 47 + .../capapplication/ca-17.expected.yaml | 47 + .../capapplication/ca-17.initial.yaml | 47 + .../capapplication/ca-18.expected.yaml | 47 + .../capapplication/ca-18.initial.yaml | 47 + .../capapplication/ca-19.expected.yaml | 46 + .../capapplication/ca-19.initial.yaml | 47 + .../capapplication/ca-20.expected.yaml | 47 + .../capapplication/ca-20.initial.yaml | 47 + .../capapplication/ca-21.expected.yaml | 50 + .../capapplication/ca-21.initial.yaml | 43 + .../capapplication/ca-22.expected.yaml | 53 + .../capapplication/ca-22.initial.yaml | 42 + .../capapplication/ca-23.expected.yaml | 58 + .../capapplication/ca-23.initial.yaml | 43 + .../capapplication/ca-24.expected.yaml | 50 + .../capapplication/ca-24.initial.yaml | 42 + .../capapplication/ca-25.expected.yaml | 50 + .../capapplication/ca-25.initial.yaml | 43 + .../capapplication/ca-26.expected.yaml | 50 + .../capapplication/ca-26.initial.yaml | 43 + .../capapplication/ca-27.expected.yaml | 50 + .../capapplication/ca-27.initial.yaml | 43 + .../capapplication/ca-28.expected.yaml | 50 + .../capapplication/ca-28.initial.yaml | 43 + .../capapplication/ca-29.expected.yaml | 55 + .../capapplication/ca-29.initial.yaml | 52 + .../capapplication/ca-30.expected.yaml | 50 + .../capapplication/ca-30.initial.yaml | 45 + .../capapplication/ca-31.expected.yaml | 58 + .../capapplication/ca-31.initial.yaml | 44 + .../capapplication/ca-32.expected.yaml | 58 + .../capapplication/ca-32.initial.yaml | 44 + .../capapplication/ca-33.expected.yaml | 58 + .../capapplication/ca-33.initial.yaml | 44 + .../capapplication/ca-34.expected.yaml | 43 + .../capapplication/ca-34.initial.yaml | 41 + .../capapplication/ca-35.expected.yaml | 47 + .../capapplication/ca-35.initial.yaml | 46 + .../capapplication/ca-36.expected.yaml | 50 + .../capapplication/ca-36.initial.yaml | 44 + .../capapplication/ca-37.expected.yaml | 48 + .../capapplication/ca-37.initial.yaml | 48 + .../capapplication/ca-38.expected.yaml | 49 + .../capapplication/ca-38.initial.yaml | 48 + .../capapplication/ca-39.expected.yaml | 49 + .../capapplication/ca-39.initial.yaml | 48 + .../capapplication/ca-40.expected.yaml | 48 + .../capapplication/ca-40.initial.yaml | 48 + .../capapplication/ca-41.expected.yaml | 49 + .../capapplication/ca-41.initial.yaml | 48 + .../capapplication/ca-42.expected.yaml | 48 + .../capapplication/ca-42.initial.yaml | 42 + .../capapplication/ca-43.expected.yaml | 50 + .../capapplication/ca-43.initial.yaml | 48 + .../capapplication/ca-44.expected.yaml | 49 + .../capapplication/ca-45.expected.yaml | 55 + .../capapplication/ca-45.initial.yaml | 52 + .../testdata/capapplication/ca-dns-error.yaml | 27 + .../capapplication/ca-dns-not-ready.yaml | 23 + .../testdata/capapplication/ca-dns.yaml | 26 + .../cat-consumer-no-finalizers-ready.yaml | 35 + .../capapplication/cat-consumer-ready.yaml | 38 + .../cat-consumer-upg-never-deleting.yaml | 37 + .../cat-consumer-upg-never-ready.yaml | 37 + .../capapplication/cat-provider-error.yaml | 37 + .../cat-provider-no-finalizers-error.yaml | 35 + .../cat-provider-no-finalizers-ready.yaml | 35 + .../cat-provider-upgrade-error.yaml | 37 + .../cav-33-version-updated-ready.yaml | 67 + .../testdata/capapplication/cav-error.yaml | 67 + .../cav-name-modified-ready.yaml | 67 + .../testdata/capapplication/gateway.yaml | 30 + .../istio-ingress-with-cert-error.yaml | 65 + ...istio-ingress-with-cert-no-finalizers.yaml | 62 + .../istio-ingress-with-cert.yaml | 64 + .../istio-ingress-with-certManager-error.yaml | 68 + ...ngress-with-certManager-no-finalizers.yaml | 66 + .../istio-ingress-with-certManager.yaml | 68 + .../istio-ingress-with-no-cert.yaml | 46 + .../cat-provider-version.yaml | 31 + .../cav-annotations.yaml | 170 ++ .../cav-cluster-netpol-port.yaml | 153 ++ .../cav-custom-destination-config.yaml | 71 + .../cav-custom-labels.yaml | 101 + .../cav-empty-status.yaml | 48 + .../cav-error-condition-status.yaml | 55 + .../cav-error-status.yaml | 50 + .../cav-failed-content-job.yaml | 70 + .../capapplicationversion/cav-invalid-ca.yaml | 44 + .../cav-invalid-env-cap.yaml | 69 + .../cav-invalid-env-content.yaml | 63 + .../cav-invalid-env-job-worker.yaml | 80 + .../cav-merged-destinations-router.yaml | 66 + .../cav-pod-security-context.yaml | 167 ++ .../cav-probes-and-resources.yaml | 143 ++ .../cav-processing-job-finished.yaml | 67 + .../capapplicationversion/cav-processing.yaml | 66 + .../cav-ready-deleting.yaml | 68 + .../cav-security-context.yaml | 156 ++ .../cav-unknown-deleting.yaml | 59 + .../cav-valid-env-config.yaml | 91 + .../content-job-completed.yaml | 71 + .../content-job-failed.yaml | 72 + .../content-job-pending.yaml | 69 + .../expected/cav-deleted-unknown.yaml | 67 + .../expected/cav-deleted.yaml | 68 + .../expected/cav-deleting.yaml | 69 + .../cav-error-condition-processing.yaml | 68 + .../expected/cav-error-processing.yaml | 68 + .../expected/cav-failed-content-job.yaml | 71 + .../expected/cav-failed-env-cap.yaml | 78 + .../expected/cav-failed-env-content.yaml | 72 + .../expected/cav-failed-env-job-worker.yaml | 89 + .../expected/cav-missing-content-job.yaml | 69 + .../expected/cav-missing-secrets.yaml | 68 + .../expected/cav-processing.yaml | 68 + .../expected/cav-ready-annotations.yaml | 449 ++++ .../expected/cav-ready-app-netpol.yaml | 308 +++ .../cav-ready-cluster-netpol-port.yaml | 349 +++ .../expected/cav-ready-content-job.yaml | 70 + .../cav-ready-custom-destination-config.yaml | 153 ++ .../cav-ready-custom-labels-config.yaml | 189 ++ .../cav-ready-merged-destinations-router.yaml | 142 ++ .../cav-ready-pod-security-context.yaml | 399 +++ .../cav-ready-probes-and-resources.yaml | 248 ++ .../expected/cav-ready-security-context.yaml | 355 +++ .../expected/cav-ready-valid-env-config.yaml | 174 ++ .../expected/cav-ready.yaml | 68 + .../testdata/captenant/cat-01.initial.yaml | 11 + .../testdata/captenant/cat-02.expected.yaml | 36 + .../testdata/captenant/cat-02.initial.yaml | 11 + .../testdata/captenant/cat-03.expected.yaml | 89 + .../testdata/captenant/cat-03.initial.yaml | 36 + .../testdata/captenant/cat-04.expected.yaml | 99 + .../testdata/captenant/cat-04.initial.yaml | 72 + .../testdata/captenant/cat-05.expected.yaml | 36 + .../testdata/captenant/cat-05.initial.yaml | 72 + .../testdata/captenant/cat-06.expected.yaml | 36 + .../testdata/captenant/cat-06.initial.yaml | 69 + .../testdata/captenant/cat-07.expected.yaml | 67 + .../testdata/captenant/cat-07.initial.yaml | 37 + .../testdata/captenant/cat-08.expected.yaml | 37 + .../testdata/captenant/cat-08.initial.yaml | 37 + .../testdata/captenant/cat-09.expected.yaml | 67 + .../testdata/captenant/cat-09.initial.yaml | 38 + .../testdata/captenant/cat-10.expected.yaml | 37 + .../testdata/captenant/cat-10.initial.yaml | 74 + .../testdata/captenant/cat-11.expected.yaml | 36 + .../testdata/captenant/cat-11.initial.yaml | 37 + .../testdata/captenant/cat-13.expected.yaml | 105 + .../testdata/captenant/cat-13.initial.yaml | 75 + .../testdata/captenant/cat-14.initial.yaml | 29 + .../testdata/captenant/cat-15.expected.yaml | 102 + .../testdata/captenant/cat-15.initial.yaml | 74 + .../testdata/captenant/cat-16.expected.yaml | 74 + .../testdata/captenant/cat-17.expected.yaml | 124 + .../testdata/captenant/cat-17.initial.yaml | 101 + .../testdata/captenant/cat-20.expected.yaml | 36 + .../testdata/captenant/cat-20.initial.yaml | 36 + .../testdata/captenant/cat-21.expected.yaml | 78 + .../testdata/captenant/cat-21.initial.yaml | 109 + .../testdata/captenant/cat-22.initial.yaml | 106 + .../testdata/captenant/cat-23.expected.yaml | 40 + .../testdata/captenant/cat-23.initial.yaml | 75 + .../testdata/captenant/cat-24.expected.yaml | 70 + .../testdata/captenant/cat-24.initial.yaml | 74 + .../testdata/captenant/cat-25.expected.yaml | 70 + .../testdata/captenant/cat-25.initial.yaml | 39 + .../testdata/captenant/cat-26.initial.yaml | 72 + .../testdata/captenant/cat-27.expected.yaml | 67 + .../testdata/captenant/cat-28.expected.yaml | 91 + .../testdata/captenant/cat-28.initial.yaml | 36 + .../testdata/captenant/cat-29.expected.yaml | 44 + .../testdata/captenant/cat-29.initial.yaml | 79 + .../captenant/cat-with-no-version.yaml | 35 + .../changed-provider-tenant-dnsentry.yaml | 27 + .../provider-tenant-dnsentry-not-ready.yaml | 27 + .../captenant/provider-tenant-dnsentry.yaml | 27 + .../captenant/provider-tenant-dr-v1.yaml | 27 + .../captenant/provider-tenant-vs-v1.yaml | 34 + ...o-be-updated-provider-tenant-dnsentry.yaml | 27 + .../captenantoperation/ctop-01.expected.yaml | 30 + .../captenantoperation/ctop-01.initial.yaml | 13 + .../captenantoperation/ctop-02.expected.yaml | 31 + .../captenantoperation/ctop-02.initial.yaml | 30 + .../captenantoperation/ctop-03.expected.yaml | 34 + .../captenantoperation/ctop-03.initial.yaml | 28 + .../captenantoperation/ctop-04.expected.yaml | 129 + .../captenantoperation/ctop-04.initial.yaml | 32 + .../captenantoperation/ctop-05.expected.yaml | 31 + .../captenantoperation/ctop-05.initial.yaml | 13 + .../captenantoperation/ctop-06.expected.yaml | 134 + .../captenantoperation/ctop-06.initial.yaml | 33 + .../captenantoperation/ctop-07.expected.yaml | 114 + .../captenantoperation/ctop-07.initial.yaml | 37 + .../captenantoperation/ctop-08.expected.yaml | 119 + .../captenantoperation/ctop-08.initial.yaml | 119 + .../captenantoperation/ctop-09.expected.yaml | 138 + .../captenantoperation/ctop-09.initial.yaml | 42 + .../captenantoperation/ctop-10.expected.yaml | 42 + .../captenantoperation/ctop-10.initial.yaml | 141 ++ .../captenantoperation/ctop-11.expected.yaml | 42 + .../captenantoperation/ctop-11.initial.yaml | 118 + .../captenantoperation/ctop-12.expected.yaml | 44 + .../captenantoperation/ctop-12.initial.yaml | 119 + .../captenantoperation/ctop-13.expected.yaml | 43 + .../captenantoperation/ctop-13.initial.yaml | 142 ++ .../captenantoperation/ctop-14.expected.yaml | 42 + .../captenantoperation/ctop-14.initial.yaml | 43 + .../captenantoperation/ctop-15.expected.yaml | 16 + .../captenantoperation/ctop-15.initial.yaml | 13 + .../captenantoperation/ctop-16.expected.yaml | 129 + .../captenantoperation/ctop-16.initial.yaml | 32 + .../captenantoperation/ctop-17.expected.yaml | 129 + .../captenantoperation/ctop-17.initial.yaml | 128 + .../captenantoperation/ctop-18.expected.yaml | 110 + .../captenantoperation/ctop-18.initial.yaml | 32 + .../captenantoperation/ctop-19.expected.yaml | 109 + .../captenantoperation/ctop-19.initial.yaml | 32 + .../captenantoperation/ctop-20.expected.yaml | 109 + .../captenantoperation/ctop-20.initial.yaml | 32 + .../captenantoperation/ctop-21.expected.yaml | 115 + .../captenantoperation/ctop-21.initial.yaml | 32 + .../captenantoperation/ctop-22.expected.yaml | 118 + .../captenantoperation/ctop-22.initial.yaml | 32 + .../captenantoperation/ctop-23.expected.yaml | 119 + .../captenantoperation/ctop-23.initial.yaml | 37 + .../captenantoperation/ctop-24.expected.yaml | 110 + .../captenantoperation/ctop-24.initial.yaml | 32 + .../captenantoperation/ctop-25.expected.yaml | 123 + .../captenantoperation/ctop-25.initial.yaml | 37 + .../captenantoperation/ctop-26.expected.yaml | 134 + .../captenantoperation/ctop-26.initial.yaml | 33 + .../common/capapplication-multi-xsuaa.yaml | 41 + .../testdata/common/capapplication.yaml | 39 + .../capapplicationversion-invalid-env.yaml | 78 + ...applicationversion-v1-mtxs-custom-cmd.yaml | 72 + .../common/capapplicationversion-v1-mtxs.yaml | 68 + .../capapplicationversion-v1-not-ready.yaml | 65 + .../capapplicationversion-v1-resources.yaml | 74 + ...pplicationversion-v1-security-context.yaml | 77 + .../common/capapplicationversion-v1.yaml | 70 + .../capapplicationversion-v2-annotations.yaml | 104 + .../capapplicationversion-v2-multi-xsuaa.yaml | 78 + ...icationversion-v2-multiple-tenant-ops.yaml | 98 + ...applicationversion-v2-no-mtx-workload.yaml | 63 + ...cationversion-v2-pod-security-context.yaml | 101 + .../common/capapplicationversion-v2.yaml | 66 + .../common/capapplicationversion-v3.yaml | 66 + .../common/captenant-provider-ready.yaml | 37 + .../captenant-provider-upgraded-ready.yaml | 37 + .../testdata/common/credential-secrets.yaml | 35 + .../testdata/common/istio-ingress.yaml | 46 + .../testdata/common/operator-gateway.yaml | 26 + internal/controller/utils.go | 222 ++ internal/util/config.go | 59 + internal/util/types.go | 42 + internal/util/uaa.go | 40 + internal/util/uaa_test.go | 97 + internal/util/vcap-credentials.go | 153 ++ internal/util/vcap-credentials_test.go | 215 ++ pkg/apis/sme.sap.com/v1alpha1/doc.go | 8 + pkg/apis/sme.sap.com/v1alpha1/register.go | 57 + pkg/apis/sme.sap.com/v1alpha1/types.go | 475 ++++ pkg/apis/sme.sap.com/v1alpha1/utils.go | 93 + .../v1alpha1/zz_generated.deepcopy.go | 793 ++++++ .../applyconfiguration/internal/internal.go | 50 + .../v1alpha1/applicationdomains.go | 61 + .../sme.sap.com/v1alpha1/btp.go | 32 + .../v1alpha1/btptenantidentification.go | 36 + .../sme.sap.com/v1alpha1/capapplication.go | 207 ++ .../v1alpha1/capapplicationspec.go | 63 + .../v1alpha1/capapplicationstatus.go | 69 + .../v1alpha1/capapplicationversion.go | 207 ++ .../v1alpha1/capapplicationversionspec.go | 70 + .../v1alpha1/capapplicationversionstatus.go | 62 + .../sme.sap.com/v1alpha1/captenant.go | 207 ++ .../v1alpha1/captenantoperation.go | 207 ++ .../v1alpha1/captenantoperationspec.go | 71 + .../v1alpha1/captenantoperationstatus.go | 69 + .../v1alpha1/captenantoperationstep.go | 49 + .../sme.sap.com/v1alpha1/captenantspec.go | 66 + .../sme.sap.com/v1alpha1/captenantstatus.go | 80 + .../sme.sap.com/v1alpha1/containerdetails.go | 89 + .../sme.sap.com/v1alpha1/deploymentdetails.go | 134 + .../sme.sap.com/v1alpha1/genericstatus.go | 42 + .../sme.sap.com/v1alpha1/jobdetails.go | 111 + .../sme.sap.com/v1alpha1/namevalue.go | 36 + .../sme.sap.com/v1alpha1/ports.go | 67 + .../sme.sap.com/v1alpha1/serviceinfo.go | 45 + .../sme.sap.com/v1alpha1/tenantoperations.go | 60 + .../tenantoperationworkloadreference.go | 36 + .../sme.sap.com/v1alpha1/workloaddetails.go | 86 + pkg/client/applyconfiguration/utils.go | 75 + pkg/client/clientset/versioned/clientset.go | 108 + .../versioned/fake/clientset_generated.go | 73 + pkg/client/clientset/versioned/fake/doc.go | 8 + .../clientset/versioned/fake/register.go | 44 + pkg/client/clientset/versioned/scheme/doc.go | 8 + .../clientset/versioned/scheme/register.go | 44 + .../sme.sap.com/v1alpha1/capapplication.go | 244 ++ .../v1alpha1/capapplicationversion.go | 244 ++ .../typed/sme.sap.com/v1alpha1/captenant.go | 244 ++ .../v1alpha1/captenantoperation.go | 244 ++ .../typed/sme.sap.com/v1alpha1/doc.go | 8 + .../typed/sme.sap.com/v1alpha1/fake/doc.go | 8 + .../v1alpha1/fake/fake_capapplication.go | 177 ++ .../fake/fake_capapplicationversion.go | 177 ++ .../v1alpha1/fake/fake_captenant.go | 177 ++ .../v1alpha1/fake/fake_captenantoperation.go | 177 ++ .../v1alpha1/fake/fake_sme.sap.com_client.go | 40 + .../v1alpha1/generated_expansion.go | 15 + .../v1alpha1/sme.sap.com_client.go | 110 + .../informers/externalversions/factory.go | 239 ++ .../informers/externalversions/generic.go | 56 + .../internalinterfaces/factory_interfaces.go | 28 + .../externalversions/sme.sap.com/interface.go | 34 + .../sme.sap.com/v1alpha1/capapplication.go | 78 + .../v1alpha1/capapplicationversion.go | 78 + .../sme.sap.com/v1alpha1/captenant.go | 78 + .../v1alpha1/captenantoperation.go | 78 + .../sme.sap.com/v1alpha1/interface.go | 54 + .../sme.sap.com/v1alpha1/capapplication.go | 87 + .../v1alpha1/capapplicationversion.go | 87 + .../listers/sme.sap.com/v1alpha1/captenant.go | 87 + .../v1alpha1/captenantoperation.go | 87 + .../v1alpha1/expansion_generated.go | 39 + website/archetypes/default.md | 6 + website/content/en/_index.md | 17 + website/content/en/docs/_index.md | 25 + website/content/en/docs/concepts/_index.md | 16 + .../concepts/cap-application-components.md | 30 + .../concepts/operator-components/_index.md | 23 + .../operator-components/controller.md | 23 + .../concepts/operator-components/mtx-job.md | 16 + .../subscription-server.md | 26 + .../content/en/docs/configuration/_index.md | 20 + .../content/en/docs/installation/_index.md | 10 + .../en/docs/installation/helm-install.md | 56 + .../en/docs/installation/prerequisites.md | 30 + website/content/en/docs/reference/_index.md | 10 + website/content/en/docs/support/_index.md | 19 + .../content/en/docs/troubleshoot/_index.md | 80 + website/content/en/docs/usage/_index.md | 8 + .../en/docs/usage/deploying-application.md | 127 + .../content/en/docs/usage/prerequisites.md | 105 + .../content/en/docs/usage/resources/_index.md | 8 + .../en/docs/usage/resources/capapplication.md | 67 + .../usage/resources/capapplicationversion.md | 318 +++ .../en/docs/usage/resources/captenant.md | 32 + .../usage/resources/captenantoperation.md | 44 + .../en/docs/usage/tenant-provisioning.md | 65 + .../content/en/docs/usage/version-upgrade.md | 120 + website/go.mod | 5 + website/go.sum | 5 + website/hugo.yaml | 167 ++ website/includes/api-reference.html | 2234 +++++++++++++++++ website/includes/chart-values.md | 67 + website/layouts/shortcodes/include.html | 1 + website/package-lock.json | 1611 ++++++++++++ website/package.json | 7 + .../img/activity-tenantprovisioning.png | Bin 0 -> 113312 bytes website/static/img/block-cluster.png | Bin 0 -> 73905 bytes website/static/img/block-controller.png | Bin 0 -> 40858 bytes website/static/img/block-subscription.png | Bin 0 -> 88912 bytes website/static/img/logo.png | Bin 0 -> 7522 bytes website/static/img/workflow.png | Bin 0 -> 256756 bytes 461 files changed, 49705 insertions(+), 62 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/workflows/go.yml create mode 100644 .github/workflows/publish-website.yaml create mode 100644 .gitignore create mode 100644 .vscode/launch.json create mode 100644 build/controller/Dockerfile create mode 100644 build/mtx-job/Dockerfile create mode 100644 build/server/Dockerfile create mode 100644 build/web-hooks/Dockerfile create mode 100644 cmd/controller/main.go create mode 100644 cmd/controller/temp.go create mode 100644 cmd/mtx-job/main.go create mode 100644 cmd/mtx-job/main_test.go create mode 100644 cmd/server/internal/client.go create mode 100644 cmd/server/internal/handler.go create mode 100644 cmd/server/internal/handler_test.go create mode 100644 cmd/server/internal/jwt.go create mode 100644 cmd/server/internal/jwt_test.go create mode 100644 cmd/server/internal/testdata/auth.service.local.crt create mode 100644 cmd/server/internal/testdata/auth.service.local.key create mode 100644 cmd/server/internal/testdata/rootCA.pem create mode 100644 cmd/server/server.go create mode 100644 cmd/web-hooks/internal/handler/handler.go create mode 100644 cmd/web-hooks/internal/handler/handler_test.go create mode 100644 cmd/web-hooks/main.go create mode 100644 crds/capapplication.yaml create mode 100644 crds/capapplicationversion.yaml create mode 100644 crds/captenant.yaml create mode 100644 crds/captenantoperation.yaml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 hack/LICENSE_BOILERPLATE.txt create mode 100644 hack/api-reference/config.json create mode 100644 hack/api-reference/generate.sh create mode 100644 hack/api-reference/template/members.tpl create mode 100644 hack/api-reference/template/pkg.tpl create mode 100644 hack/api-reference/template/placeholder.go create mode 100644 hack/api-reference/template/type.tpl create mode 100644 hack/generate.sh create mode 100644 hack/helm-reference/generate.sh create mode 100644 hack/tools.go create mode 100644 internal/controller/common_test.go create mode 100644 internal/controller/controller.go create mode 100644 internal/controller/controller_test.go create mode 100644 internal/controller/informers.go create mode 100644 internal/controller/informers_test.go create mode 100644 internal/controller/reconcile-capapplication.go create mode 100644 internal/controller/reconcile-capapplication_test.go create mode 100644 internal/controller/reconcile-capapplicationversion.go create mode 100644 internal/controller/reconcile-capapplicationversion_test.go create mode 100644 internal/controller/reconcile-captenant.go create mode 100644 internal/controller/reconcile-captenant_test.go create mode 100644 internal/controller/reconcile-captenantoperation.go create mode 100644 internal/controller/reconcile-captenantoperation_test.go create mode 100644 internal/controller/reconcile-domains.go create mode 100644 internal/controller/reconcile-domains_test.go create mode 100644 internal/controller/reconcile.go create mode 100644 internal/controller/reconcile_test.go create mode 100644 internal/controller/reconciliation-result.go create mode 100644 internal/controller/testdata/capapplication/ca-01.expected.yaml create mode 100644 internal/controller/testdata/capapplication/ca-01.initial.yaml create mode 100644 internal/controller/testdata/capapplication/ca-02.expected.yaml create mode 100644 internal/controller/testdata/capapplication/ca-02.initial.yaml create mode 100644 internal/controller/testdata/capapplication/ca-03.expected.yaml create mode 100644 internal/controller/testdata/capapplication/ca-03.initial.yaml create mode 100644 internal/controller/testdata/capapplication/ca-04.expected.yaml create mode 100644 internal/controller/testdata/capapplication/ca-04.initial.yaml create mode 100644 internal/controller/testdata/capapplication/ca-05.expected.yaml create mode 100644 internal/controller/testdata/capapplication/ca-05.initial.yaml create mode 100644 internal/controller/testdata/capapplication/ca-06.expected.yaml create mode 100644 internal/controller/testdata/capapplication/ca-06.initial.yaml create mode 100644 internal/controller/testdata/capapplication/ca-07.expected.yaml create mode 100644 internal/controller/testdata/capapplication/ca-07.initial.yaml create mode 100644 internal/controller/testdata/capapplication/ca-08.expected.yaml create mode 100644 internal/controller/testdata/capapplication/ca-08.initial.yaml create mode 100644 internal/controller/testdata/capapplication/ca-09.expected.yaml create mode 100644 internal/controller/testdata/capapplication/ca-09.initial.yaml create mode 100644 internal/controller/testdata/capapplication/ca-10.expected.yaml create mode 100644 internal/controller/testdata/capapplication/ca-10.initial.yaml create mode 100644 internal/controller/testdata/capapplication/ca-11.expected.yaml create mode 100644 internal/controller/testdata/capapplication/ca-11.initial.yaml create mode 100644 internal/controller/testdata/capapplication/ca-12.expected.yaml create mode 100644 internal/controller/testdata/capapplication/ca-12.initial.yaml create mode 100644 internal/controller/testdata/capapplication/ca-13.expected.yaml create mode 100644 internal/controller/testdata/capapplication/ca-13.initial.yaml create mode 100644 internal/controller/testdata/capapplication/ca-14.expected.yaml create mode 100644 internal/controller/testdata/capapplication/ca-14.initial.yaml create mode 100644 internal/controller/testdata/capapplication/ca-15.expected.yaml create mode 100644 internal/controller/testdata/capapplication/ca-15.initial.yaml create mode 100644 internal/controller/testdata/capapplication/ca-16.expected.yaml create mode 100644 internal/controller/testdata/capapplication/ca-16.initial.yaml create mode 100644 internal/controller/testdata/capapplication/ca-17.expected.yaml create mode 100644 internal/controller/testdata/capapplication/ca-17.initial.yaml create mode 100644 internal/controller/testdata/capapplication/ca-18.expected.yaml create mode 100644 internal/controller/testdata/capapplication/ca-18.initial.yaml create mode 100644 internal/controller/testdata/capapplication/ca-19.expected.yaml create mode 100644 internal/controller/testdata/capapplication/ca-19.initial.yaml create mode 100644 internal/controller/testdata/capapplication/ca-20.expected.yaml create mode 100644 internal/controller/testdata/capapplication/ca-20.initial.yaml create mode 100644 internal/controller/testdata/capapplication/ca-21.expected.yaml create mode 100644 internal/controller/testdata/capapplication/ca-21.initial.yaml create mode 100644 internal/controller/testdata/capapplication/ca-22.expected.yaml create mode 100644 internal/controller/testdata/capapplication/ca-22.initial.yaml create mode 100644 internal/controller/testdata/capapplication/ca-23.expected.yaml create mode 100644 internal/controller/testdata/capapplication/ca-23.initial.yaml create mode 100644 internal/controller/testdata/capapplication/ca-24.expected.yaml create mode 100644 internal/controller/testdata/capapplication/ca-24.initial.yaml create mode 100644 internal/controller/testdata/capapplication/ca-25.expected.yaml create mode 100644 internal/controller/testdata/capapplication/ca-25.initial.yaml create mode 100644 internal/controller/testdata/capapplication/ca-26.expected.yaml create mode 100644 internal/controller/testdata/capapplication/ca-26.initial.yaml create mode 100644 internal/controller/testdata/capapplication/ca-27.expected.yaml create mode 100644 internal/controller/testdata/capapplication/ca-27.initial.yaml create mode 100644 internal/controller/testdata/capapplication/ca-28.expected.yaml create mode 100644 internal/controller/testdata/capapplication/ca-28.initial.yaml create mode 100644 internal/controller/testdata/capapplication/ca-29.expected.yaml create mode 100644 internal/controller/testdata/capapplication/ca-29.initial.yaml create mode 100644 internal/controller/testdata/capapplication/ca-30.expected.yaml create mode 100644 internal/controller/testdata/capapplication/ca-30.initial.yaml create mode 100644 internal/controller/testdata/capapplication/ca-31.expected.yaml create mode 100644 internal/controller/testdata/capapplication/ca-31.initial.yaml create mode 100644 internal/controller/testdata/capapplication/ca-32.expected.yaml create mode 100644 internal/controller/testdata/capapplication/ca-32.initial.yaml create mode 100644 internal/controller/testdata/capapplication/ca-33.expected.yaml create mode 100644 internal/controller/testdata/capapplication/ca-33.initial.yaml create mode 100644 internal/controller/testdata/capapplication/ca-34.expected.yaml create mode 100644 internal/controller/testdata/capapplication/ca-34.initial.yaml create mode 100644 internal/controller/testdata/capapplication/ca-35.expected.yaml create mode 100644 internal/controller/testdata/capapplication/ca-35.initial.yaml create mode 100644 internal/controller/testdata/capapplication/ca-36.expected.yaml create mode 100644 internal/controller/testdata/capapplication/ca-36.initial.yaml create mode 100644 internal/controller/testdata/capapplication/ca-37.expected.yaml create mode 100644 internal/controller/testdata/capapplication/ca-37.initial.yaml create mode 100644 internal/controller/testdata/capapplication/ca-38.expected.yaml create mode 100644 internal/controller/testdata/capapplication/ca-38.initial.yaml create mode 100644 internal/controller/testdata/capapplication/ca-39.expected.yaml create mode 100644 internal/controller/testdata/capapplication/ca-39.initial.yaml create mode 100644 internal/controller/testdata/capapplication/ca-40.expected.yaml create mode 100644 internal/controller/testdata/capapplication/ca-40.initial.yaml create mode 100644 internal/controller/testdata/capapplication/ca-41.expected.yaml create mode 100644 internal/controller/testdata/capapplication/ca-41.initial.yaml create mode 100644 internal/controller/testdata/capapplication/ca-42.expected.yaml create mode 100644 internal/controller/testdata/capapplication/ca-42.initial.yaml create mode 100644 internal/controller/testdata/capapplication/ca-43.expected.yaml create mode 100644 internal/controller/testdata/capapplication/ca-43.initial.yaml create mode 100644 internal/controller/testdata/capapplication/ca-44.expected.yaml create mode 100644 internal/controller/testdata/capapplication/ca-45.expected.yaml create mode 100644 internal/controller/testdata/capapplication/ca-45.initial.yaml create mode 100644 internal/controller/testdata/capapplication/ca-dns-error.yaml create mode 100644 internal/controller/testdata/capapplication/ca-dns-not-ready.yaml create mode 100644 internal/controller/testdata/capapplication/ca-dns.yaml create mode 100644 internal/controller/testdata/capapplication/cat-consumer-no-finalizers-ready.yaml create mode 100644 internal/controller/testdata/capapplication/cat-consumer-ready.yaml create mode 100644 internal/controller/testdata/capapplication/cat-consumer-upg-never-deleting.yaml create mode 100644 internal/controller/testdata/capapplication/cat-consumer-upg-never-ready.yaml create mode 100644 internal/controller/testdata/capapplication/cat-provider-error.yaml create mode 100644 internal/controller/testdata/capapplication/cat-provider-no-finalizers-error.yaml create mode 100644 internal/controller/testdata/capapplication/cat-provider-no-finalizers-ready.yaml create mode 100644 internal/controller/testdata/capapplication/cat-provider-upgrade-error.yaml create mode 100644 internal/controller/testdata/capapplication/cav-33-version-updated-ready.yaml create mode 100644 internal/controller/testdata/capapplication/cav-error.yaml create mode 100644 internal/controller/testdata/capapplication/cav-name-modified-ready.yaml create mode 100644 internal/controller/testdata/capapplication/gateway.yaml create mode 100644 internal/controller/testdata/capapplication/istio-ingress-with-cert-error.yaml create mode 100644 internal/controller/testdata/capapplication/istio-ingress-with-cert-no-finalizers.yaml create mode 100644 internal/controller/testdata/capapplication/istio-ingress-with-cert.yaml create mode 100644 internal/controller/testdata/capapplication/istio-ingress-with-certManager-error.yaml create mode 100644 internal/controller/testdata/capapplication/istio-ingress-with-certManager-no-finalizers.yaml create mode 100644 internal/controller/testdata/capapplication/istio-ingress-with-certManager.yaml create mode 100644 internal/controller/testdata/capapplication/istio-ingress-with-no-cert.yaml create mode 100644 internal/controller/testdata/capapplicationversion/cat-provider-version.yaml create mode 100644 internal/controller/testdata/capapplicationversion/cav-annotations.yaml create mode 100644 internal/controller/testdata/capapplicationversion/cav-cluster-netpol-port.yaml create mode 100644 internal/controller/testdata/capapplicationversion/cav-custom-destination-config.yaml create mode 100644 internal/controller/testdata/capapplicationversion/cav-custom-labels.yaml create mode 100644 internal/controller/testdata/capapplicationversion/cav-empty-status.yaml create mode 100644 internal/controller/testdata/capapplicationversion/cav-error-condition-status.yaml create mode 100644 internal/controller/testdata/capapplicationversion/cav-error-status.yaml create mode 100644 internal/controller/testdata/capapplicationversion/cav-failed-content-job.yaml create mode 100644 internal/controller/testdata/capapplicationversion/cav-invalid-ca.yaml create mode 100644 internal/controller/testdata/capapplicationversion/cav-invalid-env-cap.yaml create mode 100644 internal/controller/testdata/capapplicationversion/cav-invalid-env-content.yaml create mode 100644 internal/controller/testdata/capapplicationversion/cav-invalid-env-job-worker.yaml create mode 100644 internal/controller/testdata/capapplicationversion/cav-merged-destinations-router.yaml create mode 100644 internal/controller/testdata/capapplicationversion/cav-pod-security-context.yaml create mode 100644 internal/controller/testdata/capapplicationversion/cav-probes-and-resources.yaml create mode 100644 internal/controller/testdata/capapplicationversion/cav-processing-job-finished.yaml create mode 100644 internal/controller/testdata/capapplicationversion/cav-processing.yaml create mode 100644 internal/controller/testdata/capapplicationversion/cav-ready-deleting.yaml create mode 100644 internal/controller/testdata/capapplicationversion/cav-security-context.yaml create mode 100644 internal/controller/testdata/capapplicationversion/cav-unknown-deleting.yaml create mode 100644 internal/controller/testdata/capapplicationversion/cav-valid-env-config.yaml create mode 100644 internal/controller/testdata/capapplicationversion/content-job-completed.yaml create mode 100644 internal/controller/testdata/capapplicationversion/content-job-failed.yaml create mode 100644 internal/controller/testdata/capapplicationversion/content-job-pending.yaml create mode 100644 internal/controller/testdata/capapplicationversion/expected/cav-deleted-unknown.yaml create mode 100644 internal/controller/testdata/capapplicationversion/expected/cav-deleted.yaml create mode 100644 internal/controller/testdata/capapplicationversion/expected/cav-deleting.yaml create mode 100644 internal/controller/testdata/capapplicationversion/expected/cav-error-condition-processing.yaml create mode 100644 internal/controller/testdata/capapplicationversion/expected/cav-error-processing.yaml create mode 100644 internal/controller/testdata/capapplicationversion/expected/cav-failed-content-job.yaml create mode 100644 internal/controller/testdata/capapplicationversion/expected/cav-failed-env-cap.yaml create mode 100644 internal/controller/testdata/capapplicationversion/expected/cav-failed-env-content.yaml create mode 100644 internal/controller/testdata/capapplicationversion/expected/cav-failed-env-job-worker.yaml create mode 100644 internal/controller/testdata/capapplicationversion/expected/cav-missing-content-job.yaml create mode 100644 internal/controller/testdata/capapplicationversion/expected/cav-missing-secrets.yaml create mode 100644 internal/controller/testdata/capapplicationversion/expected/cav-processing.yaml create mode 100644 internal/controller/testdata/capapplicationversion/expected/cav-ready-annotations.yaml create mode 100644 internal/controller/testdata/capapplicationversion/expected/cav-ready-app-netpol.yaml create mode 100644 internal/controller/testdata/capapplicationversion/expected/cav-ready-cluster-netpol-port.yaml create mode 100644 internal/controller/testdata/capapplicationversion/expected/cav-ready-content-job.yaml create mode 100644 internal/controller/testdata/capapplicationversion/expected/cav-ready-custom-destination-config.yaml create mode 100644 internal/controller/testdata/capapplicationversion/expected/cav-ready-custom-labels-config.yaml create mode 100644 internal/controller/testdata/capapplicationversion/expected/cav-ready-merged-destinations-router.yaml create mode 100644 internal/controller/testdata/capapplicationversion/expected/cav-ready-pod-security-context.yaml create mode 100644 internal/controller/testdata/capapplicationversion/expected/cav-ready-probes-and-resources.yaml create mode 100644 internal/controller/testdata/capapplicationversion/expected/cav-ready-security-context.yaml create mode 100644 internal/controller/testdata/capapplicationversion/expected/cav-ready-valid-env-config.yaml create mode 100644 internal/controller/testdata/capapplicationversion/expected/cav-ready.yaml create mode 100644 internal/controller/testdata/captenant/cat-01.initial.yaml create mode 100644 internal/controller/testdata/captenant/cat-02.expected.yaml create mode 100644 internal/controller/testdata/captenant/cat-02.initial.yaml create mode 100644 internal/controller/testdata/captenant/cat-03.expected.yaml create mode 100644 internal/controller/testdata/captenant/cat-03.initial.yaml create mode 100644 internal/controller/testdata/captenant/cat-04.expected.yaml create mode 100644 internal/controller/testdata/captenant/cat-04.initial.yaml create mode 100644 internal/controller/testdata/captenant/cat-05.expected.yaml create mode 100644 internal/controller/testdata/captenant/cat-05.initial.yaml create mode 100644 internal/controller/testdata/captenant/cat-06.expected.yaml create mode 100644 internal/controller/testdata/captenant/cat-06.initial.yaml create mode 100644 internal/controller/testdata/captenant/cat-07.expected.yaml create mode 100644 internal/controller/testdata/captenant/cat-07.initial.yaml create mode 100644 internal/controller/testdata/captenant/cat-08.expected.yaml create mode 100644 internal/controller/testdata/captenant/cat-08.initial.yaml create mode 100644 internal/controller/testdata/captenant/cat-09.expected.yaml create mode 100644 internal/controller/testdata/captenant/cat-09.initial.yaml create mode 100644 internal/controller/testdata/captenant/cat-10.expected.yaml create mode 100644 internal/controller/testdata/captenant/cat-10.initial.yaml create mode 100644 internal/controller/testdata/captenant/cat-11.expected.yaml create mode 100644 internal/controller/testdata/captenant/cat-11.initial.yaml create mode 100644 internal/controller/testdata/captenant/cat-13.expected.yaml create mode 100644 internal/controller/testdata/captenant/cat-13.initial.yaml create mode 100644 internal/controller/testdata/captenant/cat-14.initial.yaml create mode 100644 internal/controller/testdata/captenant/cat-15.expected.yaml create mode 100644 internal/controller/testdata/captenant/cat-15.initial.yaml create mode 100644 internal/controller/testdata/captenant/cat-16.expected.yaml create mode 100644 internal/controller/testdata/captenant/cat-17.expected.yaml create mode 100644 internal/controller/testdata/captenant/cat-17.initial.yaml create mode 100644 internal/controller/testdata/captenant/cat-20.expected.yaml create mode 100644 internal/controller/testdata/captenant/cat-20.initial.yaml create mode 100644 internal/controller/testdata/captenant/cat-21.expected.yaml create mode 100644 internal/controller/testdata/captenant/cat-21.initial.yaml create mode 100644 internal/controller/testdata/captenant/cat-22.initial.yaml create mode 100644 internal/controller/testdata/captenant/cat-23.expected.yaml create mode 100644 internal/controller/testdata/captenant/cat-23.initial.yaml create mode 100644 internal/controller/testdata/captenant/cat-24.expected.yaml create mode 100644 internal/controller/testdata/captenant/cat-24.initial.yaml create mode 100644 internal/controller/testdata/captenant/cat-25.expected.yaml create mode 100644 internal/controller/testdata/captenant/cat-25.initial.yaml create mode 100644 internal/controller/testdata/captenant/cat-26.initial.yaml create mode 100644 internal/controller/testdata/captenant/cat-27.expected.yaml create mode 100644 internal/controller/testdata/captenant/cat-28.expected.yaml create mode 100644 internal/controller/testdata/captenant/cat-28.initial.yaml create mode 100644 internal/controller/testdata/captenant/cat-29.expected.yaml create mode 100644 internal/controller/testdata/captenant/cat-29.initial.yaml create mode 100644 internal/controller/testdata/captenant/cat-with-no-version.yaml create mode 100644 internal/controller/testdata/captenant/changed-provider-tenant-dnsentry.yaml create mode 100644 internal/controller/testdata/captenant/provider-tenant-dnsentry-not-ready.yaml create mode 100644 internal/controller/testdata/captenant/provider-tenant-dnsentry.yaml create mode 100644 internal/controller/testdata/captenant/provider-tenant-dr-v1.yaml create mode 100644 internal/controller/testdata/captenant/provider-tenant-vs-v1.yaml create mode 100644 internal/controller/testdata/captenant/to-be-updated-provider-tenant-dnsentry.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-01.expected.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-01.initial.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-02.expected.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-02.initial.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-03.expected.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-03.initial.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-04.expected.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-04.initial.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-05.expected.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-05.initial.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-06.expected.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-06.initial.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-07.expected.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-07.initial.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-08.expected.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-08.initial.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-09.expected.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-09.initial.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-10.expected.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-10.initial.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-11.expected.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-11.initial.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-12.expected.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-12.initial.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-13.expected.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-13.initial.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-14.expected.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-14.initial.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-15.expected.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-15.initial.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-16.expected.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-16.initial.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-17.expected.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-17.initial.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-18.expected.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-18.initial.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-19.expected.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-19.initial.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-20.expected.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-20.initial.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-21.expected.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-21.initial.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-22.expected.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-22.initial.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-23.expected.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-23.initial.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-24.expected.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-24.initial.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-25.expected.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-25.initial.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-26.expected.yaml create mode 100644 internal/controller/testdata/captenantoperation/ctop-26.initial.yaml create mode 100644 internal/controller/testdata/common/capapplication-multi-xsuaa.yaml create mode 100644 internal/controller/testdata/common/capapplication.yaml create mode 100644 internal/controller/testdata/common/capapplicationversion-invalid-env.yaml create mode 100644 internal/controller/testdata/common/capapplicationversion-v1-mtxs-custom-cmd.yaml create mode 100644 internal/controller/testdata/common/capapplicationversion-v1-mtxs.yaml create mode 100644 internal/controller/testdata/common/capapplicationversion-v1-not-ready.yaml create mode 100644 internal/controller/testdata/common/capapplicationversion-v1-resources.yaml create mode 100644 internal/controller/testdata/common/capapplicationversion-v1-security-context.yaml create mode 100644 internal/controller/testdata/common/capapplicationversion-v1.yaml create mode 100644 internal/controller/testdata/common/capapplicationversion-v2-annotations.yaml create mode 100644 internal/controller/testdata/common/capapplicationversion-v2-multi-xsuaa.yaml create mode 100644 internal/controller/testdata/common/capapplicationversion-v2-multiple-tenant-ops.yaml create mode 100644 internal/controller/testdata/common/capapplicationversion-v2-no-mtx-workload.yaml create mode 100644 internal/controller/testdata/common/capapplicationversion-v2-pod-security-context.yaml create mode 100644 internal/controller/testdata/common/capapplicationversion-v2.yaml create mode 100644 internal/controller/testdata/common/capapplicationversion-v3.yaml create mode 100644 internal/controller/testdata/common/captenant-provider-ready.yaml create mode 100644 internal/controller/testdata/common/captenant-provider-upgraded-ready.yaml create mode 100644 internal/controller/testdata/common/credential-secrets.yaml create mode 100644 internal/controller/testdata/common/istio-ingress.yaml create mode 100644 internal/controller/testdata/common/operator-gateway.yaml create mode 100644 internal/controller/utils.go create mode 100644 internal/util/config.go create mode 100644 internal/util/types.go create mode 100644 internal/util/uaa.go create mode 100644 internal/util/uaa_test.go create mode 100644 internal/util/vcap-credentials.go create mode 100644 internal/util/vcap-credentials_test.go create mode 100644 pkg/apis/sme.sap.com/v1alpha1/doc.go create mode 100644 pkg/apis/sme.sap.com/v1alpha1/register.go create mode 100644 pkg/apis/sme.sap.com/v1alpha1/types.go create mode 100644 pkg/apis/sme.sap.com/v1alpha1/utils.go create mode 100644 pkg/apis/sme.sap.com/v1alpha1/zz_generated.deepcopy.go create mode 100644 pkg/client/applyconfiguration/internal/internal.go create mode 100644 pkg/client/applyconfiguration/sme.sap.com/v1alpha1/applicationdomains.go create mode 100644 pkg/client/applyconfiguration/sme.sap.com/v1alpha1/btp.go create mode 100644 pkg/client/applyconfiguration/sme.sap.com/v1alpha1/btptenantidentification.go create mode 100644 pkg/client/applyconfiguration/sme.sap.com/v1alpha1/capapplication.go create mode 100644 pkg/client/applyconfiguration/sme.sap.com/v1alpha1/capapplicationspec.go create mode 100644 pkg/client/applyconfiguration/sme.sap.com/v1alpha1/capapplicationstatus.go create mode 100644 pkg/client/applyconfiguration/sme.sap.com/v1alpha1/capapplicationversion.go create mode 100644 pkg/client/applyconfiguration/sme.sap.com/v1alpha1/capapplicationversionspec.go create mode 100644 pkg/client/applyconfiguration/sme.sap.com/v1alpha1/capapplicationversionstatus.go create mode 100644 pkg/client/applyconfiguration/sme.sap.com/v1alpha1/captenant.go create mode 100644 pkg/client/applyconfiguration/sme.sap.com/v1alpha1/captenantoperation.go create mode 100644 pkg/client/applyconfiguration/sme.sap.com/v1alpha1/captenantoperationspec.go create mode 100644 pkg/client/applyconfiguration/sme.sap.com/v1alpha1/captenantoperationstatus.go create mode 100644 pkg/client/applyconfiguration/sme.sap.com/v1alpha1/captenantoperationstep.go create mode 100644 pkg/client/applyconfiguration/sme.sap.com/v1alpha1/captenantspec.go create mode 100644 pkg/client/applyconfiguration/sme.sap.com/v1alpha1/captenantstatus.go create mode 100644 pkg/client/applyconfiguration/sme.sap.com/v1alpha1/containerdetails.go create mode 100644 pkg/client/applyconfiguration/sme.sap.com/v1alpha1/deploymentdetails.go create mode 100644 pkg/client/applyconfiguration/sme.sap.com/v1alpha1/genericstatus.go create mode 100644 pkg/client/applyconfiguration/sme.sap.com/v1alpha1/jobdetails.go create mode 100644 pkg/client/applyconfiguration/sme.sap.com/v1alpha1/namevalue.go create mode 100644 pkg/client/applyconfiguration/sme.sap.com/v1alpha1/ports.go create mode 100644 pkg/client/applyconfiguration/sme.sap.com/v1alpha1/serviceinfo.go create mode 100644 pkg/client/applyconfiguration/sme.sap.com/v1alpha1/tenantoperations.go create mode 100644 pkg/client/applyconfiguration/sme.sap.com/v1alpha1/tenantoperationworkloadreference.go create mode 100644 pkg/client/applyconfiguration/sme.sap.com/v1alpha1/workloaddetails.go create mode 100644 pkg/client/applyconfiguration/utils.go create mode 100644 pkg/client/clientset/versioned/clientset.go create mode 100644 pkg/client/clientset/versioned/fake/clientset_generated.go create mode 100644 pkg/client/clientset/versioned/fake/doc.go create mode 100644 pkg/client/clientset/versioned/fake/register.go create mode 100644 pkg/client/clientset/versioned/scheme/doc.go create mode 100644 pkg/client/clientset/versioned/scheme/register.go create mode 100644 pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/capapplication.go create mode 100644 pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/capapplicationversion.go create mode 100644 pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/captenant.go create mode 100644 pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/captenantoperation.go create mode 100644 pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/doc.go create mode 100644 pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/fake/doc.go create mode 100644 pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/fake/fake_capapplication.go create mode 100644 pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/fake/fake_capapplicationversion.go create mode 100644 pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/fake/fake_captenant.go create mode 100644 pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/fake/fake_captenantoperation.go create mode 100644 pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/fake/fake_sme.sap.com_client.go create mode 100644 pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/generated_expansion.go create mode 100644 pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/sme.sap.com_client.go create mode 100644 pkg/client/informers/externalversions/factory.go create mode 100644 pkg/client/informers/externalversions/generic.go create mode 100644 pkg/client/informers/externalversions/internalinterfaces/factory_interfaces.go create mode 100644 pkg/client/informers/externalversions/sme.sap.com/interface.go create mode 100644 pkg/client/informers/externalversions/sme.sap.com/v1alpha1/capapplication.go create mode 100644 pkg/client/informers/externalversions/sme.sap.com/v1alpha1/capapplicationversion.go create mode 100644 pkg/client/informers/externalversions/sme.sap.com/v1alpha1/captenant.go create mode 100644 pkg/client/informers/externalversions/sme.sap.com/v1alpha1/captenantoperation.go create mode 100644 pkg/client/informers/externalversions/sme.sap.com/v1alpha1/interface.go create mode 100644 pkg/client/listers/sme.sap.com/v1alpha1/capapplication.go create mode 100644 pkg/client/listers/sme.sap.com/v1alpha1/capapplicationversion.go create mode 100644 pkg/client/listers/sme.sap.com/v1alpha1/captenant.go create mode 100644 pkg/client/listers/sme.sap.com/v1alpha1/captenantoperation.go create mode 100644 pkg/client/listers/sme.sap.com/v1alpha1/expansion_generated.go create mode 100644 website/archetypes/default.md create mode 100644 website/content/en/_index.md create mode 100644 website/content/en/docs/_index.md create mode 100644 website/content/en/docs/concepts/_index.md create mode 100644 website/content/en/docs/concepts/cap-application-components.md create mode 100644 website/content/en/docs/concepts/operator-components/_index.md create mode 100644 website/content/en/docs/concepts/operator-components/controller.md create mode 100644 website/content/en/docs/concepts/operator-components/mtx-job.md create mode 100644 website/content/en/docs/concepts/operator-components/subscription-server.md create mode 100644 website/content/en/docs/configuration/_index.md create mode 100644 website/content/en/docs/installation/_index.md create mode 100644 website/content/en/docs/installation/helm-install.md create mode 100644 website/content/en/docs/installation/prerequisites.md create mode 100644 website/content/en/docs/reference/_index.md create mode 100644 website/content/en/docs/support/_index.md create mode 100644 website/content/en/docs/troubleshoot/_index.md create mode 100644 website/content/en/docs/usage/_index.md create mode 100644 website/content/en/docs/usage/deploying-application.md create mode 100644 website/content/en/docs/usage/prerequisites.md create mode 100644 website/content/en/docs/usage/resources/_index.md create mode 100644 website/content/en/docs/usage/resources/capapplication.md create mode 100644 website/content/en/docs/usage/resources/capapplicationversion.md create mode 100644 website/content/en/docs/usage/resources/captenant.md create mode 100644 website/content/en/docs/usage/resources/captenantoperation.md create mode 100644 website/content/en/docs/usage/tenant-provisioning.md create mode 100644 website/content/en/docs/usage/version-upgrade.md create mode 100644 website/go.mod create mode 100644 website/go.sum create mode 100644 website/hugo.yaml create mode 100644 website/includes/api-reference.html create mode 100644 website/includes/chart-values.md create mode 100644 website/layouts/shortcodes/include.html create mode 100644 website/package-lock.json create mode 100644 website/package.json create mode 100644 website/static/img/activity-tenantprovisioning.png create mode 100644 website/static/img/block-cluster.png create mode 100644 website/static/img/block-controller.png create mode 100644 website/static/img/block-subscription.png create mode 100644 website/static/img/logo.png create mode 100644 website/static/img/workflow.png diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0f04682 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +# More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file +# Ignore build and test binaries. +bin/ +testbin/ diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 0000000..3b06548 --- /dev/null +++ b/.github/workflows/go.yml @@ -0,0 +1,32 @@ +name: Go (Build & Unit test) + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + + build: + name: Build + runs-on: ubuntu-22.04 + steps: + + - name: Check out code into the Go module directory + uses: actions/checkout@v3 + + - name: Setup Go + uses: actions/setup-go@v4 + with: + go-version: ^1.21 + cache: true + + - name: Get dependencies + run: go get -v -t -d ./... + + - name: Build all relevant packages + run: CGO_ENABLED=0 go build -v ./cmd/... + + - name: Test relevant packages + run: CGO_ENABLED=0 go test -v ./... \ No newline at end of file diff --git a/.github/workflows/publish-website.yaml b/.github/workflows/publish-website.yaml new file mode 100644 index 0000000..35a60dd --- /dev/null +++ b/.github/workflows/publish-website.yaml @@ -0,0 +1,79 @@ +name: Publish Website + +on: + push: + branches: + - main + paths: + - website/** + - .github/workflows/publish-website.yaml + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + build: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + fetch-depth: 0 + - name: Setup Pages + id: pages + uses: actions/configure-pages@v3 + - name: Setup Go + uses: actions/setup-go@v4 + with: + go-version: ^1.21 + - name: Setup Hugo + uses: peaceiris/actions-hugo@v2 + with: + hugo-version: "0.115.2" + extended: true + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: "18.x" + - name: Update dependencies + run: | + cd website + npm ci + - name: Build with Hugo + env: + HUGO_ENVIRONMENT: production + HUGO_ENV: production + run: | + cd website + hugo \ + --gc \ + --minify \ + --baseURL "${{ steps.pages.outputs.base_url }}/" + + - name: Upload artifact + uses: actions/upload-pages-artifact@v2 + with: + path: website/public + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-22.04 + needs: build + + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..391564d --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +local/ +.kubeconfig + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +bin +testbin/* + +# Test binary, build with `go test -c` +*.test + +# OS specific +.DS_Store + +# Hugo artifacts +.hugo_build.lock +/website/resources +/website/node_modules +/website/public + +# /tmp +tmp/ \ No newline at end of file diff --git a/.reuse/dep5 b/.reuse/dep5 index 96d499f..3891e91 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -1,7 +1,7 @@ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ -Upstream-Name: -Upstream-Contact: -Source: +Upstream-Name: cap-operator +Upstream-Contact: cap-operator@sap.com +Source: Disclaimer: The code in this project may include calls to APIs ("API Calls") of SAP or third-party products or services developed outside of this project ("External Products"). @@ -24,14 +24,6 @@ Disclaimer: The code in this project may include calls to APIs ("API Calls") of you any rights to use or access any SAP External Product, or provide any third parties the right to use of access any SAP External Product, through API Calls. -Files: -Copyright: SAP SE or an SAP affiliate company and contributors -License: Apache-2.0 - -Files: -Copyright: -License: - -Files: -Copyright: -License: \ No newline at end of file +Files: ** +Copyright: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +License: Apache-2.0 \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..0ffb9a0 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Controller", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}/cmd/controller" + } + ] +} \ No newline at end of file diff --git a/LICENSE b/LICENSE index 261eeb9..2bb9ad2 100644 --- a/LICENSE +++ b/LICENSE @@ -173,29 +173,4 @@ incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/README.md b/README.md index 9721fd8..c39bdb0 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,20 @@ -# SAP Repository Template +# [CAP Operator](https://sap.github.io/cap-operator/) -Default templates for SAP open source repositories, including LICENSE, .reuse/dep5, Code of Conduct, etc... All repositories on github.com/SAP will be created based on this template. +CAP Operator manages the lifecycle operations involved in running multi-tenant CAP applications on Kubernetes clusters, primarily SAP Gardener managed clusters. -## To-Do +#### Documentation +Visit the [Documentation](https://sap.github.io/cap-operator/docs) to find out how to install and use the CAP Operator -In case you are the maintainer of a new SAP open source project, these are the steps to do with the template files: +#### Helm Chart +The local version of the [helm chart](https://github.com/sap/cap-operator-lifecycle/tree/release/chart) is now part of [CAP Operator Lifecycle](https://github.com/sap/cap-operator-lifecycle) repo. -- Check if the default license (Apache 2.0) also applies to your project. A license change should only be required in exceptional cases. If this is the case, please change the [license file](LICENSE). -- Enter the correct metadata for the REUSE tool. See our [wiki page](https://wiki.wdf.sap.corp/wiki/display/ospodocs/Using+the+Reuse+Tool+of+FSFE+for+Copyright+and+License+Information) for details how to do it. You can find an initial .reuse/dep5 file to build on. Please replace the parts inside the single angle quotation marks < > by the specific information for your repository and be sure to run the REUSE tool to validate that the metadata is correct. -- Adjust the contribution guidelines (e.g. add coding style guidelines, pull request checklists, different license if needed etc.) -- Add information about your project to this README (name, description, requirements etc). Especially take care for the placeholders - those ones need to be replaced with your project name. See the sections below the horizontal line and [our guidelines on our wiki page](https://wiki.wdf.sap.corp/wiki/display/ospodocs/Guidelines+for+README.md+file) what is required and recommended. -- Remove all content in this README above and including the horizontal line ;) +#### CRDs +CRDs for the CAP Operator can be applied from the [./crds](./crds) folder, these are also copied over to the [helm chart](https://github.com/sap/cap-operator-lifecycle/tree/main/chart) when updated. -*** - -# Our new open source project - -## About this project - -*Insert a short description of your project here...* - -## Requirements and Setup - -*Insert a short description what is required to get your project running...* ## Support, Feedback, Contributing -This project is open to feature requests/suggestions, bug reports etc. via [GitHub issues](https://github.com/SAP//issues). Contribution and feedback are encouraged and always welcome. For more information about how to contribute, the project structure, as well as additional contribution information, see our [Contribution Guidelines](CONTRIBUTING.md). +This project is open to feature requests/suggestions, bug reports etc. via [GitHub issues](https://github.com/SAP/cap-operator/issues). Contribution and feedback are encouraged and always welcome. For more information about how to contribute, the project structure, as well as additional contribution information, see our [Contribution Guidelines](CONTRIBUTING.md). ## Code of Conduct @@ -34,4 +22,4 @@ We as members, contributors, and leaders pledge to make participation in our com ## Licensing -Copyright (20xx-)20xx SAP SE or an SAP affiliate company and contributors. Please see our [LICENSE](LICENSE) for copyright and license information. Detailed information including third-party components and their licensing/copyright information is available [via the REUSE tool](https://api.reuse.software/info/github.com/SAP/). +Copyright 2023 SAP SE or an SAP affiliate company and cap-operator contributors. Please see our [LICENSE](LICENSE) for copyright and license information. Detailed information including third-party components and their licensing/copyright information is available [via the REUSE tool](https://api.reuse.software/info/github.com/SAP/cap-operator). diff --git a/build/controller/Dockerfile b/build/controller/Dockerfile new file mode 100644 index 0000000..0920f0e --- /dev/null +++ b/build/controller/Dockerfile @@ -0,0 +1,15 @@ +# Build stage for go modules +FROM golang:1.21-alpine as build + +WORKDIR /build + +COPY . . + +RUN CGO_ENABLED=0 go build -o bin/controller ./cmd/controller/ + +# Run Stage +FROM gcr.io/distroless/static AS final + +ENTRYPOINT ["/app/controller"] + +COPY --from=build /build/bin/controller /app/controller \ No newline at end of file diff --git a/build/mtx-job/Dockerfile b/build/mtx-job/Dockerfile new file mode 100644 index 0000000..348daf2 --- /dev/null +++ b/build/mtx-job/Dockerfile @@ -0,0 +1,15 @@ +# Build stage for go modules +FROM golang:1.21-alpine as build + +WORKDIR /build + +COPY . . + +RUN CGO_ENABLED=0 go build -o ./bin/mtx-job ./cmd/mtx-job/main.go + +# Run Stage +FROM gcr.io/distroless/static AS final + +ENTRYPOINT ["/app/mtx-job"] + +COPY --from=build /build/bin/mtx-job /app/mtx-job \ No newline at end of file diff --git a/build/server/Dockerfile b/build/server/Dockerfile new file mode 100644 index 0000000..4c92101 --- /dev/null +++ b/build/server/Dockerfile @@ -0,0 +1,15 @@ +# Build stage for go modules +FROM golang:1.21-alpine as build + +WORKDIR /build + +COPY . . + +RUN CGO_ENABLED=0 go build -o bin/server ./cmd/server/ + +# Run Stage +FROM gcr.io/distroless/static AS final + +ENTRYPOINT ["/app/server"] + +COPY --from=build /build/bin/server /app/server \ No newline at end of file diff --git a/build/web-hooks/Dockerfile b/build/web-hooks/Dockerfile new file mode 100644 index 0000000..356fb13 --- /dev/null +++ b/build/web-hooks/Dockerfile @@ -0,0 +1,15 @@ +# Build stage for go modules +FROM golang:1.21-alpine as build + +WORKDIR /build + +COPY . . + +RUN CGO_ENABLED=0 go build -o ./bin/webhook ./cmd/web-hooks/main.go + +# Run Stage +FROM gcr.io/distroless/static AS final + +ENTRYPOINT ["/app/webhook"] + +COPY --from=build /build/bin/webhook /app/webhook \ No newline at end of file diff --git a/cmd/controller/main.go b/cmd/controller/main.go new file mode 100644 index 0000000..cbe1341 --- /dev/null +++ b/cmd/controller/main.go @@ -0,0 +1,129 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +package main + +import ( + "context" + "os" + "os/signal" + "syscall" + "time" + + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/leaderelection" + "k8s.io/client-go/tools/leaderelection/resourcelock" + "k8s.io/klog/v2" + + certManager "github.com/cert-manager/cert-manager/pkg/client/clientset/versioned" + gardenerCert "github.com/gardener/cert-management/pkg/client/cert/clientset/versioned" + dns "github.com/gardener/external-dns-management/pkg/client/dns/clientset/versioned" + "github.com/google/uuid" + "github.com/sap/cap-operator/internal/controller" + "github.com/sap/cap-operator/internal/util" + "github.com/sap/cap-operator/pkg/client/clientset/versioned" + istio "istio.io/client-go/pkg/clientset/versioned" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + LeaseLockName = "capoperator-lease-lock" +) + +func main() { + config := util.GetConfig() + if config == nil { + klog.Fatal("Config not found") + } + + leaseLockNamespace := util.GetNamespace() + leaseLockId := uuid.New().String() + + coreClient, err := kubernetes.NewForConfig(config) + if err != nil { + klog.Fatal("Could not create kubernetes core client: ", err.Error()) + } + + crdClient, err := versioned.NewForConfig(config) + if err != nil { + klog.Fatal("could not create client for custom resources: ", err.Error()) + } + + istioClient, err := istio.NewForConfig(config) + if err != nil { + klog.Fatal("could not create client for istio resources: ", err.Error()) + } + + certClient, err := gardenerCert.NewForConfig(config) + if err != nil { + klog.Fatal("could not create client for certificate resources: ", err.Error()) + } + + certManagerClient, err := certManager.NewForConfig(config) + if err != nil { + klog.Fatal("could not create client for certManager certificate resources: ", err.Error()) + } + + dnsClient, err := dns.NewForConfig(config) + if err != nil { + klog.Fatal("could not create client for dns resources: ", err.Error()) + } + + // context for the reconciliation controller + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Handle sys exits to ensure cleanup of controller code before stopping leading + leaseCh := make(chan os.Signal, 1) + signal.Notify(leaseCh, os.Interrupt, syscall.SIGTERM) + go func() { + <-leaseCh + klog.Info("Interrupt received, shutting down operator context") + cancel() + }() + + // Create a LeaseLock resource + leaseLock := &resourcelock.LeaseLock{ + LeaseMeta: metav1.ObjectMeta{ + Name: LeaseLockName, + Namespace: leaseLockNamespace, + }, + Client: coreClient.CoordinationV1(), + LockConfig: resourcelock.ResourceLockConfig{ + Identity: leaseLockId, + }, + } + + // Run leader election + leaderelection.RunOrDie(ctx, leaderelection.LeaderElectionConfig{ + Lock: leaseLock, + LeaseDuration: 15 * time.Second, + RenewDeadline: 10 * time.Second, + RetryPeriod: 5 * time.Second, + ReleaseOnCancel: true, + Callbacks: leaderelection.LeaderCallbacks{ + OnStartedLeading: func(ctx context.Context) { + klog.Infof("Started leading: %s - %s", LeaseLockName, leaseLockId) + + checkDone := make(chan bool, 1) + go checkHashedLabels(checkDone, coreClient, crdClient, istioClient, certClient, certManagerClient, dnsClient) + <-checkDone + klog.Info("check & update of hashed labels done") + + c := controller.NewController(coreClient, crdClient, istioClient, certClient, certManagerClient, dnsClient) + go c.Start(ctx) + }, + OnStoppedLeading: func() { + klog.Infof("Stopped leading: %s - %s", LeaseLockName, leaseLockId) + os.Exit(0) + }, + OnNewLeader: func(id string) { + if id == leaseLockId { + return + } + klog.Infof("Leader exists: %s", id) + }, + }, + }) +} diff --git a/cmd/controller/temp.go b/cmd/controller/temp.go new file mode 100644 index 0000000..9dd8562 --- /dev/null +++ b/cmd/controller/temp.go @@ -0,0 +1,371 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +package main + +import ( + "context" + "crypto/sha1" + "fmt" + "strings" + + certManager "github.com/cert-manager/cert-manager/pkg/client/clientset/versioned" + gardenerCert "github.com/gardener/cert-management/pkg/client/cert/clientset/versioned" + gardenerDNS "github.com/gardener/external-dns-management/pkg/client/dns/clientset/versioned" + "github.com/sap/cap-operator/pkg/client/clientset/versioned" + istio "istio.io/client-go/pkg/clientset/versioned" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/selection" + "k8s.io/client-go/kubernetes" + "k8s.io/klog/v2" +) + +type ownerInfo struct { + kind string + ownerNamespace string + ownerName string +} + +type appIdentifier struct { + globalAccountId string + appName string +} + +const ( + AnnotationOwnerIdentifier = "sme.sap.com/owner-identifier" + AnnotationBTPApplicationIdentifier = "sme.sap.com/btp-app-identifier" + LabelOwnerIdentifierHash = "sme.sap.com/owner-identifier-hash" + LabelBTPApplicationIdentifierHash = "sme.sap.com/btp-app-identifier-hash" + CAPApplication = "CAPApplication" + CAPApplicationVersion = "CAPApplicationVersion" + CAPTenant = "CAPTenant" + CAPTenantOperation = "CAPTenantOperation" + CAPOperator = "CAPOperator" + OperatorDomains = "OperatorDomains" +) + +var btpAppIdMap map[string]*appIdentifier +var ownerMap map[string]*ownerInfo + +func checkHashedLabels(checkDone chan bool, client kubernetes.Interface, crdClient versioned.Interface, istioClient istio.Interface, gardenerCertificateClient gardenerCert.Interface, certManagerCertificateClient certManager.Interface, gardenerDNSClient gardenerDNS.Interface) { + btpAppIdMap = map[string]*appIdentifier{} + ownerMap = map[string]*ownerInfo{} + // Always set the channel to true in the end + defer func() { + checkDone <- true + }() + + ownerMap[CAPOperator+"."+OperatorDomains] = &ownerInfo{ + ownerNamespace: CAPOperator, + ownerName: OperatorDomains, + } + + ctx := context.TODO() + klog.Info("checking for old Labels on known resources") + + btpAppLabelReq, _ := labels.NewRequirement(AnnotationBTPApplicationIdentifier, selection.Exists, []string{}) + ownerLabelReq, _ := labels.NewRequirement(AnnotationOwnerIdentifier, selection.Exists, []string{}) + + btpAppSelector := labels.NewSelector() + btpAppSelector.Add(*btpAppLabelReq) + + ownerSelector := labels.NewSelector() + ownerSelector.Add(*ownerLabelReq) + + btpAppOwnerSelector := labels.NewSelector() + btpAppOwnerSelector.Add(*btpAppLabelReq, *ownerLabelReq) + + // CAP application with LabelBTPApplicationIdentifier + caList, err := crdClient.SmeV1alpha1().CAPApplications("").List(ctx, metav1.ListOptions{LabelSelector: btpAppSelector.String()}) + if err != nil { + klog.Error(err) + return + } + for _, ca := range caList.Items { + // Update map with known good values + btpAppIdMap[ca.Labels[AnnotationBTPApplicationIdentifier]] = &appIdentifier{ + globalAccountId: ca.Spec.GlobalAccountId, + appName: ca.Spec.BTPAppName, + } + ownerMap[ca.Namespace+"."+ca.Name] = &ownerInfo{ + ownerNamespace: ca.Namespace, + ownerName: ca.Name, + } + // certificates are created with CAPApplication as the kind in ownerinfo + ownerMap[CAPApplication+"."+ca.Namespace+"."+ca.Name] = &ownerInfo{ + kind: CAPApplication, + ownerNamespace: ca.Namespace, + ownerName: ca.Name, + } + // add Label/Annotation for BTP App + if updateLabelAnnotationMetadata(&ca.ObjectMeta, true, false) { + crdClient.SmeV1alpha1().CAPApplications(ca.Namespace).Update(ctx, &ca, metav1.UpdateOptions{}) + } + } + + // CAP application version with LabelBTPApplicationIdentifier and LabelOwnerIdentifier + cavList, err := crdClient.SmeV1alpha1().CAPApplicationVersions("").List(ctx, metav1.ListOptions{LabelSelector: btpAppOwnerSelector.String()}) + if err != nil { + klog.Error(err) + return + } + for _, cav := range cavList.Items { + ownerMap[cav.Namespace+"."+cav.Name] = &ownerInfo{ + ownerNamespace: cav.Namespace, + ownerName: cav.Name, + } + if updateLabelAnnotationMetadata(&cav.ObjectMeta, true, true) { + crdClient.SmeV1alpha1().CAPApplicationVersions(cav.Namespace).Update(ctx, &cav, metav1.UpdateOptions{}) + } + } + + // CAP Tenant with LabelBTPApplicationIdentifier and LabelOwnerIdentifier + catList, err := crdClient.SmeV1alpha1().CAPTenants("").List(ctx, metav1.ListOptions{LabelSelector: btpAppOwnerSelector.String()}) + if err != nil { + klog.Error(err) + return + } + for _, cat := range catList.Items { + ownerMap[cat.Namespace+"."+cat.Name] = &ownerInfo{ + ownerNamespace: cat.Namespace, + ownerName: cat.Name, + } + // DNS entries are being created with CAPTenant as the kind in owner info + ownerMap[CAPTenant+"."+cat.Namespace+cat.Name] = &ownerInfo{ + kind: CAPTenant, + ownerNamespace: cat.Namespace, + ownerName: cat.Name, + } + if updateLabelAnnotationMetadata(&cat.ObjectMeta, true, true) { + crdClient.SmeV1alpha1().CAPTenants(cat.Namespace).Update(ctx, &cat, metav1.UpdateOptions{}) + } + } + + // CAP Tenant Operation with LabelOwnerIdentifier + ctopList, err := crdClient.SmeV1alpha1().CAPTenantOperations("").List(ctx, metav1.ListOptions{LabelSelector: ownerSelector.String()}) + if err != nil { + klog.Error(err) + return + } + for _, ctop := range ctopList.Items { + ownerMap[ctop.Namespace+"."+ctop.Name] = &ownerInfo{ + ownerNamespace: ctop.Namespace, + ownerName: ctop.Name, + } + + if updateLabelAnnotationMetadata(&ctop.ObjectMeta, false, true) { + crdClient.SmeV1alpha1().CAPTenantOperations(ctop.Namespace).Update(ctx, &ctop, metav1.UpdateOptions{}) + } + } + + // Cert Manager Certificates + certManagerCertificateList, err := certManagerCertificateClient.CertmanagerV1().Certificates("").List(ctx, metav1.ListOptions{LabelSelector: ownerSelector.String()}) + if err != nil { + klog.Error(err) + return + } + for _, cert := range certManagerCertificateList.Items { + if updateLabelAnnotationMetadata(&cert.ObjectMeta, false, true) { + certManagerCertificateClient.CertmanagerV1().Certificates(cert.Namespace).Update(ctx, &cert, metav1.UpdateOptions{}) + } + } + + // Gardener Certificates + gardenerCertificateList, err := gardenerCertificateClient.CertV1alpha1().Certificates("").List(ctx, metav1.ListOptions{LabelSelector: ownerSelector.String()}) + if err != nil { + klog.Error(err) + return + } + for _, cert := range gardenerCertificateList.Items { + if updateLabelAnnotationMetadata(&cert.ObjectMeta, false, true) { + gardenerCertificateClient.CertV1alpha1().Certificates(cert.Namespace).Update(ctx, &cert, metav1.UpdateOptions{}) + } + } + + // Gateways + gwList, err := istioClient.NetworkingV1beta1().Gateways("").List(ctx, metav1.ListOptions{LabelSelector: btpAppOwnerSelector.String()}) + if err != nil { + klog.Error(err) + return + } + for _, gw := range gwList.Items { + // Update one for just app id (primary domain gw) + if updateLabelAnnotationMetadata(&gw.ObjectMeta, true, false) { + istioClient.NetworkingV1beta1().Gateways(gw.Namespace).Update(ctx, gw, metav1.UpdateOptions{}) + } + // Update one for just owner info (Secondary domain gw) + if updateLabelAnnotationMetadata(&gw.ObjectMeta, false, true) { + istioClient.NetworkingV1beta1().Gateways(gw.Namespace).Update(ctx, gw, metav1.UpdateOptions{}) + } + } + + // DNS Entries + dnsEntryList, err := gardenerDNSClient.DnsV1alpha1().DNSEntries("").List(ctx, metav1.ListOptions{LabelSelector: ownerSelector.String()}) + if err != nil { + klog.Error(err) + return + } + for _, dnsEntry := range dnsEntryList.Items { + if updateLabelAnnotationMetadata(&dnsEntry.ObjectMeta, false, true) { + gardenerDNSClient.DnsV1alpha1().DNSEntries(dnsEntry.Namespace).Update(ctx, &dnsEntry, metav1.UpdateOptions{}) + } + } + + // Destination Rules + destRuleList, err := istioClient.NetworkingV1beta1().DestinationRules("").List(ctx, metav1.ListOptions{LabelSelector: ownerSelector.String()}) + if err != nil { + klog.Error(err) + return + } + for _, destRule := range destRuleList.Items { + if updateLabelAnnotationMetadata(&destRule.ObjectMeta, false, true) { + istioClient.NetworkingV1beta1().DestinationRules(destRule.Namespace).Update(ctx, destRule, metav1.UpdateOptions{}) + } + } + + // Virtual Services + virtualServiceList, err := istioClient.NetworkingV1beta1().VirtualServices("").List(ctx, metav1.ListOptions{LabelSelector: ownerSelector.String()}) + if err != nil { + klog.Error(err) + return + } + for _, virtualService := range virtualServiceList.Items { + if updateLabelAnnotationMetadata(&virtualService.ObjectMeta, false, true) { + istioClient.NetworkingV1beta1().VirtualServices(virtualService.Namespace).Update(ctx, virtualService, metav1.UpdateOptions{}) + } + } + + // CAV Deployments + deploymentList, err := client.AppsV1().Deployments("").List(ctx, metav1.ListOptions{LabelSelector: btpAppOwnerSelector.String()}) + if err != nil { + klog.Error(err) + return + } + for _, deployment := range deploymentList.Items { + if updateLabelAnnotationMetadata(&deployment.ObjectMeta, true, true) { + client.AppsV1().Deployments(deployment.Namespace).Update(ctx, &deployment, metav1.UpdateOptions{}) + } + } + + // CAV Services + serviceList, err := client.CoreV1().Services("").List(ctx, metav1.ListOptions{LabelSelector: btpAppOwnerSelector.String()}) + if err != nil { + klog.Error(err) + return + } + for _, service := range serviceList.Items { + if updateLabelAnnotationMetadata(&service.ObjectMeta, true, true) { + client.CoreV1().Services(service.Namespace).Update(ctx, &service, metav1.UpdateOptions{}) + } + } + + // CTOP Jobs + jobList, err := client.BatchV1().Jobs("").List(ctx, metav1.ListOptions{LabelSelector: btpAppOwnerSelector.String()}) + if err != nil { + klog.Error(err) + return + } + for _, job := range jobList.Items { + if updateLabelAnnotationMetadata(&job.ObjectMeta, true, true) { + client.BatchV1().Jobs(job.Namespace).Update(ctx, &job, metav1.UpdateOptions{}) + } + } +} + +func sha1Sum(source ...string) string { + sum := sha1.Sum([]byte(strings.Join(source, ""))) + return fmt.Sprintf("%x", sum) +} + +func amendObjectMetadata(object *metav1.ObjectMeta, annotatedOldLabel string, hashLabel string, oldValue string, hashedValue string) (updated bool) { + // Check if old label exists, if so remove it + if _, ok := object.Labels[annotatedOldLabel]; ok { + delete(object.Labels, annotatedOldLabel) + klog.Infof("Removed old label %s=%s for resource %s.%s", annotatedOldLabel, oldValue, object.Namespace, object.Name) + updated = true + } + // Add hashed label as the new label with the hashed identifier value + if _, ok := object.Labels[hashLabel]; !ok { + object.Labels[hashLabel] = hashedValue + klog.Infof("Added hashed label %s=%s for resource %s.%s", hashLabel, hashedValue, object.Namespace, object.Name) + updated = true + } + // Add old label as an annotation with the old value + if _, ok := object.Annotations[annotatedOldLabel]; !ok { + object.Annotations[annotatedOldLabel] = oldValue + klog.Infof("Added annotation %s=%s for resource %s.%s", annotatedOldLabel, oldValue, object.Namespace, object.Name) + updated = true + } + // return if something was updated + return updated +} + +func updateLabelAnnotationMetadata(object *metav1.ObjectMeta, updateAppId bool, updateOwner bool) (updated bool) { + if object.Labels == nil { + object.Labels = make(map[string]string) + } + if object.Annotations == nil { + object.Annotations = map[string]string{} + } + + // Update BTP Application Identifier + if updateAppId { + var appDetails *appIdentifier + ok := false + appID := object.Labels[AnnotationBTPApplicationIdentifier] + if appID != "" { + if appDetails, ok = btpAppIdMap[appID]; !ok { + index := strings.Index(appID, ".") + appDetails = &appIdentifier{ + globalAccountId: appID[:index], + appName: appID[index+1:], + } + btpAppIdMap[appID] = appDetails + } + + if amendObjectMetadata(object, AnnotationBTPApplicationIdentifier, LabelBTPApplicationIdentifierHash, strings.Join([]string{appDetails.globalAccountId, appDetails.appName}, "."), sha1Sum(appDetails.globalAccountId, appDetails.appName)) { + updated = true + } + } + } + + // Update OwnerInfo if owner details exists + if updateOwner { + var ownerDetails *ownerInfo + owner := []string{} + + ok := false + ownerID := object.Labels[AnnotationOwnerIdentifier] + if ownerID != "" { + if ownerDetails, ok = ownerMap[ownerID]; !ok { + kind := "" + if strings.Index(ownerID, CAPApplication+".") == 0 { + kind = CAPApplication + ownerID = ownerID[15:] + } else if strings.Index(ownerID, CAPTenant+".") == 0 { + kind = CAPTenant + ownerID = ownerID[10:] + } + index := strings.Index(ownerID, ".") + ownerDetails = &ownerInfo{ + kind: kind, + ownerNamespace: ownerID[:index], + ownerName: ownerID[index+1:], + } + ownerMap[ownerID] = ownerDetails + } + if ownerDetails.kind != "" { + owner = append(owner, ownerDetails.kind) + } + owner = append(owner, ownerDetails.ownerNamespace, ownerDetails.ownerName) + + if amendObjectMetadata(object, AnnotationOwnerIdentifier, LabelOwnerIdentifierHash, strings.Join(owner, "."), sha1Sum(owner...)) { + updated = true + } + } + } + + return updated +} diff --git a/cmd/mtx-job/main.go b/cmd/mtx-job/main.go new file mode 100644 index 0000000..fe512ce --- /dev/null +++ b/cmd/mtx-job/main.go @@ -0,0 +1,543 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +package main + +import ( + "bytes" + "context" + "crypto/tls" + "crypto/x509" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net" + "net/http" + "net/url" + "os" + "path" + "strings" + "time" + + "github.com/sap/cap-operator/internal/util" + "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + "k8s.io/klog/v2" +) + +const ( + EnvXSUAAInstanceName = "XSUAA_INSTANCE_NAME" + EnvMTXServiceURL = "MTX_SERVICE_URL" + EnvMTXRequestType = "MTX_REQUEST_TYPE" + EnvMTXTenantId = "MTX_TENANT_ID" + EnvMTXPayload = "MTX_REQUEST_PAYLOAD" + EnvCredentialsPath = "CREDENTIALS_FILE_PATH" + EnvWaitForMTXTimeoutSeconds = "WAIT_FOR_MTX_TIMEOUT_SECONDS" + EnvVCAPServices = "VCAP_SERVICES" +) + +const ( + RequestTypeProvisioning = "provisioning" + RequestTypeDeprovisioning = "deprovisioning" + RequestTypeUpgrade = "upgrade" +) + +const ( + Authorization = "Authorization" + Bearer = "Bearer" + ContentType = "Content-Type" + ContentAppJson = "application/json" + ContentFormEncoded = "application/x-www-form-urlencoded" + ServiceClassXSUAA = "xsuaa" +) + +const ( + ErrorKillingProcess = "Error killing process" + FailedWith = "failed with" +) + +const ( + statusQueued = "QUEUED" + statusRunning = "RUNNING" + statusFailed = "FAILED" + statusSuccess = "SUCCESS" +) + +type asyncUpgradeResponse struct { + JobId string `json:"jobID"` +} + +type upgradeJobResponse struct { + Error string `json:"error"` + Status string `json:"status"` + Result upgradeJobResult `json:"result"` +} + +type upgradeJobResult struct { + Tenants map[string]tenantUpgradeResult `json:"tenants"` +} + +type tenantUpgradeResult struct { + Status string `json:"status"` + Message string `json:"message"` + BuildLogs string `json:"buildLogs"` +} + +func fetchXSUAAServiceCredentials() (*util.XSUAACredentials, error) { + vcap, ok := os.LookupEnv(EnvVCAPServices) + if !ok || vcap == "" { + return fetchXSUAAServiceCredentialsFromVolume() + } + return fetchXSUAAServiceCredentialsFromVCAP() +} + +func fetchXSUAAServiceCredentialsFromVCAP() (*util.XSUAACredentials, error) { + serviceInstanceName := os.Getenv(EnvXSUAAInstanceName) + raw := os.Getenv(EnvVCAPServices) + if raw == "" { + return nil, fmt.Errorf("could not read %s from environment", EnvVCAPServices) + } + var parsed map[string][]util.VCAPServiceInstance + err := json.Unmarshal([]byte(raw), &parsed) + if err != nil { + return nil, fmt.Errorf("error parsing VCAP_SERVICES: %s", err.Error()) + } + instances := parsed[ServiceClassXSUAA] + if len(instances) == 0 { + return nil, fmt.Errorf("could not find instances of service offering %s", ServiceClassXSUAA) + } + for _, i := range instances { + name := i.Name + if i.BindingName != "" { // if binding name is provided, use explicit instance name + if i.InstanceName == "" { + continue // instance name could not be identified + } + name = i.InstanceName + } + if name == serviceInstanceName { + return parseXSUAACredentials(i.Credentials) + } + } + + return nil, fmt.Errorf("credentials for service instance %s not found", serviceInstanceName) +} + +func parseXSUAACredentials(data interface{}) (credentials *util.XSUAACredentials, err error) { + defer func() { + if err != nil { + err = fmt.Errorf("could parse credentials for service %s: %s", ServiceClassXSUAA, err.Error()) + } + }() + creds, err := json.Marshal(data) + if err != nil { + return nil, err + } + var xsuaaCredentials util.XSUAACredentials + err = json.Unmarshal(creds, &xsuaaCredentials) + if err != nil { + return nil, err + } + return &xsuaaCredentials, nil +} + +func fetchXSUAAServiceCredentialsFromVolume() (*util.XSUAACredentials, error) { + instance := os.Getenv(EnvXSUAAInstanceName) + credPath := getXSUAACredentialsPath(instance) + readAttribute := func(attribute string) (string, error) { + data, err := os.ReadFile(path.Join(credPath, attribute)) + if err != nil { + return "", err + } + return string(data), nil + } + + var err error + parameters := &util.XSUAACredentials{} + parameters.CredentialType, err = readAttribute("credential-type") + if err != nil { + return nil, err + } + parameters.AuthUrl, err = readAttribute("url") + if err != nil { + return nil, err + } + parameters.ClientId, err = readAttribute("clientid") + if err != nil { + return nil, err + } + if parameters.CredentialType == "x509" { + parameters.CertificateUrl, err = readAttribute("certurl") + if err != nil { + return nil, err + } + parameters.Certificate, err = readAttribute("certificate") + if err != nil { + return nil, err + } + parameters.CertificateKey, err = readAttribute("key") + if err != nil { + return nil, err + } + } else { + parameters.ClientSecret, err = readAttribute("clientsecret") + if err != nil { + return nil, err + } + } + + return parameters, nil +} + +func getXSUAACredentialsPath(instance string) string { + basePath, ok := os.LookupEnv(EnvCredentialsPath) + if !ok { + basePath = "/etc/secrets/sapcp" + } + return path.Join(basePath, ServiceClassXSUAA, instance) +} + +type OAuthResponse struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` // bearer + ExpiresIn int `json:"expires_in"` + Scope string `json:"scope"` + JTI string `json:"jti"` +} + +func main() { + os.Exit(execute()) +} + +func execute() int { + var ( + err error // main error used for control flow + mtxURL *url.URL + token *OAuthResponse + client http.Client = http.Client{Timeout: time.Duration(5 * time.Second)} + ) + + defer func() { + // terminate mtx sidecar via tcp connect when completed + terminateMTXSidecar() + + // log final error, if any + if err != nil { + klog.Error("aborting with error: ", err.Error()) + } + }() + + if token, err = fetchOAuthToken(client); err != nil { + return 1 + } + + if mtxURLEnv, ok := os.LookupEnv(EnvMTXServiceURL); ok { + mtxURL, err = url.Parse(mtxURLEnv) + } else { + err = errors.New("could not identify mtx service URL from environment") + return 1 + } + + // wait for mtx instance by checking available tenants + if err = waitForMTX(mtxURL, client, token); err != nil { + return 1 + } + + // process request + if err = processRequest(mtxURL, client, token); err != nil { + return 1 + } + + return 0 +} + +func terminateMTXSidecar() { + klog.Info("Terminating by TCP connect..") + // TODO: make port configurable + conn, err := net.Dial("tcp", "localhost:8080") + if err != nil { + klog.Error("Error during mtx termination: ", err) + } + if conn != nil { + conn.Close() + klog.Info("Terminated..") + } +} + +func waitForMTX(mtxURL *url.URL, client http.Client, token *OAuthResponse) error { + t, ok := os.LookupEnv(EnvWaitForMTXTimeoutSeconds) + if !ok || t == "" { + t = "300" // default wait period of 5 minutes + } + d, err := time.ParseDuration(t + "s") + if err != nil { + return fmt.Errorf("error parsing wait duration: %s", err.Error()) + } + + wait, cancel := context.WithTimeout(context.Background(), d) + defer cancel() + + for { + select { + case <-wait.Done(): + return fmt.Errorf("error when waiting for mtx: %s", wait.Err().Error()) + default: + err := getTenants(mtxURL, client, token) + if err == nil { + return nil + } + klog.Warningf("waiting for mtx: %v", err) + } + time.Sleep(3 * time.Second) + } +} + +func getTenants(mtxURL *url.URL, client http.Client, token *OAuthResponse) error { + mtxURL.Path = path.Join("mtx", "v1/provisioning/tenant") + + req, err := http.NewRequest("GET", mtxURL.String(), nil) + if err != nil { + return err + } + req.Header.Set(Authorization, fmt.Sprintf("%s %s", Bearer, token.AccessToken)) + resp, err := client.Do(req) + if err != nil { + return err + } + + if resp.StatusCode >= 200 && resp.StatusCode <= 299 { + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + + klog.Infof("%s: %s", v1alpha1.CAPTenantResource, body) + return nil + } else { + return fmt.Errorf("mtx returned status %s", resp.Status) + } +} + +func fetchOAuthToken(client http.Client) (*OAuthResponse, error) { + parameters, err := fetchXSUAAServiceCredentials() + if err != nil { + return nil, fmt.Errorf("error when reading xsuaa credentials: %w", err) + } + + var req *http.Request + if parameters.CredentialType == "x509" { + req, err = createRequestWithX509Certificate(parameters, &client) + // reset client tls config + defer func() { client.Transport = &http.Transport{} }() + } else { + req, err = createRequestWithClientCredentials(parameters) + } + if err != nil { + return nil, fmt.Errorf("error preparing token request: %w", err) + } + + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("error during http call: status %w", err) + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("error reading response body: %w", err) + } + + var token OAuthResponse + err = json.Unmarshal(body, &token) + if err != nil { + return nil, fmt.Errorf("could not parse oauth response: %w", err) + } else { + klog.Infof("retrieved token with scope: %s", token.Scope) + } + + return &token, nil +} + +func prepareTokenRequest(clientID string, tokenURL string) (*http.Request, error) { + data := url.Values{} + data.Add("client_id", clientID) + data.Add("grant_type", "client_credentials") + req, err := http.NewRequest(http.MethodPost, tokenURL+"/oauth/token", strings.NewReader(data.Encode())) + if err != nil { + return nil, fmt.Errorf("error when creating request: %w", err) + } + req.Header.Set(ContentType, ContentFormEncoded) + return req, nil +} + +func createRequestWithClientCredentials(parameters *util.XSUAACredentials) (*http.Request, error) { + req, err := prepareTokenRequest(parameters.ClientId, parameters.AuthUrl) + if err != nil { + return nil, err + } + req.Header.Set(Authorization, "Basic "+base64.URLEncoding.EncodeToString([]byte(parameters.ClientId+":"+parameters.ClientSecret))) + return req, nil +} + +func createRequestWithX509Certificate(parameters *util.XSUAACredentials, client *http.Client) (*http.Request, error) { + // Read the key pair to create certificate + cert, err := tls.X509KeyPair([]byte(parameters.Certificate), []byte(parameters.CertificateKey)) + if err != nil { + return nil, err + } + + // Use system certificate pool and add add certificate to it + caCertPool, err := x509.SystemCertPool() + if err != nil { + return nil, err + } + caCertPool.AppendCertsFromPEM([]byte(parameters.Certificate)) + + // setup TLS configuration for client + tlsConfig := &tls.Config{ + RootCAs: caCertPool, + Certificates: []tls.Certificate{cert}, + } + if t, ok := client.Transport.(*http.Transport); ok { + t.TLSClientConfig = tlsConfig + } else { + client.Transport = &http.Transport{TLSClientConfig: tlsConfig} + } + + return prepareTokenRequest(parameters.ClientId, parameters.CertificateUrl) +} + +func processRequest(mtxURL *url.URL, client http.Client, token *OAuthResponse) error { + switch os.Getenv(EnvMTXRequestType) { + case RequestTypeProvisioning: + return processSubscription(RequestTypeProvisioning, mtxURL, client, token) + case RequestTypeDeprovisioning: + return processSubscription(RequestTypeDeprovisioning, mtxURL, client, token) + case RequestTypeUpgrade: + return processUpgrade(mtxURL, client, token) + default: + return errors.New("unknown mtx request type") + } +} + +func processSubscription(requestType string, mtxURL *url.URL, client http.Client, token *OAuthResponse) error { + tenantId := os.Getenv(EnvMTXTenantId) + + client.Timeout = 10 * time.Minute + mtxURL.Path = path.Join("mtx", "v1/provisioning/tenant", tenantId) + reqBody := bytes.NewBufferString(os.Getenv(EnvMTXPayload)) + var ( + req *http.Request + err error + ) + if requestType == RequestTypeProvisioning { + req, err = http.NewRequest("PUT", mtxURL.String(), reqBody) + } else { + req, err = http.NewRequest("DELETE", mtxURL.String(), reqBody) + } + if err != nil { + return fmt.Errorf("error when creating request for %s: %w", requestType, err) + } + req.Header.Set(ContentType, ContentAppJson) + req.Header.Set(Authorization, fmt.Sprintf("%s %s", Bearer, token.AccessToken)) + resp, err := client.Do(req) + if err != nil { + return err + } + + klog.Infof("%s %s %s response code: %d", v1alpha1.CAPTenantKind, tenantId, requestType, resp.StatusCode) + if resp.StatusCode >= 200 && resp.StatusCode <= 299 { + return nil + } else { + return fmt.Errorf("%s for %s %s %s: %d", requestType, v1alpha1.CAPTenantKind, tenantId, FailedWith, resp.StatusCode) + } +} + +func processUpgrade(mtxURL *url.URL, client http.Client, token *OAuthResponse) error { + tenantId := os.Getenv(EnvMTXTenantId) + client.Timeout = 10 * time.Minute + mtxURL.Path = "mtx/v1/model/asyncUpgrade" + reqBody := bytes.NewBufferString(os.Getenv(EnvMTXPayload)) + + req, err := http.NewRequest("POST", mtxURL.String(), reqBody) + + if err != nil { + return fmt.Errorf("error when creating request for upgrade: %v", err) + } + req.Header.Set(ContentType, ContentAppJson) + req.Header.Set(Authorization, fmt.Sprintf("%s %s", Bearer, token.AccessToken)) + resp, err := client.Do(req) + if err != nil { + return err + } + + klog.Infof("%s %s upgrade response code: %d", v1alpha1.CAPTenantKind, tenantId, resp.StatusCode) + if resp.StatusCode >= 200 && resp.StatusCode <= 299 { + decoder := json.NewDecoder(resp.Body) + defer resp.Body.Close() + var asyncUpgradeResp asyncUpgradeResponse + err = decoder.Decode(&asyncUpgradeResp) + if err != nil { + klog.Errorf("Error parsing response for async upgrade: %v", err) + return err + } + // Check the final job status + return checkJobStatus(asyncUpgradeResp.JobId, tenantId, mtxURL, client, token) + + } else { + return fmt.Errorf("upgrade for %s %s %s: %d", v1alpha1.CAPTenantKind, tenantId, FailedWith, resp.StatusCode) + } +} + +func checkJobStatus(jobId string, tenantId string, mtxURL *url.URL, client http.Client, token *OAuthResponse) error { + // Create Request for fetching mtx upgrade Job status + mtxURL.Path = "/mtx/v1/model/status/" + jobId + req, err := http.NewRequest("GET", mtxURL.String(), nil) + if err != nil { + return fmt.Errorf("error when creating request for %s(%s) upgrade job status: %w", v1alpha1.CAPTenantKind, tenantId, err) + } + req.Header.Set(ContentType, ContentAppJson) + req.Header.Set(Authorization, fmt.Sprintf("%s %s", Bearer, token.AccessToken)) + // wait until the final status of Upgrade job is received + success, err := fetchJobStatus(tenantId, client, req) + if err != nil || !success { + return fmt.Errorf("upgrade job for %s: %s %s: %w", v1alpha1.CAPTenantKind, tenantId, FailedWith, err) + } + // Upgrade Job was Successful + return nil +} + +// Wait for and check mtx upgrade job result until a final status is received +func fetchJobStatus(tenantId string, client http.Client, req *http.Request) (bool, error) { + // Send the Get Request to fetch mtx asyncUpgrade Job status + jobResp, err := client.Do(req) + if err != nil { + return false, err + } + // Parse the respnose + decoder := json.NewDecoder(jobResp.Body) + defer jobResp.Body.Close() + var upgradeJobResp upgradeJobResponse + err = decoder.Decode(&upgradeJobResp) + if err != nil { + klog.Errorf("parsing response for async upgrade of %s: %s%s%v", v1alpha1.CAPTenantKind, tenantId, FailedWith, err) + return false, err + } + // Re-trigger after sleeping for 10s if status is not "FINISHED" + // TODO: check if some of these should be moved to own go routines + if upgradeJobResp.Status == statusQueued || upgradeJobResp.Status == statusRunning { + time.Sleep(10 * time.Second) + return fetchJobStatus(tenantId, client, req) + } else if upgradeJobResp.Status == statusFailed { + // Job Failure + return false, fmt.Errorf("aync upgrade job for %s: %s %s: %s", v1alpha1.CAPTenantKind, tenantId, FailedWith, upgradeJobResp.Error) + } + + // Check tenant upgrade status for the current tenant and return error if needed + if upgradeJobResp.Result.Tenants[tenantId].Status != statusSuccess { + return false, fmt.Errorf("upgrade of %s: %s %s: %s", v1alpha1.CAPTenantKind, tenantId, FailedWith, upgradeJobResp.Result.Tenants[tenantId].Message) + } + // tenant upgrade was successful + klog.Infof("%s upgrade for %s successful!", v1alpha1.CAPTenantKind, tenantId) + return true, nil +} diff --git a/cmd/mtx-job/main_test.go b/cmd/mtx-job/main_test.go new file mode 100644 index 0000000..111f5be --- /dev/null +++ b/cmd/mtx-job/main_test.go @@ -0,0 +1,510 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +package main + +import ( + "context" + "crypto/tls" + "crypto/x509" + "encoding/base64" + "encoding/json" + "fmt" + "io/ioutil" + "net" + "net/http" + "net/http/httptest" + "net/url" + "os" + "path" + "strings" + "testing" + + "github.com/sap/cap-operator/internal/util" +) + +type testPayload struct { + Test string `json:"test"` +} + +type tlsAttributes struct { + certificate string + key string +} + +func readCertificateData() *struct { + cacert []byte + cert []byte + key []byte +} { + cacert, _ := ioutil.ReadFile("../server/internal/testdata/rootCA.pem") + cert, _ := ioutil.ReadFile("../server/internal/testdata/auth.service.local.crt") + key, _ := ioutil.ReadFile("../server/internal/testdata/auth.service.local.key") + return &struct { + cacert []byte + cert []byte + key []byte + }{ + cacert: cacert, + cert: cert, + key: key, + } +} + +func createTestServer(ctx context.Context, t *testing.T, handler http.HandlerFunc, enableTLS bool, useVCAP bool) (string, func(), error) { + var ( + tlsInfo *tlsAttributes = nil + serverURL string + ) + // create test server + ts := httptest.NewUnstartedServer(handler) + if enableTLS { + // Append CA cert to the system pool + rootCAs, _ := x509.SystemCertPool() + if rootCAs == nil { + rootCAs = x509.NewCertPool() + } + certInfo := readCertificateData() + rootCAs.AppendCertsFromPEM(certInfo.cacert) + cert, err := tls.X509KeyPair(certInfo.cert, certInfo.key) + if err != nil { + return "", nil, err + } + ts.TLS = &tls.Config{Certificates: []tls.Certificate{cert}, RootCAs: rootCAs} + ts.StartTLS() + + tlsInfo = &tlsAttributes{ + certificate: string(certInfo.cert), + key: string(certInfo.key), + } + serverURL = ts.Listener.Addr().String() + } else { + ts.Start() + serverURL = ts.URL + } + go func() { + <-ctx.Done() + ts.Close() + }() + + basePath, err := createCredentialsForTesting(ts.URL, tlsInfo, useVCAP) + return serverURL, func() { + if !useVCAP { + os.RemoveAll(basePath) + } else { + os.Unsetenv(EnvVCAPServices) + } + }, err +} + +func createCredentialsForTesting(url string, tls *tlsAttributes, useVCAP bool) (string, error) { + instance := "test-uaa" + os.Setenv(EnvXSUAAInstanceName, instance) + + files := map[string]string{ + "clientid": "xsuaa-client-id", + "clientsecret": "xsuaa-secret", + "url": url, + "credential-type": "instance-secret", + "certurl": "https://cert.auth.service.local", + } + if tls != nil { + files["credential-type"] = "x509" + files["certificate"] = tls.certificate + files["key"] = tls.key + } + + if useVCAP { + vcap := map[string][]util.VCAPServiceInstance{"xsuaa": {{ + Name: instance, + InstanceName: instance, + Label: "xsuaa", + Tags: []string{"xsuaa"}, + Credentials: files, + }}} + data, _ := json.Marshal(vcap) + os.Setenv(EnvVCAPServices, string(data)) + return "", nil + } + + basePath, err := os.MkdirTemp("", "test-*") + if err != nil { + return "", fmt.Errorf("could not create temp dir: %s", err.Error()) + } + os.Setenv(EnvCredentialsPath, basePath) + + tmpDir := path.Join(basePath, "xsuaa", instance) + if err := os.MkdirAll(tmpDir, 0755); err != nil { + return "", fmt.Errorf("could not create temp dir: %s", err.Error()) + } + var createAttributeFile = func(ch chan<- error, file string, value string) { + ch <- os.WriteFile(path.Join(tmpDir, file), []byte(value), 0755) + } + + ch := make(chan error, len(files)) + for k, v := range files { + go createAttributeFile(ch, k, v) + } + for i := 0; i < len(files); i++ { + err := <-ch + if err != nil { + return "", fmt.Errorf("could not create temp file") + } + } + + return basePath, nil +} + +func TestWaitForMTX(t *testing.T) { + ready, timeout := false, false + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/mtx/v1/provisioning/tenant" { + t.Error("wrong URI path") + } + if ready && !timeout { + body, _ := json.Marshal([]string{"t1", "t2"}) + w.Write(body) + } else { + ready = true + w.WriteHeader(http.StatusServiceUnavailable) + } + })) + defer ts.Close() + mtxURL, _ := url.Parse(ts.URL) + token := &OAuthResponse{AccessToken: "sample-access-token", TokenType: "bearer"} + + // test with one iteration expecting error and the next successful + err := waitForMTX(mtxURL, http.Client{}, token) + if err != nil { + t.Error("unexpected error") + } + + // test timeout + timeout = true + os.Setenv(EnvWaitForMTXTimeoutSeconds, "2") + err = waitForMTX(mtxURL, http.Client{}, token) + if err == nil { + t.Error("expected context deadline error") + } else { + if err.Error() != "error when waiting for mtx: context deadline exceeded" { + t.Errorf("wrong error: %s", err.Error()) + } + } +} + +func TestFetchOAuthTokenWithClientSecret(t *testing.T) { + var unauthorized bool + handler := func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + t.Error("expected POST operation") + } + if r.Header.Get(ContentType) != ContentFormEncoded { + t.Errorf("expected %s %s", ContentType, ContentFormEncoded) + } + if r.Header.Get(Authorization) != fmt.Sprintf("Basic %s", base64.URLEncoding.EncodeToString([]byte("xsuaa-client-id:xsuaa-secret"))) { + t.Errorf("incorrect authorization header") + } + if !unauthorized { + w.Write([]byte("{\"access_token\":\"sample-access-token\",\"token_type\":\"bearer\",\"expires_in\":43199}")) + } else { + w.WriteHeader(http.StatusUnauthorized) + } + } + + _, tearDown, err := createTestServer(context.TODO(), t, handler, false, false) + if err != nil { + t.Fatal(err.Error()) + } + defer tearDown() + + // test valid request + token, err := fetchOAuthToken(http.Client{}) + if err != nil { + t.Fatal(err.Error()) + } + if token.TokenType != "bearer" || token.AccessToken != "sample-access-token" { + t.Error("unexpected response body") + } + + // test unauthorized + unauthorized = true + _, err = fetchOAuthToken(http.Client{}) + if err == nil { + t.Error("expected error") + } +} + +func TestFetchOAuthTokenWithX509Cert(t *testing.T) { + handler := func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + t.Error("expected POST operation") + } + if r.Header.Get(ContentType) != ContentFormEncoded { + t.Errorf("expected %s %s", ContentType, ContentFormEncoded) + } + w.Write([]byte("{\"access_token\":\"sample-access-token\",\"token_type\":\"bearer\",\"expires_in\":43199}")) + } + + type testConfig struct { + testCase string + useVCAP bool + } + + for _, p := range []testConfig{ + {testCase: "fetch token using x509 certificate using credentials from volume", useVCAP: false}, + {testCase: "fetch token using x509 certificate using credentials from VCAP_SERVICES", useVCAP: true}, + } { + t.Run(p.testCase, func(t *testing.T) { + serverAddr, tearDown, err := createTestServer(context.TODO(), t, handler, true, p.useVCAP) + if err != nil { + t.Fatal(err.Error()) + } + defer tearDown() + + dial := func(ctx context.Context, network, addr string) (net.Conn, error) { + if strings.Contains(addr, "auth.service.local:") { + addr = serverAddr + } + return net.Dial(network, addr) + } + client := http.Client{} + if t, ok := client.Transport.(*http.Transport); ok { + t.DialContext = dial + } else { + client.Transport = &http.Transport{DialContext: dial} + } + + // test valid request + token, err := fetchOAuthToken(client) + if err != nil { + t.Fatal(err.Error()) + } + if token.TokenType != "bearer" || token.AccessToken != "sample-access-token" { + t.Error("unexpected response body") + } + }) + } +} + +func TestSubscription(t *testing.T) { + mtx := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/mtx/v1/provisioning/tenant/test-tenant-id" { + t.Errorf("expected route with tenant id test-tenant-id") + } + if r.Header.Get(ContentType) != ContentAppJson { + t.Errorf("expected %s %s", ContentType, ContentAppJson) + } + if r.Header.Get(Authorization) != "Bearer sample-token" { + t.Error("expected oauth token for authorization") + } + content, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Errorf("could not read payload: %s", err.Error()) + } + var p testPayload + err = json.Unmarshal(content, &p) + if err != nil { + t.Errorf("could not read payload: %s", err.Error()) + } + switch p.Test { + case "provisioning-ok": + if r.Method != http.MethodPut { + t.Errorf("expected %s", http.MethodPut) + } + w.WriteHeader(http.StatusOK) + case "provisioning-fail": + w.WriteHeader(http.StatusBadRequest) + case "deprovisioning-ok": + if r.Method != http.MethodDelete { + t.Errorf("expected %s", http.MethodDelete) + } + w.WriteHeader(http.StatusOK) + default: + t.Error("unknown test payload") + } + })) + defer mtx.Close() + mtxURL, _ := url.Parse(mtx.URL) + os.Setenv(EnvMTXTenantId, "test-tenant-id") + + os.Setenv(EnvMTXRequestType, RequestTypeProvisioning) + os.Setenv(EnvMTXPayload, "{\"test\":\"provisioning-ok\"}") + err := processRequest(mtxURL, http.Client{}, &OAuthResponse{AccessToken: "sample-token", TokenType: "bearer"}) + if err != nil { + t.Errorf("unexpected error: %s", err.Error()) + } + + os.Setenv(EnvMTXPayload, "{\"test\":\"provisioning-fail\"}") + err = processRequest(mtxURL, http.Client{}, &OAuthResponse{AccessToken: "sample-token", TokenType: "bearer"}) + if err == nil { + t.Errorf("expected error") + } + + os.Setenv(EnvMTXRequestType, RequestTypeDeprovisioning) + os.Setenv(EnvMTXPayload, "{\"test\":\"deprovisioning-ok\"}") + err = processRequest(mtxURL, http.Client{}, &OAuthResponse{AccessToken: "sample-token", TokenType: "bearer"}) + if err != nil { + t.Errorf("unexpected error: %s", err.Error()) + } +} + +func TestUpgrade(t *testing.T) { + jobPreviousStatus := "" + mtx := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get(Authorization) != "Bearer sample-token" { + t.Error("expected oauth token for authorization") + } + + switch r.URL.Path { + case "/mtx/v1/model/asyncUpgrade": + if r.Method != http.MethodPost { + t.Errorf("expected %s", http.MethodPost) + } + jobPreviousStatus = "" + if r.Header.Get(ContentType) != ContentAppJson { + t.Errorf("expected %s %s", ContentType, ContentAppJson) + } + content, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Errorf("could not read payload: %s", err.Error()) + } + var p testPayload + err = json.Unmarshal(content, &p) + if err != nil { + t.Errorf("could not read payload: %s", err.Error()) + } + switch p.Test { + case "upgrade-job-ok": + w.WriteHeader(http.StatusCreated) + w.Write([]byte("{\"jobID\":\"job-id-ok\"}")) + case "upgrade-job-fail": + w.WriteHeader(http.StatusCreated) + w.Write([]byte("{\"jobID\":\"job-id-fail\"}")) + case "upgrade-job-queue-tenant-fail": + w.WriteHeader(http.StatusCreated) + w.Write([]byte("{\"jobID\":\"job-id-queue-fail\"}")) + case "upgrade-fail": + w.WriteHeader(http.StatusInternalServerError) + } + case "/mtx/v1/model/status/job-id-ok": + if r.Method != http.MethodGet { + t.Errorf("expected %s", http.MethodGet) + } + jobResponse := upgradeJobResponse{Status: "FINISHED", Result: upgradeJobResult{Tenants: map[string]tenantUpgradeResult{"test-tenant-id": {Status: "SUCCESS"}}}} + body, _ := json.Marshal(jobResponse) + w.Write(body) + case "/mtx/v1/model/status/job-id-fail": + if r.Method != http.MethodGet { + t.Errorf("expected %s", http.MethodGet) + } + jobResponse := upgradeJobResponse{Status: "FAILED", Error: "memory dump"} + body, _ := json.Marshal(jobResponse) + w.Write(body) + case "/mtx/v1/model/status/job-id-queue-fail": + if r.Method != http.MethodGet { + t.Errorf("expected %s", http.MethodGet) + } + var jobResponse upgradeJobResponse + if jobPreviousStatus == "" { + jobResponse = upgradeJobResponse{Status: "QUEUED"} + jobPreviousStatus = "QUEUED" + } else { + jobResponse = upgradeJobResponse{Status: "FINISHED", Result: upgradeJobResult{Tenants: map[string]tenantUpgradeResult{"test-tenant-id": {Status: "FAILED", Message: "tenant upgrade failed"}}}} + } + body, _ := json.Marshal(jobResponse) + w.Write(body) + } + })) + defer mtx.Close() + mtxURL, _ := url.Parse(mtx.URL) + os.Setenv(EnvMTXRequestType, RequestTypeUpgrade) + os.Setenv(EnvMTXTenantId, "test-tenant-id") + + os.Setenv(EnvMTXPayload, "{\"test\":\"upgrade-job-ok\"}") + err := processRequest(mtxURL, http.Client{}, &OAuthResponse{AccessToken: "sample-token", TokenType: "bearer"}) + if err != nil { + t.Errorf("unexpected error: %s", err.Error()) + } + + os.Setenv(EnvMTXPayload, "{\"test\":\"upgrade-fail\"}") + err = processRequest(mtxURL, http.Client{}, &OAuthResponse{AccessToken: "sample-token", TokenType: "bearer"}) + if err == nil { + t.Errorf("expected error") + } + + os.Setenv(EnvMTXPayload, "{\"test\":\"upgrade-job-fail\"}") + err = processRequest(mtxURL, http.Client{}, &OAuthResponse{AccessToken: "sample-token", TokenType: "bearer"}) + if err == nil { + t.Errorf("expected error") + } + + os.Setenv(EnvMTXPayload, "{\"test\":\"upgrade-job-queue-tenant-fail\"}") + err = processRequest(mtxURL, http.Client{}, &OAuthResponse{AccessToken: "sample-token", TokenType: "bearer"}) + if err == nil { + t.Errorf("expected error") + } +} + +func TestExecute(t *testing.T) { + provisioningError, mtxTimeout := false, false + handler := func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/oauth/token": + w.Write([]byte("{\"access_token\":\"sample-access-token\",\"token_type\":\"bearer\",\"expires_in\":43199}")) + case "/mtx/v1/provisioning/tenant": + if mtxTimeout { + w.WriteHeader(http.StatusBadGateway) + } else { + w.Write([]byte("[]")) + } + case "/mtx/v1/provisioning/tenant/test-tenant-id": + if provisioningError { + w.WriteHeader(http.StatusInternalServerError) + } else { + w.WriteHeader(http.StatusOK) + } + } + } + + serverURL, tearDown, err := createTestServer(context.TODO(), t, handler, false, false) + if err != nil { + t.Fatal(err.Error()) + } + defer tearDown() + + // test w/o mtx url + os.Unsetenv(EnvMTXServiceURL) + code := execute() + if code != 1 { + t.Error("expected error") + } + + // test w/o request type + provisioningError = true + os.Setenv(EnvMTXServiceURL, serverURL) + os.Unsetenv(EnvMTXRequestType) + code = execute() + if code != 1 { + t.Error("expected error") + } + + // test with successful simulated provisioning + provisioningError = false + os.Setenv(EnvMTXTenantId, "test-tenant-id") + os.Setenv(EnvMTXPayload, "{}") + os.Setenv(EnvMTXRequestType, RequestTypeProvisioning) + code = execute() + if code != 0 { + t.Error("expected exit code 0") + } + + // test timeout waiting for mtx + mtxTimeout = true + os.Setenv(EnvMTXServiceURL, serverURL) + os.Setenv(EnvWaitForMTXTimeoutSeconds, "2") + code = execute() + if code != 1 { + t.Error("expected error") + } +} diff --git a/cmd/server/internal/client.go b/cmd/server/internal/client.go new file mode 100644 index 0000000..84e5e69 --- /dev/null +++ b/cmd/server/internal/client.go @@ -0,0 +1,19 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +package handler + +import ( + "net/http" +) + +type httpClientGenerator interface { + NewHTTPClient() *http.Client +} + +type httpClientGeneratorImpl struct{} + +func (facade *httpClientGeneratorImpl) NewHTTPClient() *http.Client { + return &http.Client{} +} diff --git a/cmd/server/internal/handler.go b/cmd/server/internal/handler.go new file mode 100644 index 0000000..a01c82d --- /dev/null +++ b/cmd/server/internal/handler.go @@ -0,0 +1,585 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +package handler + +import ( + "bytes" + "context" + "crypto/sha1" + "crypto/tls" + "crypto/x509" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "strings" + "sync" + "time" + + k8sErrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/kubernetes" + "k8s.io/klog/v2" + + "github.com/sap/cap-operator/internal/util" + "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + "github.com/sap/cap-operator/pkg/client/clientset/versioned" +) + +const ( + LabelBTPApplicationIdentifierHash = "sme.sap.com/btp-app-identifier-hash" + LabelTenantId = "sme.sap.com/btp-tenant-id" +) + +const ( + ResourceCreated = "resource created successfully" + ResourceFound = "resource exists" + ResourceDeleted = "resource deleted successfully" + ResourceNotFound = "resource not found" +) + +const ErrorOccured = "Error occured " +const InvalidRequestMethod = "invalid request method" +const AuthorizationCheckFailed = "authorization check failed" +const BearerPrefix = "Bearer " +const BasicPrefix = "Basic " + +const ( + CallbackSucceeded = "SUCCEEDED" + CallbackFailed = "FAILED" + ProvisioningSucceededMessage = "Provisioning successful" + ProvisioningFailedMessage = "Provisioning failed" + DeprovisioningSucceededMessage = "Deprovisioning successful" + DeprovisioningFailedMessage = "Deprovisioning failed" +) + +type Result struct { + Tenant *v1alpha1.CAPTenant + Message string +} + +type SubscriptionHandler struct { + Clientset versioned.Interface + KubeClienset kubernetes.Interface + httpClientGenerator httpClientGenerator +} + +type UserInfo struct { + UserId string `json:"userId"` + UserName string `json:"userName"` + Email string `json:"email"` + SubIdp string `json:"subIdp"` + Sub string `json:"sub"` +} + +type AdditionalInformation struct { + Clientid string `json:"clientid"` + Clientsecret string `json:"clientsecret"` + Tokenurl string `json:"tokenurl"` +} + +type DeprovisioningRequest struct { + SubscriptionAppId string `json:"subscriptionAppId"` + SubscriptionAppName string `json:"subscriptionAppName"` + SubscribedTenantId string `json:"subscribedTenantId"` + SubscribedZoneId string `json:"subscribedZoneId"` + SubscribedSubdomain string `json:"subscribedSubdomain"` + SubscribedSubaccountId string `json:"subscribedSubaccountId"` + SubscribedCrmId string `json:"subscribedCrmId"` + SubscriptionAppPlan string `json:"subscriptionAppPlan"` + SubscriptionAppAmount string `json:"subscriptionAppAmount"` + DependentServiceInstanceAppIds string `json:"dependentServiceInstanceAppIds"` + GlobalAccountGUID string `json:"globalAccountGUID"` + UserId string `json:"userId"` + UserInfo UserInfo `json:"userInfo"` +} + +type ProvisioningRequest struct { + SubscriptionAppId string `json:"subscriptionAppId"` + SubscriptionAppName string `json:"subscriptionAppName"` + SubscribedTenantId string `json:"subscribedTenantId"` + SubscribedZoneId string `json:"subscribedZoneId"` + SubscribedSubdomain string `json:"subscribedSubdomain"` + SubscribedSubaccountId string `json:"subscribedSubaccountId"` + SubscribedLicenseType string `json:"subscribedLicenseType"` + SubscribedCrmId string `json:"subscribedCrmId"` + SubscriptionAppPlan string `json:"subscriptionAppPlan"` + SubscriptionAppAmount string `json:"subscriptionAppAmount"` + DependentServiceInstanceAppIds string `json:"dependentServiceInstanceAppIds"` + GlobalAccountGUID string `json:"globalAccountGUID"` + EventType string `json:"eventType"` + AdditionalInformation AdditionalInformation `json:"additionalInformation"` + UserId string `json:"userId"` + UserInfo UserInfo `json:"userInfo"` +} + +type GetRequest struct { + SubscriptionAppName string `json:"subscriptionAppName"` + GlobalAccountGUID string `json:"globalAccountGUID"` + SubscribedTenantId string `json:"subscribedTenantId"` +} + +type CallbackResponse struct { + Status string `json:"status"` + Message string `json:"message"` + SubscriptionUrl string `json:"subscriptionUrl"` +} + +type OAuthResponse struct { + AccessToken string `json:"access_token"` +} + +func (s *SubscriptionHandler) CreateTenant(req *http.Request) *Result { + klog.Info("Create Tenant triggered") + var created = false + // Get the relevant provisioning request + decoder := json.NewDecoder(req.Body) + var reqType ProvisioningRequest + err := decoder.Decode(&reqType) + if err != nil { + klog.Error(ErrorOccured, err.Error()) + return &Result{Tenant: nil, Message: err.Error()} + } + + // Check if CAPApplication instance for the given btpApp exists + ca, err := s.checkCAPApp(reqType.GlobalAccountGUID, reqType.SubscriptionAppName) + if err != nil { + klog.Error(ErrorOccured, err.Error()) + return &Result{Tenant: nil, Message: err.Error()} + } + + // fetch SaaS Registry and XSUAA information + saasData, uaaData := s.getServiceDetails(ca) + if saasData == nil || uaaData == nil { + return &Result{Tenant: nil, Message: ResourceNotFound} + } + + // validate token + err = s.checkAuthorization(req.Header.Get("Authorization"), saasData, uaaData) + if err != nil { + return &Result{Tenant: nil, Message: err.Error()} + } + + // Check if A CRO for CAPTenant already exists + tenant := s.getTenant(reqType.GlobalAccountGUID, reqType.SubscriptionAppName, reqType.SubscribedTenantId, ca.Namespace).Tenant + + // If the resource doesn't exist, we'll create it + if tenant == nil { + created = true + klog.Info("Creating Tenant") + tenant, _ = s.Clientset.SmeV1alpha1().CAPTenants(ca.Namespace).Create(context.TODO(), &v1alpha1.CAPTenant{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: ca.Name + "-", + Namespace: ca.Namespace, + Labels: map[string]string{ + LabelBTPApplicationIdentifierHash: sha1Sum(reqType.GlobalAccountGUID, reqType.SubscriptionAppName), + LabelTenantId: reqType.SubscribedTenantId, + }, + }, + Spec: v1alpha1.CAPTenantSpec{ + CAPApplicationInstance: ca.Name, + BTPTenantIdentification: v1alpha1.BTPTenantIdentification{ + SubDomain: reqType.SubscribedSubdomain, + TenantId: reqType.SubscribedTenantId, + }, + }, + }, metav1.CreateOptions{}) + } + + // TODO: consider retying tenant creation if it is in Error state + if tenant != nil { + s.initializeCallback(tenant.Name, ca, saasData, req, reqType.SubscribedSubdomain, true) + } + + // Tenant created/exists + message := func(isCreated bool) string { + if isCreated { + return ResourceCreated + } else { + return ResourceFound + } + } + klog.V(2).Info("Done with create: ", message, tenant) + return &Result{Tenant: tenant, Message: message(created)} +} + +func (s *SubscriptionHandler) getTenant(globalAccountGUID string, btpAppName string, tenantId string, namespace string) *Result { + labelSelector, err := labels.ValidatedSelectorFromSet(map[string]string{ + LabelBTPApplicationIdentifierHash: sha1Sum(globalAccountGUID, btpAppName), + LabelTenantId: tenantId, + }) + if err != nil { + klog.Error("Error occurred in getTenant", err.Error()) + return &Result{Tenant: nil, Message: err.Error()} + } + + ctList, err := s.Clientset.SmeV1alpha1().CAPTenants(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: labelSelector.String()}) + if err != nil { + klog.Error("Error occured in getTenant", err.Error()) + return &Result{Tenant: nil, Message: err.Error()} + } + if len(ctList.Items) == 0 { + klog.Info("No tenant found") + return &Result{Tenant: nil, Message: ResourceNotFound} + } + // Assume only 1 tenant actually matches the selector! + klog.V(2).Info("Tenant found", &ctList.Items[0]) + return &Result{Tenant: &ctList.Items[0], Message: ResourceFound} +} + +func (s *SubscriptionHandler) DeleteTenant(req *http.Request) *Result { + klog.Info("Delete Tenant triggered") + // Get the relevant deprovisioning request + decoder := json.NewDecoder(req.Body) + var reqType DeprovisioningRequest + err := decoder.Decode(&reqType) + if err != nil { + klog.Error(ErrorOccured, err.Error()) + return &Result{Tenant: nil, Message: err.Error()} + } + + // Check if CAPApplication instance for the given btpApp exists + ca, err := s.checkCAPApp(reqType.GlobalAccountGUID, reqType.SubscriptionAppName) + if err != nil { + klog.Error(ErrorOccured, err.Error()) + return &Result{Tenant: nil, Message: err.Error()} + } + + // fetch SaaS Registry and XSUAA information + saasData, uaaData := s.getServiceDetails(ca) + if saasData == nil || uaaData == nil { + return &Result{Tenant: nil, Message: ResourceNotFound} + } + if saasData == nil || uaaData == nil { + return &Result{Tenant: nil, Message: ResourceNotFound} + } + + // validate token + err = s.checkAuthorization(req.Header.Get("Authorization"), saasData, uaaData) + if err != nil { + return &Result{Tenant: nil, Message: err.Error()} + } + + tenant := s.getTenant(reqType.GlobalAccountGUID, reqType.SubscriptionAppName, reqType.SubscribedTenantId, ca.Namespace).Tenant + + tenantName := "foo" //TODO + if tenant != nil { + tenantName = tenant.Name + klog.Info("Tenant found, deleting") + err = s.Clientset.SmeV1alpha1().CAPTenants(tenant.Namespace).Delete(context.TODO(), tenant.Name, metav1.DeleteOptions{}) + if err != nil { + klog.Error("Error deleting tenant", err.Error()) + return &Result{Tenant: nil, Message: err.Error()} + } + } + + s.initializeCallback(tenantName, ca, saasData, req, reqType.SubscribedSubdomain, false) + + return &Result{Tenant: tenant, Message: ResourceDeleted} +} + +func (s *SubscriptionHandler) checkCAPApp(globalAccountId string, btpAppName string) (*v1alpha1.CAPApplication, error) { + labelSelector, err := labels.ValidatedSelectorFromSet(map[string]string{ + LabelBTPApplicationIdentifierHash: sha1Sum(globalAccountId, btpAppName), + }) + if err != nil { + return nil, err + } + + capAppsList, err := s.Clientset.SmeV1alpha1().CAPApplications(metav1.NamespaceAll).List(context.TODO(), metav1.ListOptions{LabelSelector: labelSelector.String()}) + if err != nil { + return nil, err + } + if len(capAppsList.Items) == 0 { + return nil, errors.New(ResourceNotFound) // TODO proper error message handling + } + // Assume only 1 app actually matches the selector! + return &capAppsList.Items[0], nil +} + +func (s *SubscriptionHandler) checkAuthorization(authHeader string, saasData *util.SaasRegistryCredentials, uaaData *util.XSUAACredentials) error { + if strings.Index(authHeader, BearerPrefix) != 0 { + return errors.New("expected bearer token") + } + + token := authHeader[7:] + err := VerifyXSUAAJWTToken(context.TODO(), token, &XSUAAConfig{ + UAADomain: saasData.UAADomain, + ClientID: saasData.ClientId, + XSAppName: uaaData.XSAppName, + RequiredScopes: []string{uaaData.XSAppName + ".Callback", uaaData.XSAppName + ".mtcallback"}, + }, s.httpClientGenerator.NewHTTPClient()) + if err != nil { + klog.Errorf("failed token validation: %s", err.Error()) + return errors.New(AuthorizationCheckFailed) + } + return nil +} + +func (s *SubscriptionHandler) initializeCallback(tenantName string, ca *v1alpha1.CAPApplication, saasData *util.SaasRegistryCredentials, req *http.Request, tenantSubDomain string, isProvisioning bool) { + appUrl := "https://" + tenantSubDomain + "." + ca.Spec.Domains.Primary + asyncCallbackPath := req.Header.Get("STATUS_CALLBACK") + klog.Infof("Subscription URL: %s, Async callback URL: %s", appUrl, asyncCallbackPath) + + go func() { + // create a context for tenant checks and outgoing requests + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Check tenant status asynchronously + klog.Info("Waiting for tenant status check...") + status := s.checkCAPTenantStatus(ctx, ca.Namespace, tenantName, isProvisioning, saasData.CallbackTimeoutMillis) + klog.Info("CAPTenant check result: ", status) + + s.handleAsyncCallback(ctx, saasData, status, asyncCallbackPath, appUrl, isProvisioning) + }() + + klog.Info("Waiting for async saas callback after checks...") +} + +func (s *SubscriptionHandler) checkCAPTenantStatus(ctx context.Context, tenantNamespace string, tenantName string, provisioning bool, callbackTimeoutMs string) bool { + asyncCallbackTimeout := 15 * time.Minute + if callbackTimeoutMs != "" { + asyncCallbackTimeout, _ = time.ParseDuration(callbackTimeoutMs + "ms") + } + + timedCtx, cancel := context.WithTimeout(ctx, asyncCallbackTimeout) // Assume tenants won't take over 15mins to be "Ready" + defer cancel() + + for { + select { + case <-timedCtx.Done(): + klog.Warningf("tenant status check: %s", timedCtx.Err().Error()) + return false + default: + capTenant, err := s.Clientset.SmeV1alpha1().CAPTenants(tenantNamespace).Get(context.TODO(), tenantName, metav1.GetOptions{}) + if k8sErrors.IsNotFound(err) { + klog.Info("No tenant found.. Exiting CAPTenant status check.") + if !provisioning { + return true + } + } + if capTenant != nil { + klog.Info("CAPTenant (tenantid: "+capTenant.Spec.TenantId+"), status: ", capTenant.Status.State) + if provisioning && (capTenant.Status.State == v1alpha1.CAPTenantStateReady || capTenant.Status.State == v1alpha1.CAPTenantStateProvisioningError) { + klog.Info("Exiting CAPTenant status check: ", capTenant.Status.State) + return capTenant.Status.State == v1alpha1.CAPTenantStateReady + } + } + time.Sleep(5 * time.Second) + } + } +} + +func (s *SubscriptionHandler) getServiceDetails(ca *v1alpha1.CAPApplication) (*util.SaasRegistryCredentials, *util.XSUAACredentials) { + var ( + wg sync.WaitGroup + saasData *util.SaasRegistryCredentials + uaaData *util.XSUAACredentials + ) + wg.Add(1) + go func() { + defer wg.Done() + saasData = s.getSaasDetails(ca) + }() + wg.Add(1) + go func() { + defer wg.Done() + uaaData = s.getXSUAADetails(ca) + }() + + wg.Wait() + return saasData, uaaData +} + +func (s *SubscriptionHandler) getSaasDetails(capApp *v1alpha1.CAPApplication) *util.SaasRegistryCredentials { + var ( + result *util.SaasRegistryCredentials = nil + err error + info *v1alpha1.ServiceInfo + ) + if info, err = s.getServiceInfo(capApp, "saas-registry"); err == nil { + result, err = util.ReadServiceCredentialsFromSecret[util.SaasRegistryCredentials](info, capApp.Namespace, s.KubeClienset) + } + if err != nil { + klog.Error("SaaS Registry credentials could not be read. Exiting..", err.Error()) + } + return result +} + +func (s *SubscriptionHandler) getXSUAADetails(capApp *v1alpha1.CAPApplication) *util.XSUAACredentials { + var ( + result *util.XSUAACredentials = nil + err error + info *v1alpha1.ServiceInfo + ) + info = util.GetXSUAAInfo(capApp.Spec.BTP.Services, capApp) + + if info == nil { + err = fmt.Errorf("could not find service with class %s in CAPApplication %s.%s", "xsuaa", capApp.Namespace, capApp.Name) + } else { + result, err = util.ReadServiceCredentialsFromSecret[util.XSUAACredentials](info, capApp.Namespace, s.KubeClienset) + } + + if err != nil { + klog.Error("XSUAA credentials could not be read. Exiting..", err.Error()) + } + return result +} + +func (s *SubscriptionHandler) getServiceInfo(ca *v1alpha1.CAPApplication, serviceClass string) (*v1alpha1.ServiceInfo, error) { + for i := range ca.Spec.BTP.Services { + if ca.Spec.BTP.Services[i].Class == serviceClass { + return &ca.Spec.BTP.Services[i], nil + } + } + return nil, fmt.Errorf("could not find service with class %s in CAPApplication %s.%s", serviceClass, ca.Namespace, ca.Name) +} + +func prepareTokenRequest(ctx context.Context, saasData *util.SaasRegistryCredentials, client *http.Client) (tokenReq *http.Request, err error) { + defer func() { + if err != nil { + err = fmt.Errorf("error preparing token request: %w", err) + } + }() + var ( + tokenURL string + ) + if saasData.CredentialType == "x509" { + tokenURL = saasData.CertificateUrl + "/oauth/token" + + // setup client for mTLS + cert, err := tls.X509KeyPair([]byte(saasData.Certificate), []byte(saasData.CertificateKey)) + if err != nil { + return nil, err + } + caCertPool, err := x509.SystemCertPool() + if err != nil { + return nil, err + } + caCertPool.AppendCertsFromPEM([]byte(saasData.Certificate)) + tlsConfig := &tls.Config{ + RootCAs: caCertPool, + Certificates: []tls.Certificate{cert}, + } + if t, ok := client.Transport.(*http.Transport); ok { + t.TLSClientConfig = tlsConfig + } else { + client.Transport = &http.Transport{TLSClientConfig: tlsConfig} + } + } else { + tokenURL = saasData.AuthUrl + "/oauth/token" + } + tokenData := url.Values{} + tokenData.Add("client_id", saasData.ClientId) + tokenData.Add("grant_type", "client_credentials") + + tokenReq, err = http.NewRequestWithContext(ctx, http.MethodPost, tokenURL, strings.NewReader(tokenData.Encode())) + if err != nil { + return nil, err + } + tokenReq.Header.Set("Content-Type", "application/x-www-form-urlencoded") + if saasData.CredentialType != "x509" { + tokenReq.Header.Set("Authorization", BasicPrefix+base64.StdEncoding.EncodeToString([]byte(saasData.ClientId+":"+saasData.ClientSecret))) + } + + return tokenReq, nil +} + +func (s *SubscriptionHandler) handleAsyncCallback(ctx context.Context, saasData *util.SaasRegistryCredentials, status bool, asyncCallbackPath string, appUrl string, isProvisioning bool) { + // Get OAuth token + tokenClient := s.httpClientGenerator.NewHTTPClient() + tokenReq, err := prepareTokenRequest(ctx, saasData, tokenClient) + if err != nil { + klog.Error(err.Error()) + return + } + klog.V(2).Info("Triggering OAuth: ", tokenReq) + + tokenResponse, err := tokenClient.Do(tokenReq) + if err != nil { + klog.Error("Error getting token for async callback: ", err.Error()) + return + } else { + klog.V(2).Info("Response from token handling for async callback: ", tokenResponse) + // Get the relevant OAuth request + decoder := json.NewDecoder(tokenResponse.Body) + var oAuthType OAuthResponse + err := decoder.Decode(&oAuthType) + if err != nil { + klog.Error("Error parsing token for async callback: ", err.Error()) + return + } + defer tokenResponse.Body.Close() + + checkMatch := func(match bool, trueVal string, falseVal string) string { + if match { + return trueVal + } + return falseVal + } + + payload, _ := json.Marshal(&CallbackResponse{ + Status: checkMatch(status, CallbackSucceeded, CallbackFailed), + Message: checkMatch(status, checkMatch(isProvisioning, ProvisioningSucceededMessage, DeprovisioningSucceededMessage), checkMatch(isProvisioning, ProvisioningFailedMessage, DeprovisioningFailedMessage)), + SubscriptionUrl: appUrl, + }) + callbackReq, _ := http.NewRequestWithContext(ctx, http.MethodPut, saasData.SaasManagerUrl+asyncCallbackPath, bytes.NewBuffer(payload)) + callbackReq.Header.Set("Content-Type", "application/json") + callbackReq.Header.Set("Authorization", BearerPrefix+oAuthType.AccessToken) + + client := s.httpClientGenerator.NewHTTPClient() + klog.V(2).Info("Triggering callback: ", callbackReq) + + callbackResponse, err := client.Do(callbackReq) + if err != nil { + klog.Error("Error sending async callback: ", err.Error()) + return + } else { + klog.Info("Response from async callback: ", callbackResponse) + defer callbackResponse.Body.Close() + } + } + + klog.Info("Exiting from async callback..") +} + +func (s *SubscriptionHandler) HandleRequest(w http.ResponseWriter, req *http.Request) { + var subscriptionResult *Result + switch req.Method { + case http.MethodPut: + subscriptionResult = s.CreateTenant(req) + if subscriptionResult.Tenant == nil { + w.WriteHeader(http.StatusNotAcceptable) + } else { + w.WriteHeader(http.StatusAccepted) + } + case http.MethodDelete: + subscriptionResult = s.DeleteTenant(req) + if subscriptionResult.Message != ResourceDeleted { + w.WriteHeader(http.StatusNotAcceptable) + } else { + w.WriteHeader(http.StatusAccepted) + } + default: + subscriptionResult = &Result{Tenant: nil, Message: InvalidRequestMethod} + w.WriteHeader(http.StatusMethodNotAllowed) + } + res, _ := json.Marshal(subscriptionResult) + w.Write(res) +} + +func NewSubscriptionHandler(clientset versioned.Interface, kubeClienset kubernetes.Interface) *SubscriptionHandler { + return &SubscriptionHandler{Clientset: clientset, KubeClienset: kubeClienset, httpClientGenerator: &httpClientGeneratorImpl{}} +} + +// Returns an sha1 checksum for a given source string +func sha1Sum(source ...string) string { + sum := sha1.Sum([]byte(strings.Join(source, ""))) + return fmt.Sprintf("%x", sum) +} diff --git a/cmd/server/internal/handler_test.go b/cmd/server/internal/handler_test.go new file mode 100644 index 0000000..11ec5f0 --- /dev/null +++ b/cmd/server/internal/handler_test.go @@ -0,0 +1,663 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +package handler + +import ( + "context" + "crypto/tls" + "crypto/x509" + "encoding/json" + "io" + "net" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + "time" + + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + k8sfake "k8s.io/client-go/kubernetes/fake" + + "github.com/sap/cap-operator/internal/util" + "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + "github.com/sap/cap-operator/pkg/client/clientset/versioned/fake" +) + +const RequestPath = "/provision" + +type httpTestClientGenerator struct { + client *http.Client +} + +func (facade *httpTestClientGenerator) NewHTTPClient() *http.Client { return facade.client } + +const ( + cavName = "cav-test-controller" + caName = "ca-test-controller" + appName = "some-app-name" + globalAccountId = "cap-app-global" + subDomain = "foo" + tenantId = "012012012-1234-1234-123456" +) + +func setup(ca *v1alpha1.CAPApplication, cat *v1alpha1.CAPTenant, client *http.Client) *SubscriptionHandler { + crdObjects := []runtime.Object{} + if ca != nil { + crdObjects = append(crdObjects, ca) + } + if cat != nil { + crdObjects = append(crdObjects, cat) + } + + subHandler := NewSubscriptionHandler(fake.NewSimpleClientset(crdObjects...), k8sfake.NewSimpleClientset(createSecrets()...)) + if client != nil { + subHandler.httpClientGenerator = &httpTestClientGenerator{client: client} + } + return subHandler +} + +func createSecrets() []runtime.Object { + secs := []runtime.Object{} + secs = append(secs, &corev1.Secret{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-xsuaa-sec", + Namespace: v1.NamespaceDefault, + }, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + "credentials": []byte(`{ + "uaadomain": "auth.service.local", + "xsappname": "appname!b14", + "trustedclientidsuffix": "|appname!b14", + "verificationkey": "", + "sburl": "internal.auth.service.local", + "url": "https://app-domain.auth.service.local", + "credential-type": "instance-secret" + }`), + }, + }, &corev1.Secret{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-xsuaa-sec2", + Namespace: v1.NamespaceDefault, + }, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + "credentials": []byte(`{ + "uaadomain": "auth2.service.local", + "xsappname": "appname!b21", + "trustedclientidsuffix": "|appname!b21", + "verificationkey": "", + "sburl": "internal.auth2.service.local", + "url": "https://app2-domain.auth2.service.local", + "credential-type": "instance-secret" + }`), + }, + }, &corev1.Secret{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-saas-sec", + Namespace: v1.NamespaceDefault, + }, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + "credentials": []byte(`{ + "saas_registry_url": "https://sm.service.local", + "clientid": "clientid", + "clientsecret": "clientsecret", + "uaadomain": "auth.service.local", + "sburl": "internal.auth.service.local", + "url": "https://app-domain.auth.service.local", + "credential-type": "instance-secret" + }`), + }, + }) + + return secs +} + +func createCA() *v1alpha1.CAPApplication { + return &v1alpha1.CAPApplication{ + ObjectMeta: v1.ObjectMeta{ + Name: caName, + Namespace: v1.NamespaceDefault, + Labels: map[string]string{ + LabelBTPApplicationIdentifierHash: sha1Sum(globalAccountId, appName), + }, + }, + Spec: v1alpha1.CAPApplicationSpec{ + Domains: v1alpha1.ApplicationDomains{Primary: "app.sme.sap.com", IstioIngressGatewayLabels: []v1alpha1.NameValue{{Name: "foo", Value: "bar"}}}, + GlobalAccountId: globalAccountId, + BTPAppName: appName, + Provider: v1alpha1.BTPTenantIdentification{ + SubDomain: subDomain, + TenantId: tenantId, + }, + BTP: v1alpha1.BTP{ + Services: []v1alpha1.ServiceInfo{ + { + Class: "xsuaa", + Name: "test-xsuaa", + Secret: "test-xsuaa-sec", + }, + { + Class: "xsuaa", + Name: "test-xsuaa2", + Secret: "test-xsuaa-sec2", + }, + { + Class: "saas-registry", + Name: "test-saas", + Secret: "test-saas-sec", + }, + { + Class: "service-manager", + Name: "test-sm", + Secret: "test-sm-sec", + }, + { + Class: "destination", + Name: "test-dest", + Secret: "test-dest-sec", + }, + { + Class: "html5-apps-repo", + Name: "test-html-host", + Secret: "test-html-host-sec", + }, + { + Class: "html5-apps-repo", + Name: "test-html-rt", + Secret: "test-html-rt-sec", + }, + }, + }, + }, + } +} + +func createCAT(ready bool) *v1alpha1.CAPTenant { + cat := &v1alpha1.CAPTenant{ + ObjectMeta: v1.ObjectMeta{ + Name: caName + "-provider", + Namespace: v1.NamespaceDefault, + Labels: map[string]string{ + LabelBTPApplicationIdentifierHash: sha1Sum(globalAccountId, appName), + LabelTenantId: tenantId, + }, + }, + Spec: v1alpha1.CAPTenantSpec{ + CAPApplicationInstance: caName, + BTPTenantIdentification: v1alpha1.BTPTenantIdentification{ + SubDomain: subDomain, + TenantId: tenantId, + }, + }, + } + if ready { + cat.Status = v1alpha1.CAPTenantStatus{ + State: v1alpha1.CAPTenantStateReady, + CurrentCAPApplicationVersionInstance: "cap-version", + GenericStatus: v1alpha1.GenericStatus{ + Conditions: []v1.Condition{{ + Type: string(v1alpha1.ConditionTypeReady), + Status: "True", + Reason: "TenantReady", + }}, + }, + } + } + return cat +} + +func TestMain(m *testing.M) { + m.Run() +} + +func Test_IncorrectMethod(t *testing.T) { + res := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPatch, RequestPath, strings.NewReader(`{"foo": "bar"}`)) + subHandler := setup(nil, nil, nil) + subHandler.HandleRequest(res, req) + if res.Code != http.StatusMethodNotAllowed { + t.Errorf("Expected status '%d', received '%d'", http.StatusMethodNotAllowed, res.Code) + } + + // Get the relevant response + decoder := json.NewDecoder(res.Body) + var resType Result + err := decoder.Decode(&resType) + if err != nil { + t.Error("Unexpected error in expected response: ", res.Body) + } + + if resType.Tenant != nil && resType.Message != InvalidRequestMethod { + t.Error("Response: ", res.Body, " does not match expected result: ", InvalidRequestMethod) + } + +} + +func Test_provisioning(t *testing.T) { + tests := []struct { + name string + method string + body string + createCROs bool + withSecretKey bool + existingTenant bool + expectedStatusCode int + expectedResponse Result + }{ + { + name: "Invalid Provisioning Request", + method: http.MethodPut, + body: "", + expectedStatusCode: http.StatusNotAcceptable, + expectedResponse: Result{ + Message: "EOF", //TODO + }, + }, + { + name: "Provisioning Request without CROs", + method: http.MethodPut, + body: `{"subscriptionAppName":"` + appName + `","globalAccountGUID":"` + globalAccountId + `","subscribedTenantId":"` + tenantId + `"}`, + expectedStatusCode: http.StatusNotAcceptable, + expectedResponse: Result{ + Message: "the server could not find the requested resource (get capapplications.sme.sap.com)", //TODO + }, + }, + { + name: "Provisioning Request with CROs with invalid app name", + method: http.MethodPut, + body: `{"subscriptionAppName":"test-app","globalAccountGUID":"` + globalAccountId + `","subscribedTenantId":"` + tenantId + `"}`, + createCROs: true, + expectedStatusCode: http.StatusNotAcceptable, + expectedResponse: Result{ + Message: "", //TODO + }, + }, + { + name: "Provisioning Request valid", + method: http.MethodPut, + body: `{"subscriptionAppName":"` + appName + `","globalAccountGUID":"` + globalAccountId + `","subscribedTenantId":"` + tenantId + `"}`, + createCROs: true, + expectedStatusCode: http.StatusAccepted, + expectedResponse: Result{ + Message: ResourceCreated, + }, + }, + { + name: "Provisioning Request with existing tenant", + method: http.MethodPut, + body: `{"subscriptionAppName":"` + appName + `","globalAccountGUID":"` + globalAccountId + `","subscribedTenantId":"` + tenantId + `"}`, + createCROs: true, + existingTenant: true, + expectedStatusCode: http.StatusAccepted, + expectedResponse: Result{ + Message: ResourceFound, + }, + }, + } + + for _, testData := range tests { + t.Run(testData.name, func(t *testing.T) { + var ca *v1alpha1.CAPApplication + var cat *v1alpha1.CAPTenant + if testData.createCROs { + ca = createCA() + } + if testData.existingTenant { + cat = createCAT(false) + } + client, tokenString, err := SetupValidTokenAndIssuerForSubscriptionTests("appname!b14") + if err != nil { + t.Fatal(err.Error()) + } + subHandler := setup(ca, cat, client) + + res := httptest.NewRecorder() + req := httptest.NewRequest(testData.method, RequestPath, strings.NewReader(testData.body)) + req.Header.Set("Authorization", "Bearer "+tokenString) + subHandler.HandleRequest(res, req) + if res.Code != testData.expectedStatusCode { + t.Errorf("Expected status '%d', received '%d'", testData.expectedStatusCode, res.Code) + } + + // Get the relevant response + decoder := json.NewDecoder(res.Body) + var resType Result + err = decoder.Decode(&resType) + if err != nil { + t.Error("Unexpected error in expected response: ", res.Body) + } + + if resType.Tenant != testData.expectedResponse.Tenant && resType.Message != testData.expectedResponse.Message { + t.Error("Response: ", res.Body, " does not match expected result: ", testData.expectedResponse) + } + }) + } +} + +func Test_deprovisioning(t *testing.T) { + tests := []struct { + name string + method string + createCROs bool + existingTenant bool + body string + expectedStatusCode int + expectedResponse Result + withSecretKey bool + }{ + { + name: "Invalid Deprovisioning Request", + method: http.MethodDelete, + + body: "", + expectedStatusCode: http.StatusNotAcceptable, + expectedResponse: Result{ + Message: "EOF", //TODO + }, + }, + { + name: "Deprovisioning Request w/o CROs", + method: http.MethodDelete, + + body: `{"subscriptionAppName":"` + appName + `","globalAccountGUID":"` + globalAccountId + `","subscribedTenantId":"` + tenantId + `"}`, + expectedStatusCode: http.StatusNotAcceptable, + expectedResponse: Result{ + Message: "the server could not find the requested resource (get capapplications.sme.sap.com)", //TODO + }, + }, + { + name: "Deprovisioning Request valid", + method: http.MethodDelete, + createCROs: true, + body: `{"subscriptionAppName":"` + appName + `","globalAccountGUID":"` + globalAccountId + `","subscribedTenantId":"` + tenantId + `"}`, + expectedStatusCode: http.StatusAccepted, + expectedResponse: Result{ + Message: ResourceDeleted, + }, + }, + { + name: "Deprovisioning Request valid existing tenant", + method: http.MethodDelete, + createCROs: true, + existingTenant: true, + body: `{"subscriptionAppName":"` + appName + `","globalAccountGUID":"` + globalAccountId + `","subscribedTenantId":"` + tenantId + `"}`, + expectedStatusCode: http.StatusAccepted, + expectedResponse: Result{ + Message: ResourceDeleted, + }, + }, + } + + for _, testData := range tests { + t.Run(testData.name, func(t *testing.T) { + var ca *v1alpha1.CAPApplication + var cat *v1alpha1.CAPTenant + if testData.createCROs { + ca = createCA() + } + if testData.existingTenant { + cat = createCAT(false) + } + + // set custom client for testing + client, tokenString, err := SetupValidTokenAndIssuerForSubscriptionTests("appname!b14") + if err != nil { + t.Fatal(err.Error()) + } + subHandler := setup(ca, cat, client) + + res := httptest.NewRecorder() + req := httptest.NewRequest(testData.method, RequestPath, strings.NewReader(testData.body)) + req.Header.Set("Authorization", "Bearer "+tokenString) + subHandler.HandleRequest(res, req) + if res.Code != testData.expectedStatusCode { + t.Errorf("Expected status '%d', received '%d'", testData.expectedStatusCode, res.Code) + } + + // Get the relevant response + decoder := json.NewDecoder(res.Body) + var resType Result + err = decoder.Decode(&resType) + if err != nil { + t.Error("Unexpected error in expected response: ", res.Body) + } + + if resType.Tenant != testData.expectedResponse.Tenant && resType.Message != testData.expectedResponse.Message { + t.Error("Response: ", res.Body, " does not match expected result: ", testData.expectedResponse) + } + }) + } +} + +func getX509KeyPair(t *testing.T) (string, string) { + read := func(file string) string { + value, err := os.ReadFile(file) + if err != nil { + t.Fatalf("error reading domain key pair: %s", err.Error()) + } + return string(value) + } + return read("testdata/auth.service.local.crt"), read("testdata/auth.service.local.key") +} + +func TestAsyncCallback(t *testing.T) { + certValue, keyValue := getX509KeyPair(t) + type testConfig struct { + testName string + status bool + useCredentialType string + isProvisioning bool + } + saasData := &util.SaasRegistryCredentials{ + SaasManagerUrl: "https://saas-manager.auth.service.local", + CredentialData: util.CredentialData{ + CredentialType: "x509", + ClientId: "randomapp!b14", + AuthUrl: "https://secret.auth.service.local", + UAADomain: "auth.service.local", + Certificate: certValue, + CertificateKey: keyValue, + CertificateUrl: "https://cert.auth.service.local", + }, + } + + type testContextKey string + const cKey testContextKey = "async-callback-test" + createCallbackTestServer := func(ctx context.Context, t *testing.T, params *testConfig) *http.Client { + // NOTE: reusing the wildcard domain and certificates for *.auth.service.local + + // Append CA cert to the system pool + rootCAs, _ := x509.SystemCertPool() + if rootCAs == nil { + rootCAs = x509.NewCertPool() + } + certs, err := os.ReadFile("testdata/rootCA.pem") + if err != nil { + t.Fatalf("error reading root CA certificate: %s", err.Error()) + } + rootCAs.AppendCertsFromPEM(certs) + + var calledHost string + ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/oauth/token": + t.Log(calledHost) + var expectedHostPattern string + if params.useCredentialType == "x509" { + expectedHostPattern = "cert.auth.service.local:" + } else { + expectedHostPattern = "secret.auth.service.local:" + } + if !strings.Contains(calledHost, expectedHostPattern) { + t.Error("wrong host for token fetch") + } + w.Write([]byte("{\"access_token\": \"test-server-access-token\"}")) + case "/async/callback": + if !strings.Contains(calledHost, "saas-manager.auth.service.local:") { + t.Error("wrong host for async callback") + } + if r.Header.Get("Authorization") != "Bearer test-server-access-token" { + t.Error("expected authorization header with token in async callback") + } + payload := &CallbackResponse{} + body, err := io.ReadAll(r.Body) + if err != nil { + t.Fatalf("could not read callback request body: %s", err.Error()) + } + err = json.Unmarshal(body, payload) + if err != nil { + t.Fatalf("could not parse callback request body: %s", err.Error()) + } + if (params.status && payload.Status != CallbackSucceeded) || (!params.status && payload.Status != CallbackFailed) { + t.Fatalf("status %s does not match status initiated from callback", payload.Status) + } + if params.isProvisioning { + if strings.Index(payload.Message, "Provisioning ") != 0 { + t.Fatal("incorrect message in payload") + } + } else { + if strings.Index(payload.Message, "Deprovisioning ") != 0 { + t.Fatal("incorrect message in payload") + } + } + w.WriteHeader(200) + } + })) + cert, err := tls.X509KeyPair([]byte(certValue), []byte(keyValue)) + if err != nil { + t.Fatalf("error reading domain key pair: %s", err.Error()) + } + ts.TLS = &tls.Config{Certificates: []tls.Certificate{cert}, RootCAs: rootCAs} + ts.StartTLS() + + // adjust client to have custom domain resolution + client := ts.Client() + client.Transport = &http.Transport{ + DialContext: func(c context.Context, network, addr string) (net.Conn, error) { + if strings.Contains(addr, "auth.service.local:") { + if v := c.Value(cKey); v != nil { + calledHost = addr + } else { + calledHost = "" + } + addr = ts.Listener.Addr().String() + } + return net.Dial(network, addr) + }, + TLSClientConfig: ts.TLS, + } + + go func() { + <-ctx.Done() + ts.Close() + }() + return client + } + + tests := []testConfig{ + {testName: "1", status: true, useCredentialType: "x509", isProvisioning: true}, + {testName: "2", status: true, useCredentialType: "x509", isProvisioning: false}, + {testName: "3", status: false, useCredentialType: "instance-secret", isProvisioning: true}, + } + + ctx := context.WithValue(context.Background(), cKey, true) + for _, p := range tests { + saasData.CredentialType = p.useCredentialType + t.Run(p.testName, func(t *testing.T) { + client := createCallbackTestServer(context.TODO(), t, &p) + subHandler := setup(nil, nil, client) + subHandler.handleAsyncCallback( + ctx, + saasData, + p.status, + "/async/callback", + "https://app.cluster.local", + p.isProvisioning, + ) + }) + } +} + +func TestCheckTenantStatusContextCancellationAsyncTimeout(t *testing.T) { + execTestsWithBLI(t, "Check Tenant Status Context Cancellation AsyncTimeout", []string{"ERP4SMEPREPWORKAPPPLAT-2240"}, func(t *testing.T) { + // test context cancellation (like deadline) + subHandler := setup(nil, nil, nil) + notify := make(chan bool) + go func() { + r := subHandler.checkCAPTenantStatus(context.Background(), "default", "test-cat", true, "4000") + notify <- r + }() + + timeout := time.After(6 * time.Second) // this is greater than the sleep duration of the tenant check routine + + select { + case r := <-notify: + if r != false { + t.Error("expected tenant check to return false") + } + case <-timeout: + t.Fatal("failed to cancel tenant check routine") + } + }) +} + +func TestCheckTenantStatusTenantReady(t *testing.T) { + // test context cancellation (like deadline) + cat := createCAT(true) + subHandler := setup(nil, cat, nil) + r := subHandler.checkCAPTenantStatus(context.TODO(), cat.Namespace, cat.Name, true, "") + + if r != true { + t.Error("expected tenant check to return false") + } +} + +func TestCheckTenantStatusWithCallbacktimeout(t *testing.T) { + execTestsWithBLI(t, "Check Tenant Status With Callback timeout", []string{"ERP4SMEPREPWORKAPPPLAT-2240"}, func(t *testing.T) { + // test context cancellation (like deadline) + cat := createCAT(false) + subHandler := setup(nil, cat, nil) + r := subHandler.checkCAPTenantStatus(context.TODO(), cat.Namespace, cat.Name, true, "4000") + + if r != false { + t.Error("expected tenant check to return false, due to timeout (async callback timeout exceeded)") + } + }) +} + +func TestMultiXSUAA(t *testing.T) { + execTestsWithBLI(t, "Check Multiple xsuaa services used in a CA", []string{"ERP4SMEPREPWORKAPPPLAT-3773"}, func(t *testing.T) { + // CA without "sme.sap.com/primary-xsuaa" annotation + ca := createCA() + + subHandler := setup(ca, nil, nil) + uaaCreds := subHandler.getXSUAADetails(ca) + + if uaaCreds.AuthUrl != "https://app-domain.auth.service.local" { + t.Error("incorrect uaa returned") + } + + // CA with "sme.sap.com/primary-xsuaa" annotation + ca2 := createCA() + ca2.Annotations = map[string]string{ + util.AnnotationPrimaryXSUAA: "test-xsuaa2", + } + + uaaCreds = subHandler.getXSUAADetails(ca2) + + if uaaCreds.AuthUrl != "https://app2-domain.auth2.service.local" { + t.Error("incorrect uaa via annotations returned") + } + }) +} + +func execTestsWithBLI(t *testing.T, name string, backlogItems []string, test func(t *testing.T)) { + t.Run(name+", BLIs: "+strings.Join(backlogItems, ", "), test) +} diff --git a/cmd/server/internal/jwt.go b/cmd/server/internal/jwt.go new file mode 100644 index 0000000..6d5eb61 --- /dev/null +++ b/cmd/server/internal/jwt.go @@ -0,0 +1,235 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +package handler + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "path" + "strings" + "time" + + "github.com/MicahParks/keyfunc/v2" + "github.com/golang-jwt/jwt/v5" +) + +type XSUAAConfig struct { + UAADomain string + // one of xsappname OR clientid must be part of the audience + XSAppName string + ClientID string + // all requested scopes must be fulfilled + RequiredScopes []string +} + +type XSUAAJWTClaims struct { + Scope []string `json:"scope"` + ClientID string `json:"client_id"` + AuthorizedParty string `json:"azp"` + jwt.RegisteredClaims `json:",inline"` +} + +type OpenIDConfig struct { + JWKSURI string `json:"jwks_uri"` + SigningAlgorithmsSupported []string `json:"token_endpoint_auth_signing_alg_values_supported"` + ClaimsSupported []string `json:"claims_supported"` +} + +var ( + errorJKUTokenHeader = errors.New("jku token header validation failed") + errorInvalidClaimsType = errors.New("invalid token claims type") + errorInvalidAudience = errors.New("invalid token audience") + errorInvalidScope = errors.New("invalid token scope") +) + +func getOpenIDConfig(uaaURL string, client *http.Client) (*OpenIDConfig, error) { + url, err := url.Parse(uaaURL) + if err != nil { + return nil, err + } + url.Path = path.Join(".well-known", "openid-configuration") + resp, err := client.Get(url.String()) + if err != nil { + return nil, err + } + if resp.StatusCode != 200 { + return nil, fmt.Errorf("fetching openid configuration returned status %s", resp.Status) + } + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + config := OpenIDConfig{} + return &config, json.Unmarshal(body, &config) +} + +// token validation for XSUAA token implemented by following the guidelines provided -> CPSecurity/Knowledge-Base/03_ApplicationSecurity/TokenValidation/ +func VerifyXSUAAJWTToken(ctx context.Context, tokenString string, config *XSUAAConfig, client *http.Client) error { + _, err := jwt.ParseWithClaims(tokenString, &XSUAAJWTClaims{}, func(t *jwt.Token) (interface{}, error) { + // verify claims excluding expiration (as this is now done internally in jwt v5) + err := verifyClaims(t, config) + if err != nil { + return nil, err + } + + // verify token algorithm + if tokenAlg, ok := t.Header["alg"].(string); !ok || tokenAlg == "" { + return nil, fmt.Errorf("expected token alg to be string") + } + + // verify JKU header as per XSUAA requirements + jkuHost, ok := verifyJKUHeader(t, config.UAADomain) + if !ok { + return nil, errorJKUTokenHeader + } + + // get well-known openid configuration + oidConfig, err := getOpenIDConfig("https://"+jkuHost, client) + if err != nil { + return nil, err + } + + jwks, err := keyfunc.Get(oidConfig.JWKSURI, keyfunc.Options{Ctx: ctx, Client: client}) + if err != nil { + return nil, err + } + return jwks.Keyfunc(t) + }, jwt.WithLeeway(-15*time.Second)) + + if err != nil { + return err + } + + return nil // all good! +} + +func verifyJKUHeader(t *jwt.Token, uaaDomain string) (string, bool) { + if jku, ok := t.Header["jku"].(string); ok { + if jku == "" { + return "", false + } + u, err := url.Parse(jku) + if err != nil { + return "", false + } + if len(u.Query()) > 0 || // there should not be any query parameters + !u.IsAbs() || // should contain a schema (HTTPS) + u.Fragment != "" || // should not contain fragments + !(strings.HasSuffix(u.Path, "token_keys") || strings.HasSuffix(u.Path, "token_keys/")) || // path should end with token_keys + !strings.HasSuffix(u.Hostname(), uaaDomain) { // jku hostname must be a subdomain of uaa domain + return "", false + } + + return u.Host, true + } + return "", false +} + +func verifyClaims(t *jwt.Token, config *XSUAAConfig) error { + claims, ok := t.Claims.(*XSUAAJWTClaims) + if !ok { + return errorInvalidClaimsType + } + + // NOTE: in XSUAA scenarios, do not rely on token iss attribute + + // verify audience + ok = verifyAudience(claims, config) + if !ok { + return errorInvalidAudience + } + + ok = verifyScopes(claims, config) + if !ok { + return errorInvalidScope + } + + return nil +} + +func verifyAudience(claims *XSUAAJWTClaims, config *XSUAAConfig) bool { + tokenAud := convertToMap(extractAudience(claims)) + knownAud := appendWithTrim([]string{config.ClientID}, config.XSAppName) // unless XSAPPNAME is provided, don't add it to valid audience list + + // should match at least one of the expected audience + for _, expected := range knownAud { + if _, ok := tokenAud[expected]; ok { + return true // valid audience + } + + // additional check for broker clients + if strings.Contains(expected, "!b") { // is a broker + for a := range tokenAud { + if strings.HasSuffix(a, "|"+expected) { + return true // valid + } + } + } + } + + return false +} + +func extractAudience(claims *XSUAAJWTClaims) []string { + r := adjustForNamespace(claims.Audience, false) + + // extract audience from client id and scope (XSUAA specific) + // REFERENCE -> CPSecurity/Knowledge-Base/03_ApplicationSecurity/TokenValidation/#xsuaa-specifics_1 + if len(r) == 0 { + + r = append(r, adjustForNamespace(claims.Scope, true)...) + } + // use client id from token as audience + r = appendWithTrim(r, claims.ClientID) + + return r +} + +func appendWithTrim(s []string, v string) []string { + if val := strings.TrimSpace(v); len(val) > 0 { + return append(s, val) + } + return s +} + +func adjustForNamespace(s []string, ignoreIfNotNamespaced bool) []string { + r := []string{} + for _, v := range s { + if i := strings.Index(v, "."); i > -1 { + r = appendWithTrim(r, v[:i]) + } else if !ignoreIfNotNamespaced { // when processing scope, add to list only when namespaced + r = appendWithTrim(r, v) + } + } + return r +} + +func verifyScopes(claims *XSUAAJWTClaims, config *XSUAAConfig) bool { + scope := claims.Scope + tokenScope := convertToMap(scope) + for _, expected := range config.RequiredScopes { + if _, ok := tokenScope[expected]; !ok { + return false // all expected scopes should match + } + } + return true +} + +// Create a dummy lookup map +func convertToMap(s []string) map[string]struct{} { + if s == nil { + return nil + } + m := map[string]struct{}{} + for _, item := range s { + m[item] = struct{}{} + } + return m +} diff --git a/cmd/server/internal/jwt_test.go b/cmd/server/internal/jwt_test.go new file mode 100644 index 0000000..286745d --- /dev/null +++ b/cmd/server/internal/jwt_test.go @@ -0,0 +1,360 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +package handler + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "encoding/json" + "errors" + "fmt" + "net" + "net/http" + "net/http/httptest" + "os" + "reflect" + "runtime" + "strings" + "testing" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/lestrrat-go/jwx/v2/jwk" +) + +type JWKeys struct { + Keys []jwk.RSAPublicKey `json:"keys"` +} + +type rsaKeyParams struct { + jwks *JWKeys + key *rsa.PrivateKey + keyID string +} + +const jwksKeyID = "test-key-rsa" +const jwtTestUAADomain = "auth.service.local" +const testSubdomain = "test-subdomain" + +func createRSAKey() (*rsaKeyParams, error) { + privateKey, _ := rsa.GenerateKey(rand.Reader, 2048) + key, _ := jwk.FromRaw(privateKey.PublicKey) + publicKey := key.(jwk.RSAPublicKey) + publicKey.Set(jwk.KeyIDKey, jwksKeyID) + publicKey.Set(jwk.KeyUsageKey, "sig") + return &rsaKeyParams{ + jwks: &JWKeys{Keys: []jwk.RSAPublicKey{publicKey}}, + key: privateKey, + keyID: jwksKeyID, + }, nil +} + +func SetupValidTokenAndIssuerForSubscriptionTests(xsappname string) (*http.Client, string, error) { + return setupTokenAndIssuer(&XSUAAConfig{ + UAADomain: jwtTestUAADomain, + XSAppName: xsappname, + ClientID: "some-client-id", + RequiredScopes: []string{xsappname + ".Callback", xsappname + ".mtcallback"}, + }, &jwtTestParameters{}) +} + +type jwtTestParameters struct { + invalidJKUHeader bool + useDifferentKeyToSign bool + invalidAudience bool + clientIsBroker bool + invalidScope bool + expiredJWT bool + notBeforeInFuture bool +} + +func setupTokenAndIssuer(config *XSUAAConfig, params *jwtTestParameters) (*http.Client, string, error) { + rsaKey, err := createRSAKey() + if err != nil { + return nil, "", fmt.Errorf("error generating rsa key: %s", err.Error()) + } + claims := XSUAAJWTClaims{ + Scope: config.RequiredScopes, + ClientID: "srv-broker!b14", + AuthorizedParty: "srv-broker!b14", + RegisteredClaims: jwt.RegisteredClaims{ + Audience: jwt.ClaimStrings{config.XSAppName, "srv-broker!b14"}, + ID: "jwt-token-01", + Issuer: "https://" + strings.Join([]string{testSubdomain, config.UAADomain}, ".") + "/token", + Subject: "jwt-token-01", + IssuedAt: jwt.NewNumericDate(time.Now().Add(-30 * time.Minute)), + ExpiresAt: jwt.NewNumericDate(time.Now().Add(15 * time.Minute)), + }, + } + + // audience configuration + if params.invalidAudience { + claims.Audience = jwt.ClaimStrings{"invalid-a", "invalid-b"} + } else if params.clientIsBroker { + claims.ClientID = "sb-d447781d-c010-4c19-af30-ed49097f22de!b446|" + config.XSAppName + claims.Audience = jwt.ClaimStrings{} + } + + if params.invalidScope { + claims.Scope = []string{"scope-a", "scope-b"} + } + + if params.expiredJWT { + claims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(-5 * time.Minute)) + } + if params.notBeforeInFuture { + claims.NotBefore = jwt.NewNumericDate(time.Now().Add(5 * time.Minute)) + } + token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) + + if params.invalidJKUHeader { + token.Header["jku"] = "https://" + strings.Join([]string{testSubdomain, "foo.bar.local"}, ".") + "/token_keys" + } else { + token.Header["jku"] = "https://" + strings.Join([]string{testSubdomain, config.UAADomain}, ".") + "/token_keys" + } + + token.Header["kid"] = rsaKey.keyID + + // sign token + signKey := rsaKey + if params.useDifferentKeyToSign { + signKey, _ = createRSAKey() + } + tokenString, err := token.SignedString(signKey.key) + if err != nil { + return nil, "", fmt.Errorf("error signing token: %s", err.Error()) + } + + client, err := createJWTTestTLSServer(context.TODO(), rsaKey.jwks) + if err != nil { + return nil, "", err + } + + return client, tokenString, nil +} + +func createJWTTestTLSServer(ctx context.Context, jwks *JWKeys) (*http.Client, error) { + domain := strings.Join([]string{testSubdomain, jwtTestUAADomain}, ".") + + // Append CA cert to the system pool + rootCAs, _ := x509.SystemCertPool() + if rootCAs == nil { + rootCAs = x509.NewCertPool() + } + certs, err := os.ReadFile("testdata/rootCA.pem") + if err != nil { + return nil, err + } + if ok := rootCAs.AppendCertsFromPEM(certs); !ok { + return nil, errors.New("could not append CA cert") + } + + // create test TLS server + ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var body []byte + switch r.URL.Path { + case "/.well-known/openid-configuration": + body, _ = json.Marshal(OpenIDConfig{JWKSURI: "https://" + domain + "/token_keys", SigningAlgorithmsSupported: []string{"RS256"}}) + case "/token_keys": + body, _ = json.MarshalIndent(jwks, "", " ") + } + w.Write(body) + })) + cert, err := tls.LoadX509KeyPair("testdata/auth.service.local.crt", "testdata/auth.service.local.key") + if err != nil { + return nil, err + } + ts.TLS = &tls.Config{Certificates: []tls.Certificate{cert}, RootCAs: rootCAs} + ts.StartTLS() + + // adjust client to have custom domain resolution + client := ts.Client() + client.Transport = &http.Transport{ + DialContext: func(_ context.Context, network, addr string) (net.Conn, error) { + if strings.Contains(addr, jwtTestUAADomain+":") { + addr = ts.Listener.Addr().String() + } + return net.Dial(network, addr) + }, + TLSClientConfig: ts.TLS, + } + + go func() { + <-ctx.Done() + ts.Close() + }() + return client, nil +} + +var testXSUAAConfig *XSUAAConfig = &XSUAAConfig{ + UAADomain: jwtTestUAADomain, + XSAppName: "myxsappname", + ClientID: "some-client-id", + RequiredScopes: []string{"myxsappname.Callback", "myxsappname.mtcallback"}, +} + +func testVerifyValidToken(t *testing.T) { + client, tokenString, err := setupTokenAndIssuer(testXSUAAConfig, &jwtTestParameters{}) + if err != nil { + t.Fatal(err.Error()) + } + + err = VerifyXSUAAJWTToken(context.TODO(), tokenString, testXSUAAConfig, client) + if err != nil { + t.Fatal("token validation failed") + } +} + +func testValidTokenWithBrokerClientId(t *testing.T) { + brokerXSUAAConfig := &XSUAAConfig{ + UAADomain: testXSUAAConfig.UAADomain, + ClientID: testXSUAAConfig.ClientID, + RequiredScopes: testXSUAAConfig.RequiredScopes, + XSAppName: "xsapp!b4711", + } + client, tokenString, err := setupTokenAndIssuer(brokerXSUAAConfig, &jwtTestParameters{clientIsBroker: true}) + if err != nil { + t.Fatal(err.Error()) + } + + err = VerifyXSUAAJWTToken(context.TODO(), tokenString, brokerXSUAAConfig, client) + if err != nil { + t.Fatal("token validation failed") + } +} + +func testInvalidJKUHeader(t *testing.T) { + client, tokenString, err := setupTokenAndIssuer(testXSUAAConfig, &jwtTestParameters{invalidJKUHeader: true}) + if err != nil { + t.Fatal(err.Error()) + } + + err = VerifyXSUAAJWTToken(context.TODO(), tokenString, testXSUAAConfig, client) + if err == nil { + t.Fatal("expected token validation to fail") + } + + if !errors.Is(err, errorJKUTokenHeader) { + t.Error("error message was not as expected") + } +} + +func testExpiredToken(t *testing.T) { + client, tokenString, err := setupTokenAndIssuer(testXSUAAConfig, &jwtTestParameters{expiredJWT: true}) + if err != nil { + t.Fatal(err.Error()) + } + + err = VerifyXSUAAJWTToken(context.TODO(), tokenString, testXSUAAConfig, client) + if err == nil { + t.Fatal("expected token validation to fail") + } + if !errors.Is(err, jwt.ErrTokenExpired) { + t.Error("error message was not as expected") + } +} + +func testTokenWithNotBefore(t *testing.T) { + client, tokenString, err := setupTokenAndIssuer(testXSUAAConfig, &jwtTestParameters{notBeforeInFuture: true}) + if err != nil { + t.Fatal(err.Error()) + } + + err = VerifyXSUAAJWTToken(context.TODO(), tokenString, testXSUAAConfig, client) + if err == nil { + t.Fatal("expected token validation to fail") + } + if !errors.Is(err, jwt.ErrTokenNotValidYet) { + t.Error("error message was not as expected") + } +} + +func testInvalidSignature(t *testing.T) { + client, tokenString, err := setupTokenAndIssuer(testXSUAAConfig, &jwtTestParameters{useDifferentKeyToSign: true}) + if err != nil { + t.Fatal(err.Error()) + } + + err = VerifyXSUAAJWTToken(context.TODO(), tokenString, testXSUAAConfig, client) + if err == nil { + t.Fatal("expected token validation to fail") + } +} + +func testInvalidAudience(t *testing.T) { + client, tokenString, err := setupTokenAndIssuer(testXSUAAConfig, &jwtTestParameters{invalidAudience: true}) + if err != nil { + t.Fatal(err.Error()) + } + + err = VerifyXSUAAJWTToken(context.TODO(), tokenString, testXSUAAConfig, client) + if err == nil { + t.Fatal("expected token validation to fail") + } + if !errors.Is(err, errorInvalidAudience) { + t.Error("error message was not as expected") + } +} + +func testInvalidScope(t *testing.T) { + client, tokenString, err := setupTokenAndIssuer(testXSUAAConfig, &jwtTestParameters{invalidScope: true}) + if err != nil { + t.Fatal(err.Error()) + } + + err = VerifyXSUAAJWTToken(context.TODO(), tokenString, testXSUAAConfig, client) + if err == nil { + t.Fatal("expected token validation to fail") + } + if !errors.Is(err, errorInvalidScope) { + t.Error("error message was not as expected") + } +} + +func testInvalidClaimsType(t *testing.T) { + type foo struct { + Foo string `json:"foo"` + jwt.RegisteredClaims + } + + _, tokenString, _ := setupTokenAndIssuer(testXSUAAConfig, &jwtTestParameters{}) + + token, _ := jwt.ParseWithClaims(tokenString, &foo{}, func(t *jwt.Token) (interface{}, error) { + return []byte("Test"), nil + }) + + err := verifyClaims(token, testXSUAAConfig) + if err == nil { + t.Fatal("expected token validation to fail") + } + if !errors.Is(err, errorInvalidClaimsType) { + t.Error("error message was not as expected") + } +} + +func TestJWT(t *testing.T) { + catalog := &[]struct { + test func(t *testing.T) + backlogItems []string + }{ + {test: testVerifyValidToken, backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2019", "ERP4SMEPREPWORKAPPPLAT-3188"}}, + {test: testValidTokenWithBrokerClientId, backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2019", "ERP4SMEPREPWORKAPPPLAT-3188"}}, + {test: testInvalidJKUHeader, backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2019", "ERP4SMEPREPWORKAPPPLAT-3188"}}, + {test: testExpiredToken, backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2019", "ERP4SMEPREPWORKAPPPLAT-3188"}}, + {test: testTokenWithNotBefore, backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2019", "ERP4SMEPREPWORKAPPPLAT-3188"}}, + {test: testInvalidSignature, backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2019", "ERP4SMEPREPWORKAPPPLAT-3188"}}, + {test: testInvalidAudience, backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2019", "ERP4SMEPREPWORKAPPPLAT-3188"}}, + {test: testInvalidScope, backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2019", "ERP4SMEPREPWORKAPPPLAT-3188"}}, + {test: testInvalidSignature, backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2019", "ERP4SMEPREPWORKAPPPLAT-3188"}}, + {test: testInvalidClaimsType}, + } + for _, tc := range *catalog { + nameParts := []string{runtime.FuncForPC(reflect.ValueOf(tc.test).Pointer()).Name()} + t.Run(strings.Join(append(nameParts, tc.backlogItems...), " "), tc.test) + } +} diff --git a/cmd/server/internal/testdata/auth.service.local.crt b/cmd/server/internal/testdata/auth.service.local.crt new file mode 100644 index 0000000..a424362 --- /dev/null +++ b/cmd/server/internal/testdata/auth.service.local.crt @@ -0,0 +1,30 @@ +-----BEGIN CERTIFICATE----- +MIIFJTCCBA2gAwIBAgIJANGmQ9qKKpLJMA0GCSqGSIb3DQEBCwUAMIG3MQswCQYD +VQQGEwJERTEUMBIGA1UECAwLUmFuZG9tU3RhdGUxEzARBgNVBAcMClJhbmRvbUNp +dHkxGzAZBgNVBAoMElJhbmRvbU9yZ2FuaXphdGlvbjEfMB0GA1UECwwWUmFuZG9t +T3JnYW5pemF0aW9uVW5pdDEgMB4GCSqGSIb3DQEJARYRaGVsbG9AZXhhbXBsZS5j +b20xHTAbBgNVBAMMFCouYXV0aC5zZXJ2aWNlLmxvY2FsMB4XDTIyMDUwNzIxMjAx +MloXDTIzMDkxOTIxMjAxMlowgbcxCzAJBgNVBAYTAkRFMRQwEgYDVQQIDAtSYW5k +b21TdGF0ZTETMBEGA1UEBwwKUmFuZG9tQ2l0eTEbMBkGA1UECgwSUmFuZG9tT3Jn +YW5pemF0aW9uMR8wHQYDVQQLDBZSYW5kb21Pcmdhbml6YXRpb25Vbml0MSAwHgYJ +KoZIhvcNAQkBFhFoZWxsb0BleGFtcGxlLmNvbTEdMBsGA1UEAwwUKi5hdXRoLnNl +cnZpY2UubG9jYWwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDwQOin +JSqDaezbcNDCMSrhd2vG9P7wFssZywzYvy6aU9zBSzQZfDQp/spt5lbQvP/iB4/I +EFkGiSgjqSb6WK/2ids0t2gfI62xLEjCX9QWPtQeeenp8uxpZg8dp4JBYTaS4zlo +1JYt/gyaGd/VhERsd0VNSPyQ4aI+nV7ARzP1lw3sdpOuGkfOtpzajniRw+YMK0tK +k0LSwISVsN3CKYhNNk/Esg0kz+znKTdvQ7IlHUIdVgoQtDsC73VYvt6uADYbKZJq +YmsL9CkESSZ2ZFkzPpYmAdunnOQTz9ZDxarniC7YPmo3sXM7O3tfyw762/nnWPPe +wSLCiqnNqvv1j56LAgMBAAGjggEwMIIBLDCB1gYDVR0jBIHOMIHLoYG9pIG6MIG3 +MQswCQYDVQQGEwJERTEUMBIGA1UECAwLUmFuZG9tU3RhdGUxEzARBgNVBAcMClJh +bmRvbUNpdHkxGzAZBgNVBAoMElJhbmRvbU9yZ2FuaXphdGlvbjEfMB0GA1UECwwW +UmFuZG9tT3JnYW5pemF0aW9uVW5pdDEgMB4GCSqGSIb3DQEJARYRaGVsbG9AZXhh +bXBsZS5jb20xHTAbBgNVBAMMFCouYXV0aC5zZXJ2aWNlLmxvY2FsggkA0Tp8M8gl +9j8wCQYDVR0TBAIwADALBgNVHQ8EBAMCBPAwOQYDVR0RBDIwMIISYXV0aC5zZXJ2 +aWNlLmxvY2FsghQqLmF1dGguc2VydmljZS5sb2NhbIcEfwAAATANBgkqhkiG9w0B +AQsFAAOCAQEAHKtlbkGSPaGryU3ou8QdKqBfSSPXFhl6aoE1drS2VVeSWYpvReAM +weACk70v3jJsG+ia51HRvBHBVPdNUr/H8gawJX7fbECUf3DOkXbww/IUFwxd9jFM +tvv4c3mMh2oHDX68jABApamPfQRhH9qmuEGdsQEwlE8uh0CnUmEeaCw2gtCmjwZ5 +QqCidRtyHQDZtioQzQYxVHeSHEf4YVKtWYkvJSc8kKmAcJAn51cjbD9R7HR3wtsi +ezQo9wHaTfj4rdHDHqXwEpcPdeCA/CrWrDZfXtF4dkVP8BKvH1NVk6f5Cz8LcAe8 +S7s91u98+w/7Tx+cPVPJE+iTmeahEcAP7Q== +-----END CERTIFICATE----- diff --git a/cmd/server/internal/testdata/auth.service.local.key b/cmd/server/internal/testdata/auth.service.local.key new file mode 100644 index 0000000..3ba01ab --- /dev/null +++ b/cmd/server/internal/testdata/auth.service.local.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDwQOinJSqDaezb +cNDCMSrhd2vG9P7wFssZywzYvy6aU9zBSzQZfDQp/spt5lbQvP/iB4/IEFkGiSgj +qSb6WK/2ids0t2gfI62xLEjCX9QWPtQeeenp8uxpZg8dp4JBYTaS4zlo1JYt/gya +Gd/VhERsd0VNSPyQ4aI+nV7ARzP1lw3sdpOuGkfOtpzajniRw+YMK0tKk0LSwISV +sN3CKYhNNk/Esg0kz+znKTdvQ7IlHUIdVgoQtDsC73VYvt6uADYbKZJqYmsL9CkE +SSZ2ZFkzPpYmAdunnOQTz9ZDxarniC7YPmo3sXM7O3tfyw762/nnWPPewSLCiqnN +qvv1j56LAgMBAAECggEBAI8mjbkxyu/8SFXEFY7vjtZCuqQUTGavnhpjQudOmqz3 +tPwzG/rnZ4lyOBldenLreieqS8BwBSuAw7rjyca22zmxkDwL3+1V6+M6OKwgPxV2 +IBt8lqR/yt9OIUmRCmp8SvEglI9iw4zp54ZWTmlBYyehtVhEWcDVwD9Aszkr88ir +LeWsHiApvmoHJafCqY4q3DGPWEzg1ub/9fvi+BHnnH6crOhOH4HqwoJLrLmsGKDo +4qBHLWfiRCR34zDJ2I8S75m1QNjFuvMYHarPFLyq5VvDp/Sml7TQvPPIbLB0YD5U +HfvPoPB6LRG9eFkfTLsmOH9d/rU6054VJBUgwLlRoxECgYEA+BMqt5J6V9oZ2j1W +v0muKyhvywV9krlEmG5+H84A7a9KLSXwIkx88cbAsKehkQqHavQ/xbSiEcTJ5WsW +GTBj/cKfnqL/62QYxD9Kpm/LgWP7NZNteJZyx8NBGt7I9zLTYRJhlMh48ivTNRxp +CxNIEpkf+8eL4hMmkwxGgH0YYnMCgYEA9+3G2WKojfQQhOrgWgal534xD88fxbIU +TPHmOpGHF07YLP+Kogq3ow4C52Qul+O48z6oNZAKm8uOelyXQHxcgyg8eK3fQjPA +j+Ghvn+OH74oqfamvbZbNYFlclYyWBOUlGV8xbMeeva6PeuWynqUirGMFyafuI9J +2epNXFlylYkCgYBfYY81Eb60dIkoHhlyZvPuaBfDqZLEjTNQoHsh42T7/j+46DNS +HLKVi2OfCHTYfYHfn5W9gFwoFM/Dw861VKO9d81Dg0x+xve2zNb481b9ouF9kfev +O7laETrBCBOg6AvZ8OVP/VxzUGJes1O4DGvTqshfWDPycoaMV1XsJSzw/QKBgBo8 +QBmK1hlHZWQbUqhUIcQwV1K78TnDUWCfDGTQN4Jg5oFEfVAOYEZR2j7QHBoYj961 +l6krV+QKk0YhfCPnxQZgAJ4okAJ6ZXsUPkBhURHM1pK9tgFHRbmQusJxmpw1Xjih +0KU/Ag+zAhxBTNCaThOrHA7rGGW4S/FSWONX18c5AoGAYkjPXs/I6B5Jp3H7maNo +RT3nnNGOd0ZhxU+Z1vveilW9w8ZnAbxYolFucyFDX9NXNKyDVuOpAJUgjB0MNVg2 +whT9txoeDAgfhHDy/3sXZ5c5aMSS2og2nX3OmkO5t9YeHRpBscqmvqrK+f6ffudk +K7gbgRz/ykbZKIEQBRCjaeA= +-----END PRIVATE KEY----- diff --git a/cmd/server/internal/testdata/rootCA.pem b/cmd/server/internal/testdata/rootCA.pem new file mode 100644 index 0000000..6be1214 --- /dev/null +++ b/cmd/server/internal/testdata/rootCA.pem @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIID7DCCAtQCCQDROnwzyCX2PzANBgkqhkiG9w0BAQsFADCBtzELMAkGA1UEBhMC +REUxFDASBgNVBAgMC1JhbmRvbVN0YXRlMRMwEQYDVQQHDApSYW5kb21DaXR5MRsw +GQYDVQQKDBJSYW5kb21Pcmdhbml6YXRpb24xHzAdBgNVBAsMFlJhbmRvbU9yZ2Fu +aXphdGlvblVuaXQxIDAeBgkqhkiG9w0BCQEWEWhlbGxvQGV4YW1wbGUuY29tMR0w +GwYDVQQDDBQqLmF1dGguc2VydmljZS5sb2NhbDAeFw0yMjA1MDcyMTE4NDdaFw0y +NTAyMjQyMTE4NDdaMIG3MQswCQYDVQQGEwJERTEUMBIGA1UECAwLUmFuZG9tU3Rh +dGUxEzARBgNVBAcMClJhbmRvbUNpdHkxGzAZBgNVBAoMElJhbmRvbU9yZ2FuaXph +dGlvbjEfMB0GA1UECwwWUmFuZG9tT3JnYW5pemF0aW9uVW5pdDEgMB4GCSqGSIb3 +DQEJARYRaGVsbG9AZXhhbXBsZS5jb20xHTAbBgNVBAMMFCouYXV0aC5zZXJ2aWNl +LmxvY2FsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4gDdW6Z0md2h +Nym5YLV6bIeupl3dFurcnhq0s7CicjAodeS9U/zwgZdzatEk81y0WELsdvUuzsyx +g6onObsM3xL/tWnPlrNlvwPlvXD38zb6TYmyuP7eW+da5wW2tlirx1N7adpWz623 +DQpkyvpJHbTmlGE9EGNALAOtvr0h7uR7eHXin4U4ezT2e+vsYK2kQ0zcFm2Qh9Ar +0YedvlyWvYLLWT8vg2z8dTGbM16A3GiaRBxOo49YoDWhV5S1cviublt0BZ5wmzTr +0kew4RdHA4WA7cA/dL1Hx8J1FVCjZvTDVt+FUOYb31slSEBPtSw4OL40wtaGUD/K +8PrtrCV9oQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQA1mFZq7DLojN04uo5bZ1JC +PO/y4/Cn3j4g/lvZpe5IZe6O5qh17J76p0lFoc3JwczACSX33xB3m657pQWhfrx4 +izJ31O4EIsywxgteRp9BYSQxh5ME0YZiHCWRhba4rbJjX8RCA/9RF0n/3XW3cAtZ +xVF56mjMVlSlVCWdmCJN5vxyHQneU5Fv67wS/qiIweJD2LFm/F7yqC7aElDaxJ9w +7OPi/Pmyi/aFlmUQVEFkSWeehDfNktK6MHTAc8tD8Og4cQEb0Gy9UA625kNYshOi +glBBpcwEIx+NPXiRvs1eeydr4G4bB6zcQpYuIulEnImRrMjwmBvwrC3kWHNdnlPq +-----END CERTIFICATE----- diff --git a/cmd/server/server.go b/cmd/server/server.go new file mode 100644 index 0000000..574573c --- /dev/null +++ b/cmd/server/server.go @@ -0,0 +1,71 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +package main + +import ( + "net/http" + "os" + "strconv" + + "k8s.io/client-go/kubernetes" + "k8s.io/klog/v2" + + handler "github.com/sap/cap-operator/cmd/server/internal" + "github.com/sap/cap-operator/internal/util" + "github.com/sap/cap-operator/pkg/client/clientset/versioned" +) + +func main() { + subHandler := getSubscriptionHandler() + http.HandleFunc("/provision/", subHandler.HandleRequest) + + // Default port + port := "4000" + + // Get Port from env + portEnv := os.Getenv("PORT") + if portEnv != "" { + port = portEnv + } + + // Default TLS enabled = false + tlsEnabled := false + + // Get TLS details from env + var tlsCertFile, tlsKeyFile string + tlsEnv := os.Getenv("TLS_ENABLED") + if tlsEnv != "" { + tlsEnvBool, err := strconv.ParseBool(tlsEnv) + if err != nil { + klog.Error("Error parsing TLS_ENABLED: ", err.Error()) + } + tlsEnabled = tlsEnvBool + tlsCertFile = os.Getenv("TLS_CERT") + tlsKeyFile = os.Getenv("TLS_KEY") + } + + klog.Info("Server running and listening (provider) with TLS: ", tlsEnabled, ", at port: ", port) + if tlsEnabled { + klog.Fatal(http.ListenAndServeTLS(":"+port, tlsCertFile, tlsKeyFile, nil)) + } else { + http.ListenAndServe(":"+port, nil) + } +} + +func getSubscriptionHandler() *handler.SubscriptionHandler { + config := util.GetConfig() + + customResourceClientSet, err := versioned.NewForConfig(config) + if err != nil { + klog.Fatal("could not create client for custom resources: ", err.Error()) + } + + kubernetesClientset, err := kubernetes.NewForConfig(config) + if err != nil { + klog.Fatal("could not create client for k8s resources: ", err.Error()) + } + + return handler.NewSubscriptionHandler(customResourceClientSet, kubernetesClientset) +} diff --git a/cmd/web-hooks/internal/handler/handler.go b/cmd/web-hooks/internal/handler/handler.go new file mode 100644 index 0000000..f79badd --- /dev/null +++ b/cmd/web-hooks/internal/handler/handler.go @@ -0,0 +1,629 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +package handler + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "os" + "strconv" + + "github.com/google/go-cmp/cmp" + "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + "github.com/sap/cap-operator/pkg/client/clientset/versioned" + "golang.org/x/exp/slices" + admissionv1 "k8s.io/api/admission/v1" + "k8s.io/api/admission/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/klog/v2" +) + +const ( + LabelTenantType = "sme.sap.com/tenant-type" + ProviderTenantType = "provider" + SideCarEnv = "WEBHOOK_SIDE_CAR" + AdmissionError = "admission error:" + InvalidResource = "invalid resource" + InvalidationMessage = "invalidated from webhook" + ValidationMessage = "validated from webhook" + RequestPath = "/request" + DeploymentWorkloadCountErr = "%s %s there should always be one workload deployment definition of type %s. Currently, there are %d workloads of type %s" + JobWorkloadCountErr = "%s %s there should always be one workload job definition of type %s. Currently, there are %d workloads of type %s" + TenantOpJobWorkloadCountErr = "%s %s there should not be more than one workload job definition of type %s. Currently, there are %d workloads of type %s" +) + +type validateResource struct { + errorOccured bool + allowed bool + message string +} + +var ( + universalDeserializer = serializer.NewCodecFactory(runtime.NewScheme()).UniversalDeserializer() +) + +type WebhookHandler struct { + CrdClient versioned.Interface +} + +// Metadata struct for parsing +type Metadata struct { + Name string `json:"name"` + Namespace string `json:"namespace"` + Labels map[string]string `json:"labels"` +} + +type ResponseCat struct { + Metadata `json:"metadata"` + Spec *v1alpha1.CAPTenantSpec `json:"spec"` + Status *v1alpha1.CAPTenantStatus `json:"status"` + Kind string `json:"kind"` +} + +type ResponseCav struct { + Metadata `json:"metadata"` + Spec *v1alpha1.CAPApplicationVersionSpec `json:"spec"` + Kind string `json:"kind"` +} + +type ResponseCa struct { + Metadata `json:"metadata"` + Spec *v1alpha1.CAPApplicationSpec `json:"spec"` + Kind string `json:"kind"` +} + +type responseInterface interface { + isEmpty() bool +} + +func (m Metadata) isEmpty() bool { + return m.Name == "" +} + +func checkWorkloadPort(workload *v1alpha1.WorkloadDetails) validateResource { + if workload.DeploymentDefinition == nil { + return validAdmissionReviewObj() + } + + if len(workload.DeploymentDefinition.Ports) == 0 { + return validAdmissionReviewObj() + } + + // Checks- + // at least one port configuration should have routerDestinationName defined + // port name and port number should be unique + uniquePortNameCountMap := make(map[string]int) + uniquePortNumCountMap := make(map[string]int) + routerDestinationNameFound := false + for _, port := range workload.DeploymentDefinition.Ports { + if port.RouterDestinationName != "" { + routerDestinationNameFound = true + } + uniquePortNameCountMap[port.Name] += 1 + uniquePortNumCountMap[strconv.Itoa(int(port.Port))] += 1 + } + + if !routerDestinationNameFound && workload.DeploymentDefinition.Type == v1alpha1.DeploymentCAP { // workloads of type Additional need not have a router destination + return validateResource{ + allowed: false, + message: fmt.Sprintf("%s %s routerDestinationName not defined in port configuration of workload - %s", InvalidationMessage, v1alpha1.CAPApplicationVersionKind, workload.Name), + } + } + + if routerDestinationNameFound && workload.DeploymentDefinition.Type == v1alpha1.DeploymentRouter { + return validateResource{ + allowed: false, + message: fmt.Sprintf("%s %s routerDestinationName should not be defined for workload of type Router - %s", InvalidationMessage, v1alpha1.CAPApplicationVersionKind, workload.Name), + } + } + + for portName, cnt := range uniquePortNameCountMap { + if cnt > 1 { + return validateResource{ + allowed: false, + message: fmt.Sprintf("%s %s duplicate port name: %s in workload - %s", InvalidationMessage, v1alpha1.CAPApplicationVersionKind, portName, workload.Name), + } + } + } + + for portNum, cnt := range uniquePortNumCountMap { + if cnt > 1 { + return validateResource{ + allowed: false, + message: fmt.Sprintf("%s %s duplicate port number: %s in workload - %s", InvalidationMessage, v1alpha1.CAPApplicationVersionKind, portNum, workload.Name), + } + } + } + + return validAdmissionReviewObj() +} + +func checkWorkloadType(workload *v1alpha1.WorkloadDetails) validateResource { + if workload.DeploymentDefinition != nil && workload.DeploymentDefinition.Type != v1alpha1.DeploymentCAP && workload.DeploymentDefinition.Type != v1alpha1.DeploymentRouter && workload.DeploymentDefinition.Type != v1alpha1.DeploymentAdditional { + return validateResource{ + allowed: false, + message: fmt.Sprintf("%s %s invalid deployment definition type. Only supported - CAP, Router and Additional", InvalidationMessage, v1alpha1.CAPApplicationVersionKind), + } + } + + if workload.JobDefinition != nil && workload.JobDefinition.Type != v1alpha1.JobContent && workload.JobDefinition.Type != v1alpha1.JobTenantOperation && workload.JobDefinition.Type != v1alpha1.JobCustomTenantOperation { + return validateResource{ + allowed: false, + message: fmt.Sprintf("%s %s invalid job definition type. Only supported - Content, TenantOperation and CustomTenantOperation", InvalidationMessage, v1alpha1.CAPApplicationVersionKind), + } + } + + return validAdmissionReviewObj() +} + +func getWorkloadTypeCount(workloads []v1alpha1.WorkloadDetails) map[string]int { + workloadTypeCount := make(map[string]int) + for _, workload := range workloads { + if workload.DeploymentDefinition != nil && workload.DeploymentDefinition.Type == v1alpha1.DeploymentCAP { + workloadTypeCount[string(v1alpha1.DeploymentCAP)] += 1 + } + + if workload.DeploymentDefinition != nil && workload.DeploymentDefinition.Type == v1alpha1.DeploymentRouter { + workloadTypeCount[string(v1alpha1.DeploymentRouter)] += 1 + } + + if workload.JobDefinition != nil && workload.JobDefinition.Type == v1alpha1.JobContent { + workloadTypeCount[string(v1alpha1.JobContent)] += 1 + } + + if workload.JobDefinition != nil && workload.JobDefinition.Type == v1alpha1.JobTenantOperation { + workloadTypeCount[string(v1alpha1.JobTenantOperation)] += 1 + } + } + + return workloadTypeCount +} + +func checkWorkloadTypeCount(cavObjNew *ResponseCav) validateResource { + + workloadTypeCount := getWorkloadTypeCount(cavObjNew.Spec.Workloads) + + if workloadTypeCount[string(v1alpha1.DeploymentCAP)] != 1 { + return validateResource{ + allowed: false, + message: fmt.Sprintf(DeploymentWorkloadCountErr, InvalidationMessage, cavObjNew.Kind, v1alpha1.DeploymentCAP, workloadTypeCount[string(v1alpha1.DeploymentCAP)], v1alpha1.DeploymentCAP), + } + } + + if workloadTypeCount[string(v1alpha1.DeploymentRouter)] != 1 { + return validateResource{ + allowed: false, + message: fmt.Sprintf(DeploymentWorkloadCountErr, InvalidationMessage, cavObjNew.Kind, v1alpha1.DeploymentRouter, workloadTypeCount[string(v1alpha1.DeploymentRouter)], v1alpha1.DeploymentRouter), + } + } + + if workloadTypeCount[string(v1alpha1.JobContent)] != 1 { + return validateResource{ + allowed: false, + message: fmt.Sprintf(JobWorkloadCountErr, InvalidationMessage, cavObjNew.Kind, v1alpha1.JobContent, workloadTypeCount[string(v1alpha1.JobContent)], v1alpha1.JobContent), + } + } + + return validAdmissionReviewObj() +} + +func validateWorkloads(cavObjNew *ResponseCav) validateResource { + // Check: Workload name should be unique + // Only one workload deployment of type CAP, router and content is allowed + uniqueWorkloadNameCountMap := make(map[string]int) + for _, workload := range cavObjNew.Spec.Workloads { + + if workloadTypeValidate := checkWorkloadType(&workload); !workloadTypeValidate.allowed { + return workloadTypeValidate + } + + if workloadPortValidate := checkWorkloadPort(&workload); !workloadPortValidate.allowed { + return workloadPortValidate + } + + // get count of workload names + uniqueWorkloadNameCountMap[workload.Name] += 1 + } + + for workloadName, cnt := range uniqueWorkloadNameCountMap { + if cnt > 1 { + return validateResource{ + allowed: false, + message: fmt.Sprintf("%s %s duplicate workload name: %s", InvalidationMessage, cavObjNew.Kind, workloadName), + } + } + } + + if workloadTypeCntValidate := checkWorkloadTypeCount(cavObjNew); !workloadTypeCntValidate.allowed { + return workloadTypeCntValidate + } + + return validAdmissionReviewObj() +} + +func getTenantOperationsFromSpec(cavObjNew *ResponseCav) map[string]int { + specTenantOperationsCntMap := make(map[string]int) + var tenantOperationsList []v1alpha1.TenantOperationWorkloadReference + if cavObjNew.Spec.TenantOperations.Provisioning != nil { + tenantOperationsList = append(tenantOperationsList, cavObjNew.Spec.TenantOperations.Provisioning...) + } + + if cavObjNew.Spec.TenantOperations.Deprovisioning != nil { + tenantOperationsList = append(tenantOperationsList, cavObjNew.Spec.TenantOperations.Deprovisioning...) + } + + if cavObjNew.Spec.TenantOperations.Upgrade != nil { + tenantOperationsList = append(tenantOperationsList, cavObjNew.Spec.TenantOperations.Upgrade...) + } + + for _, tenantOperation := range tenantOperationsList { + specTenantOperationsCntMap[tenantOperation.WorkloadName] += 1 + } + return specTenantOperationsCntMap +} + +func checkForTenantOpJob(tenantOperations []v1alpha1.TenantOperationWorkloadReference, tenantOperationWorkloadCntMap map[string]int) bool { + return slices.ContainsFunc(tenantOperations, func(tenantOp v1alpha1.TenantOperationWorkloadReference) bool { + return tenantOperationWorkloadCntMap[tenantOp.WorkloadName] > 0 + }) +} + +func validateWorkloadsinTenantOperations(allTenantOperationsWorkloadCntMap map[string]int, tenantOperationWorkloadCntMap map[string]int, cavObjNew *ResponseCav) validateResource { + + specTenantOperationsCntMap := getTenantOperationsFromSpec(cavObjNew) + + // If spec.tenantOperations is specified, the entries (for provisioning, upgrade and deprovisioning) must include all spec.workloads.jobDefinitions of type TenantOperation and CustomTenantOperation + for workloadTenantOperation := range allTenantOperationsWorkloadCntMap { + if specTenantOperationsCntMap[workloadTenantOperation] == 0 { + return validateResource{ + allowed: false, + message: fmt.Sprintf("%s %s workload tenant operation %s is not specified in spec.tenantOperations", InvalidationMessage, v1alpha1.CAPApplicationVersionKind, workloadTenantOperation), + } + } + } + + // All the entries specified in spec.tenantOperations should be a valid workload of type TenantOperation or customTenantOperation + for specTenantOperation := range specTenantOperationsCntMap { + if allTenantOperationsWorkloadCntMap[specTenantOperation] == 0 { + return validateResource{ + allowed: false, + message: fmt.Sprintf("%s %s %s specified in spec.tenantOperations is not a valid workload of type TenantOperation or CustomTenantOperation", InvalidationMessage, v1alpha1.CAPApplicationVersionKind, specTenantOperation), + } + } + } + + // If spec.tenantOperations are defined for provisioning, upgrade or deprovisioning, one of the operation must be a tenant operation + if cavObjNew.Spec.TenantOperations.Provisioning != nil && !checkForTenantOpJob(cavObjNew.Spec.TenantOperations.Provisioning, tenantOperationWorkloadCntMap) { + return validateResource{ + allowed: false, + message: fmt.Sprintf("%s %s - No tenant operation specified in spec.tenantOperation.provisioning", InvalidationMessage, v1alpha1.CAPApplicationVersionKind), + } + } + + if cavObjNew.Spec.TenantOperations.Upgrade != nil && !checkForTenantOpJob(cavObjNew.Spec.TenantOperations.Upgrade, tenantOperationWorkloadCntMap) { + return validateResource{ + allowed: false, + message: fmt.Sprintf("%s %s - No tenant operation specified in spec.tenantOperation.upgrade", InvalidationMessage, v1alpha1.CAPApplicationVersionKind), + } + } + + if cavObjNew.Spec.TenantOperations.Deprovisioning != nil && !checkForTenantOpJob(cavObjNew.Spec.TenantOperations.Deprovisioning, tenantOperationWorkloadCntMap) { + return validateResource{ + allowed: false, + message: fmt.Sprintf("%s %s - No tenant operation specified in spec.tenantOperation.deprovisioning", InvalidationMessage, v1alpha1.CAPApplicationVersionKind), + } + } + + return validAdmissionReviewObj() +} + +func validateTenantOperations(cavObjNew *ResponseCav) validateResource { + // Check: If a jobDefinition of type CustomTenantOperation is part of the workloads, spec.tenantOperations must be specified. It is possible to omit spec.tenantOperations when there are no jobs of type CustomTenantOperation and only one job of type TenantOperation + // If spec.tenantOperations is specified, the entries (for provisioning, upgrade and deprovisioning) must include all spec.workloads.jobDefinitions of type TenantOperation + // All the entries specified in spec.tenantOperations should be a valid workload of type TenantOperation or CustomTenantOperation + tenantOperationWorkloadCntMap := make(map[string]int) + allTenantOperationsWorkloadCntMap := make(map[string]int) + customTenantOpWorkloadCntMap := make(map[string]int) + for _, workload := range cavObjNew.Spec.Workloads { + if workload.JobDefinition != nil && workload.JobDefinition.Type == v1alpha1.JobTenantOperation { + tenantOperationWorkloadCntMap[workload.Name] += 1 + allTenantOperationsWorkloadCntMap[workload.Name] += 1 + } + + if workload.JobDefinition != nil && workload.JobDefinition.Type == v1alpha1.JobCustomTenantOperation { + customTenantOpWorkloadCntMap[workload.Name] += 1 + allTenantOperationsWorkloadCntMap[workload.Name] += 1 + } + } + + // It is possible to omit spec.tenantOperations when there are no jobs of type CustomTenantOperation and only one job of type TenantOperation + if len(customTenantOpWorkloadCntMap) == 0 && cavObjNew.Spec.TenantOperations == nil { + return validAdmissionReviewObj() + } + + // If a jobDefinition of type CustomTenantOperation is part of the workloads, spec.tenantOperations must be specified + if len(customTenantOpWorkloadCntMap) > 0 && cavObjNew.Spec.TenantOperations == nil { + return validateResource{ + allowed: false, + message: fmt.Sprintf("%s %s - If a jobDefinition of type CustomTenantOperation is part of the workloads, then spec.tenantOperations must be specified", InvalidationMessage, cavObjNew.Kind), + } + } + + if cavObjNew.Spec.TenantOperations == nil { + return validAdmissionReviewObj() + } + + if workloadsinTenantOperationsValidate := validateWorkloadsinTenantOperations(allTenantOperationsWorkloadCntMap, tenantOperationWorkloadCntMap, cavObjNew); !workloadsinTenantOperationsValidate.allowed { + return workloadsinTenantOperationsValidate + } + + return validAdmissionReviewObj() +} + +func (wh *WebhookHandler) checkCAPAppExists(cavObjNew *ResponseCav) validateResource { + if app, err := wh.CrdClient.SmeV1alpha1().CAPApplications(cavObjNew.Metadata.Namespace).Get(context.TODO(), cavObjNew.Spec.CAPApplicationInstance, metav1.GetOptions{}); app == nil || err != nil { + return validateResource{ + allowed: false, + message: fmt.Sprintf("%s %s no valid %s found for: %s.%s", InvalidationMessage, cavObjNew.Kind, v1alpha1.CAPApplicationKind, cavObjNew.Metadata.Namespace, cavObjNew.Metadata.Name), + } + } + + return validAdmissionReviewObj() +} + +func (wh *WebhookHandler) validateCAPApplicationVersion(w http.ResponseWriter, admissionReview *admissionv1.AdmissionReview) validateResource { + cavObjOld := ResponseCav{} + cavObjNew := ResponseCav{} + + // Note: Object is nil for "DELETE" operation + if admissionReview.Request.Operation == admissionv1.Create || admissionReview.Request.Operation == admissionv1.Update { + if validatedResource := unmarshalRawObj(w, admissionReview.Request.Object.Raw, &cavObjNew, v1alpha1.CAPApplicationVersionKind); !validatedResource.allowed { + return validatedResource + } + } + + // Note: OldObject is nil for "CONNECT" and "CREATE" operations + if admissionReview.Request.Operation == admissionv1.Delete || admissionReview.Request.Operation == admissionv1.Update { + if validatedResource := unmarshalRawObj(w, admissionReview.Request.OldObject.Raw, &cavObjOld, v1alpha1.CAPApplicationVersionKind); !validatedResource.allowed { + return validatedResource + } + } + + // check: on create + if admissionReview.Request.Operation == admissionv1.Create { + // Check: CAPApplication exists + if capAppExistsValidate := wh.checkCAPAppExists(&cavObjNew); !capAppExistsValidate.allowed { + return capAppExistsValidate + } + + if workloadValidate := validateWorkloads(&cavObjNew); !workloadValidate.allowed { + return workloadValidate + } + + return validateTenantOperations(&cavObjNew) + + } + + // check: update on .Spec + if admissionReview.Request.Operation == admissionv1.Update && !cmp.Equal(cavObjOld.Spec, cavObjNew.Spec) { + return validateResource{ + allowed: false, + message: fmt.Sprintf("%s %s spec cannot be modified for: %s.%s", InvalidationMessage, cavObjNew.Kind, cavObjNew.Metadata.Namespace, cavObjNew.Metadata.Name), + } + } + return validAdmissionReviewObj() +} + +func (wh *WebhookHandler) checkCaIsConsistent(catObjOld ResponseCat) validateResource { + + ca, err := wh.CrdClient.SmeV1alpha1().CAPApplications(catObjOld.Metadata.Namespace).Get(context.TODO(), catObjOld.Spec.CAPApplicationInstance, metav1.GetOptions{}) + + if ca != nil && err == nil && ca.Status.State == v1alpha1.CAPApplicationStateConsistent && catObjOld.Metadata.Labels[LabelTenantType] == ProviderTenantType && catObjOld.Status.State == v1alpha1.CAPTenantStateReady { + return validateResource{ + allowed: false, + message: fmt.Sprintf("%s provider %s %s cannot be deleted when a consistent %s %s exists. Delete the %s instead to delete all tenants", InvalidationMessage, catObjOld.Kind, catObjOld.Name, v1alpha1.CAPApplicationKind, ca.Name, v1alpha1.CAPApplicationKind), + } + } + return validAdmissionReviewObj() +} + +func (wh *WebhookHandler) validateCAPTenant(w http.ResponseWriter, admissionReview *admissionv1.AdmissionReview) validateResource { + catObjOld := ResponseCat{} + catObjNew := ResponseCat{} + + // Note: Object is nil for "DELETE" operation + if admissionReview.Request.Operation == admissionv1.Create || admissionReview.Request.Operation == admissionv1.Update { + if validatedResource := unmarshalRawObj(w, admissionReview.Request.Object.Raw, &catObjNew, v1alpha1.CAPTenantKind); !validatedResource.allowed { + return validatedResource + } + } + // Note: OldObject is nil for "CONNECT" and "CREATE" operations + if admissionReview.Request.Operation == admissionv1.Delete || admissionReview.Request.Operation == admissionv1.Update { + if validatedResource := unmarshalRawObj(w, admissionReview.Request.OldObject.Raw, &catObjOld, v1alpha1.CAPTenantKind); !validatedResource.allowed { + return validatedResource + } + } + + // check: CAPApplication exists on create + if admissionReview.Request.Operation == admissionv1.Create { + if app, err := wh.CrdClient.SmeV1alpha1().CAPApplications(catObjNew.Metadata.Namespace).Get(context.TODO(), catObjNew.Spec.CAPApplicationInstance, metav1.GetOptions{}); app == nil || err != nil { + return validateResource{ + allowed: false, + message: fmt.Sprintf("%s %s no valid %s found for: %s.%s", InvalidationMessage, catObjNew.Kind, v1alpha1.CAPApplicationKind, catObjNew.Metadata.Namespace, catObjNew.Metadata.Name), + } + } + } + // check: update on .Spec.CapApplicationInstance + if admissionReview.Request.Operation == admissionv1.Update && catObjOld.Spec.CAPApplicationInstance != catObjNew.Spec.CAPApplicationInstance { + return validateResource{ + allowed: false, + message: fmt.Sprintf("%s %s capApplicationInstance value cannot be modified for: %s.%s", InvalidationMessage, catObjNew.Kind, catObjNew.Metadata.Namespace, catObjNew.Metadata.Name), + } + } + + // check: dont allow provider tenant deletion when CA is consistent + if admissionReview.Request.Operation == admissionv1.Delete { + return wh.checkCaIsConsistent(catObjOld) + } + + return validAdmissionReviewObj() +} + +func (wh *WebhookHandler) validateCAPApplication(w http.ResponseWriter, admissionReview *admissionv1.AdmissionReview) validateResource { + caObjOld := ResponseCa{} + caObjNew := ResponseCa{} + + // Note: OldObject is nil for "CONNECT" and "CREATE" operations + if admissionReview.Request.Operation == admissionv1.Delete || admissionReview.Request.Operation == admissionv1.Update { + if validatedResource := unmarshalRawObj(w, admissionReview.Request.OldObject.Raw, &caObjOld, v1alpha1.CAPApplicationKind); !validatedResource.allowed { + return validatedResource + } + } + if admissionReview.Request.Operation == admissionv1.Update || admissionReview.Request.Operation == admissionv1.Create { + // Note: Object is nil for "DELETE" operation + + if validatedResource := unmarshalRawObj(w, admissionReview.Request.Object.Raw, &caObjNew, v1alpha1.CAPApplicationKind); !validatedResource.allowed { + return validatedResource + } + // check: update on .Spec.Provider + if admissionReview.Request.Operation == admissionv1.Update && !cmp.Equal(caObjNew.Spec.Provider, caObjOld.Spec.Provider) { + return validateResource{ + allowed: false, + message: fmt.Sprintf("%s %s provider details cannot be changed for: %s.%s", InvalidationMessage, caObjNew.Kind, caObjNew.Metadata.Namespace, caObjNew.Metadata.Name), + } + } + } + return validAdmissionReviewObj() +} + +func unmarshalRawObj(w http.ResponseWriter, rawBytes []byte, response responseInterface, resourceKind string) validateResource { + if err := json.Unmarshal(rawBytes, response); err != nil || response.isEmpty() { + return invalidAdmissionReviewObj(w, resourceKind, err) + } + return validAdmissionReviewObj() +} + +func (wh *WebhookHandler) Validate(w http.ResponseWriter, r *http.Request) { + // read incoming request to bytes + body, err := ioutil.ReadAll(r.Body) + if err != nil { + httpError(w, http.StatusInternalServerError, fmt.Errorf("%s %w", AdmissionError, err)) + return + } + + // sidecar + if !enableSidecar(w, body) { + return + } + + // create admission review from bytes + admissionReview := getAdmissionRequestFromBytes(w, body) + if admissionReview == nil { + return + } + + klog.Infof("incoming admission review for: %s", admissionReview.Request.Kind.Kind) + + validation := validAdmissionReviewObj() + + switch admissionReview.Request.Kind.Kind { + case v1alpha1.CAPApplicationVersionKind: + if validation = wh.validateCAPApplicationVersion(w, admissionReview); validation.errorOccured { + return + } + case v1alpha1.CAPTenantKind: + if validation = wh.validateCAPTenant(w, admissionReview); validation.errorOccured { + return + } + case v1alpha1.CAPApplicationKind: + if validation = wh.validateCAPApplication(w, admissionReview); validation.errorOccured { + return + } + } + + // prepare response + if responseBytes := prepareResponse(w, admissionReview, validation); responseBytes == nil { + return + } else { + w.Write(responseBytes) + } +} + +func enableSidecar(w http.ResponseWriter, body []byte) bool { + // write request to volume mount - if side car is enabled + sidecarEnv := os.Getenv(SideCarEnv) + + if sidecarEnv != "" { + if sidecarEnabled, err := strconv.ParseBool(sidecarEnv); err != nil { + httpError(w, http.StatusInternalServerError, fmt.Errorf("sidecar env read error: %w", err)) + return false + } else if sidecarEnabled { + if err = ioutil.WriteFile(os.TempDir()+RequestPath, body, 0644); err != nil { + httpError(w, http.StatusInternalServerError, fmt.Errorf("request object write error: %w", err)) + return false + } + } + } + return true +} + +func getAdmissionRequestFromBytes(w http.ResponseWriter, body []byte) *admissionv1.AdmissionReview { + admissionReview := admissionv1.AdmissionReview{} + if _, _, err := universalDeserializer.Decode(body, nil, &admissionReview); err != nil { + httpError(w, http.StatusBadRequest, fmt.Errorf("%s %w", AdmissionError, err)) + return nil + } else if admissionReview.Request == nil { + httpError(w, http.StatusBadRequest, fmt.Errorf("%s empty request", AdmissionError)) + return nil + } + return &admissionReview +} + +func prepareResponse(w http.ResponseWriter, admissionReview *admissionv1.AdmissionReview, validation validateResource) []byte { + // prepare response object + finalizedAdmissionReview := v1beta1.AdmissionReview{} + finalizedAdmissionReview.Kind = admissionReview.Kind + finalizedAdmissionReview.APIVersion = admissionReview.APIVersion + finalizedAdmissionReview.Response = &v1beta1.AdmissionResponse{ + UID: admissionReview.Request.UID, + Allowed: validation.allowed, + } + finalizedAdmissionReview.APIVersion = admissionReview.APIVersion + + if !validation.allowed { + finalizedAdmissionReview.Response.Result = &metav1.Status{ + Message: validation.message, + } + klog.Info(admissionReview.Request.Kind.Kind + " " + string(admissionReview.Request.Operation) + " " + InvalidationMessage) + } else { + klog.Info(admissionReview.Request.Kind.Kind + " " + string(admissionReview.Request.Operation) + " " + ValidationMessage) + } + + if bytes, err := json.Marshal(&finalizedAdmissionReview); err != nil { + httpError(w, http.StatusInternalServerError, fmt.Errorf("%s %w", AdmissionError, err)) + return nil + } else { + return bytes + } +} + +func httpError(w http.ResponseWriter, code int, err error) { + klog.Error(err) + http.Error(w, err.Error(), code) +} + +func invalidAdmissionReviewObj(w http.ResponseWriter, kind string, sourceErr error) validateResource { + httpError(w, http.StatusInternalServerError, fmt.Errorf("%s %s %s %w", InvalidResource, kind, AdmissionError, sourceErr)) + return validateResource{errorOccured: true} +} + +func validAdmissionReviewObj() validateResource { + return validateResource{allowed: true} +} diff --git a/cmd/web-hooks/internal/handler/handler_test.go b/cmd/web-hooks/internal/handler/handler_test.go new file mode 100644 index 0000000..d10c90e --- /dev/null +++ b/cmd/web-hooks/internal/handler/handler_test.go @@ -0,0 +1,1164 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +package handler + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + + "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + fakeCrdClient "github.com/sap/cap-operator/pkg/client/clientset/versioned/fake" + admissionv1 "k8s.io/api/admission/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +const ( + cavName = "someCavName" + catName = "someCatName" + caName = "someCaName" + uid = "someUID" + apiVersion = "apiVersion" + subDomain = "someSubdomain" + tenantId = "someTenantId" +) + +type updateType int + +const ( + noUpdate updateType = iota + providerUpdate + appInstanceUpdate + registrySecretsUpdate + consumedBTPServicesUpdate + versionUpdate + imageUpdate + emptyUpdate +) + +func createCaCRO() *v1alpha1.CAPApplication { + return &v1alpha1.CAPApplication{ + ObjectMeta: metav1.ObjectMeta{ + Name: caName, + Namespace: metav1.NamespaceDefault, + }, + Spec: v1alpha1.CAPApplicationSpec{ + Domains: v1alpha1.ApplicationDomains{Primary: "primaryDomain", IstioIngressGatewayLabels: []v1alpha1.NameValue{{Name: "foo", Value: "bar"}}}, + GlobalAccountId: "globalAccountId", + BTPAppName: "btpApplicationName", + Provider: v1alpha1.BTPTenantIdentification{ + SubDomain: subDomain, + TenantId: tenantId, + }, + BTP: v1alpha1.BTP{ + Services: []v1alpha1.ServiceInfo{ + { + Class: "xsuaa", + Name: "test-xsuaa", + Secret: "test-xsuaa-sec", + }, + { + Class: "saas-registry", + Name: "test-saas", + Secret: "test-saas-sec", + }, + { + Class: "service-manager", + Name: "test-sm", + Secret: "test-sm-sec", + }, + { + Class: "destination", + Name: "test-dest", + Secret: "test-dest-sec", + }, + { + Class: "html5-apps-repo", + Name: "test-html-host", + Secret: "test-html-host-sec", + }, + { + Class: "html5-apps-repo", + Name: "test-html-rt", + Secret: "test-html-rt-sec", + }, + }, + }, + }, + Status: v1alpha1.CAPApplicationStatus{ + State: v1alpha1.CAPApplicationStateConsistent, + }, + } +} + +func getHttpRequest(operation admissionv1.Operation, crdType string, crdName string, change updateType, t *testing.T) (*http.Request, *httptest.ResponseRecorder) { + admissionReview, err := createAdmissionRequest(operation, crdType, crdName, change) + if err != nil { + t.Fatal("admission review error") + } + bytesRequest, err := json.Marshal(admissionReview) + if err != nil { + t.Fatal("marshal error") + } + req := httptest.NewRequest(http.MethodGet, "/validate", bytes.NewBuffer(bytesRequest)) + w := httptest.NewRecorder() + return req, w +} + +func createAdmissionRequest(operation admissionv1.Operation, crdType string, crdName string, change updateType) (*admissionv1.AdmissionReview, error) { + admissionReview := &admissionv1.AdmissionReview{ + TypeMeta: metav1.TypeMeta{ + Kind: crdType, + APIVersion: apiVersion, + }, + Request: &admissionv1.AdmissionRequest{ + Name: crdName, + Kind: metav1.GroupVersionKind{ + Kind: crdType, + }, + Operation: operation, + UID: uid, + }, + } + + var rawBytes []byte + var rawBytesOld []byte + var err error + + switch crdType { + + case v1alpha1.CAPApplicationKind: + crd := &ResponseCa{} + if change != emptyUpdate { + crd = &ResponseCa{ + Metadata: Metadata{ + Name: caName, + Namespace: metav1.NamespaceDefault, + }, + Spec: &v1alpha1.CAPApplicationSpec{ + Provider: v1alpha1.BTPTenantIdentification{ + SubDomain: subDomain, + TenantId: tenantId, + }, + BTP: v1alpha1.BTP{}, + }, + Kind: crdType, + } + } + rawBytes, err = json.Marshal(crd) + rawBytesOld = rawBytes + if operation == admissionv1.Update && err == nil { + crdOld := crd + if change == providerUpdate { + crdOld.Spec.Provider.SubDomain = crdOld.Spec.Provider.SubDomain + "modified" + crdOld.Spec.Provider.TenantId = crdOld.Spec.Provider.TenantId + "modified" + rawBytesOld, err = json.Marshal(crdOld) + } + } + case v1alpha1.CAPApplicationVersionKind: + crd := &ResponseCav{} + if change != emptyUpdate { + crd = &ResponseCav{ + Metadata: Metadata{ + Name: crdName, + Namespace: metav1.NamespaceDefault, + }, + Spec: &v1alpha1.CAPApplicationVersionSpec{ + CAPApplicationInstance: caName, + Workloads: []v1alpha1.WorkloadDetails{ + { + Name: "cap-backend", + ConsumedBTPServices: []string{}, + DeploymentDefinition: &v1alpha1.DeploymentDetails{ + Type: v1alpha1.DeploymentCAP, + ContainerDetails: v1alpha1.ContainerDetails{ + Image: "foo", + }, + }, + }, + { + Name: "cap-router", + ConsumedBTPServices: []string{}, + DeploymentDefinition: &v1alpha1.DeploymentDetails{ + Type: v1alpha1.DeploymentRouter, + ContainerDetails: v1alpha1.ContainerDetails{ + Image: "foo", + }, + }, + }, + { + Name: "content", + ConsumedBTPServices: []string{}, + JobDefinition: &v1alpha1.JobDetails{ + Type: v1alpha1.JobContent, + ContainerDetails: v1alpha1.ContainerDetails{ + Image: "foo", + }, + }, + }, + }, + }, + Kind: crdType, + } + } + rawBytes, err = json.Marshal(crd) + rawBytesOld = rawBytes + if operation == admissionv1.Update && err == nil && change != noUpdate { + crdOld := crd + + switch change { + case appInstanceUpdate: + crdOld.Spec.CAPApplicationInstance = crdOld.Spec.CAPApplicationInstance + "modified" + case registrySecretsUpdate: + crdOld.Spec.RegistrySecrets = append(crdOld.Spec.RegistrySecrets, "newSecret") + case consumedBTPServicesUpdate: + crdOld.Spec.Workloads[0].ConsumedBTPServices = append(crdOld.Spec.Workloads[0].ConsumedBTPServices, "newService") + case versionUpdate: + crdOld.Spec.Version = crdOld.Spec.Version + "modified" + case imageUpdate: + crdOld.Spec.Workloads[0].DeploymentDefinition.Image = crdOld.Spec.Workloads[0].DeploymentDefinition.Image + "modified" + } + rawBytesOld, err = json.Marshal(crdOld) + } + case v1alpha1.CAPTenantKind: + crd := &ResponseCat{} + if change != emptyUpdate { + crd = &ResponseCat{ + Metadata: Metadata{ + Name: crdName, + Namespace: metav1.NamespaceDefault, + Labels: map[string]string{ + LabelTenantType: ProviderTenantType, + }, + }, + Spec: &v1alpha1.CAPTenantSpec{ + CAPApplicationInstance: caName, + }, + Status: &v1alpha1.CAPTenantStatus{ + State: v1alpha1.CAPTenantStateReady, + }, + Kind: crdType, + } + } + rawBytes, err = json.Marshal(crd) + rawBytesOld = rawBytes + if (operation == admissionv1.Update || operation == admissionv1.Delete) && err == nil { + crdOld := crd + if change == appInstanceUpdate { + crdOld.Spec.CAPApplicationInstance = crdOld.Spec.CAPApplicationInstance + "modified" + rawBytesOld, err = json.Marshal(crdOld) + } + } + } + + if err != nil { + return nil, err + } + + if operation != admissionv1.Delete { + admissionReview.Request.Object.Raw = rawBytes + } + + if operation != admissionv1.Create && operation != admissionv1.Connect { + admissionReview.Request.OldObject.Raw = rawBytesOld + } + + return admissionReview, nil +} + +func TestInvalidRequest(t *testing.T) { + wh := &WebhookHandler{ + CrdClient: fakeCrdClient.NewSimpleClientset(), + } + + recorder := httptest.NewRecorder() + admissionReview := admissionv1.AdmissionReview{} + bytesRequest, err := json.Marshal(admissionReview) + if err != nil { + t.Fatal("marshal error") + } + request := httptest.NewRequest(http.MethodGet, "/validate", bytes.NewBuffer(bytesRequest)) + + wh.Validate(recorder, request) + if recorder.Code != http.StatusBadRequest { + t.Fatal("Error was not recorded correctly") + } +} + +func TestSideCar(t *testing.T) { + wh := &WebhookHandler{ + CrdClient: fakeCrdClient.NewSimpleClientset(), + } + + tests := []struct { + sideCarEnv string + }{ + { + sideCarEnv: "true", + }, + { + sideCarEnv: "false", + }, + { + sideCarEnv: "invalid", + }, + } + for _, test := range tests { + t.Run("Testing SideCarEnv value "+test.sideCarEnv, func(t *testing.T) { + os.Setenv(SideCarEnv, test.sideCarEnv) + + recorder := httptest.NewRecorder() + + admissionReview := admissionv1.AdmissionReview{} + bytesRequest, err := json.Marshal(admissionReview) + if err != nil { + t.Fatal("marshal error") + } + request := httptest.NewRequest(http.MethodGet, "/validate", bytes.NewBuffer(bytesRequest)) + + wh.Validate(recorder, request) + file, err := os.ReadFile(os.TempDir() + RequestPath) + if err != nil { + t.Error("Side car file read error") + } + json.Unmarshal(file, &admissionReview) + + if admissionReview.Request != nil || admissionReview.Response != nil { + t.Fatal("Invalid admission review with http error") + } + + if test.sideCarEnv == "invalid" { + if recorder.Code != http.StatusInternalServerError { + t.Fatal("Error was not recorded correctly") + } + } else if recorder.Code != http.StatusBadRequest { + t.Fatal("Error was not recorded correctly") + } + + os.Unsetenv(SideCarEnv) + }) + } +} + +func TestEmptyResources(t *testing.T) { + wh := &WebhookHandler{ + CrdClient: fakeCrdClient.NewSimpleClientset(), + } + tests := []struct { + operation admissionv1.Operation + crdType string + }{ + { + operation: admissionv1.Update, + crdType: v1alpha1.CAPApplicationVersionKind, + }, + { + operation: admissionv1.Create, + crdType: v1alpha1.CAPApplicationVersionKind, + }, + { + operation: admissionv1.Delete, + crdType: v1alpha1.CAPApplicationVersionKind, + }, + { + operation: admissionv1.Update, + crdType: v1alpha1.CAPTenantKind, + }, + { + operation: admissionv1.Create, + crdType: v1alpha1.CAPTenantKind, + }, + { + operation: admissionv1.Delete, + crdType: v1alpha1.CAPTenantKind, + }, + { + operation: admissionv1.Update, + crdType: v1alpha1.CAPApplicationKind, + }, + { + operation: admissionv1.Create, + crdType: v1alpha1.CAPApplicationKind, + }, + { + operation: admissionv1.Delete, + crdType: v1alpha1.CAPApplicationKind, + }, + } + for _, test := range tests { + t.Run("Testing admission review for empty (invalid) "+test.crdType+" resource with operation "+string(test.operation), func(t *testing.T) { + crdName := cavName + if test.crdType == v1alpha1.CAPTenantKind { + crdName = catName + } else { + crdName = caName + } + + request, recorder := getHttpRequest(test.operation, test.crdType, crdName, emptyUpdate, t) + + wh.Validate(recorder, request) + + admissionReview := admissionv1.AdmissionReview{} + bytes, _ := io.ReadAll(recorder.Body) + universalDeserializer.Decode(bytes, nil, &admissionReview) + if admissionReview.Request != nil || admissionReview.Response != nil { + t.Fatal("Invalid admission review with http error") + } + if recorder.Code != http.StatusInternalServerError { + t.Fatal("Error was not recorded correctly") + } + }) + } +} + +func TestUnhandledType(t *testing.T) { + wh := &WebhookHandler{ + CrdClient: fakeCrdClient.NewSimpleClientset(), + } + + tests := []struct { + operation admissionv1.Operation + crdType string + }{ + { + operation: admissionv1.Update, + }, + { + operation: admissionv1.Create, + }, + { + operation: admissionv1.Delete, + }, + { + operation: admissionv1.Connect, + }, + } + for _, test := range tests { + t.Run("Testing unhandled resource type validity for operation "+string(test.operation), func(t *testing.T) { + + request, recorder := getHttpRequest(test.operation, "Unhandled", "unhandled", noUpdate, t) + + wh.Validate(recorder, request) + + admissionReview := admissionv1.AdmissionReview{} + bytes, _ := io.ReadAll(recorder.Body) + universalDeserializer.Decode(bytes, nil, &admissionReview) + if !admissionReview.Response.Allowed || admissionReview.Response.UID != uid { + t.Fatal("validation response error") + } + }) + } +} + +func TestCavAndCatValidity(t *testing.T) { + // valid CAPApplication + Ca := createCaCRO() + wh := &WebhookHandler{ + CrdClient: fakeCrdClient.NewSimpleClientset(Ca), + } + + tests := []struct { + operation admissionv1.Operation + crdType string + backlogItems []string + }{ + { + operation: admissionv1.Update, + crdType: v1alpha1.CAPApplicationVersionKind, + }, + { + operation: admissionv1.Create, + crdType: v1alpha1.CAPApplicationVersionKind, + }, + { + operation: admissionv1.Delete, + crdType: v1alpha1.CAPApplicationVersionKind, + }, + { + operation: admissionv1.Connect, + crdType: v1alpha1.CAPApplicationVersionKind, + }, + { + operation: admissionv1.Update, + crdType: v1alpha1.CAPTenantKind, + }, + { + operation: admissionv1.Create, + crdType: v1alpha1.CAPTenantKind, + }, + { + operation: admissionv1.Delete, + crdType: v1alpha1.CAPTenantKind, + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2520"}, + }, + { + operation: admissionv1.Connect, + crdType: v1alpha1.CAPTenantKind, + }, + } + for _, test := range tests { + nameParts := []string{"Testing " + test.crdType + " validity for operation " + string(test.operation) + "; "} + testName := strings.Join(append(nameParts, test.backlogItems...), " ") + t.Run(testName, func(t *testing.T) { + crdName := cavName + if test.crdType == v1alpha1.CAPTenantKind { + crdName = catName + } + + request, recorder := getHttpRequest(test.operation, test.crdType, crdName, noUpdate, t) + + wh.Validate(recorder, request) + + admissionReview := admissionv1.AdmissionReview{} + bytes, err := io.ReadAll(recorder.Body) + if err != nil { + t.Fatal("io read error") + } + + universalDeserializer.Decode(bytes, nil, &admissionReview) + + var errorMessage string + if test.operation == admissionv1.Delete && test.crdType == v1alpha1.CAPTenantKind { + errorMessage = fmt.Sprintf("%s provider %s %s cannot be deleted when a consistent %s %s exists. Delete the %s instead to delete all tenants", InvalidationMessage, admissionReview.Kind, catName, v1alpha1.CAPApplicationKind, Ca.Name, v1alpha1.CAPApplicationKind) + if admissionReview.Response.Allowed || + admissionReview.Response.UID != uid || + admissionReview.APIVersion != apiVersion || + admissionReview.Response.Result.Message != errorMessage { + t.Fatal("validation response error") + } + } else if !admissionReview.Response.Allowed || + admissionReview.Response.UID != uid || + admissionReview.APIVersion != apiVersion || + admissionReview.Response.Result != nil { + t.Fatal("validation response error") + } + }) + } +} + +func TestCavAndCatInvalidityNoApp(t *testing.T) { + // no CAPApplication + wh := &WebhookHandler{ + CrdClient: fakeCrdClient.NewSimpleClientset(), + } + + tests := []struct { + operation admissionv1.Operation + crdType string + }{ + { + operation: admissionv1.Create, + crdType: v1alpha1.CAPApplicationVersionKind, + }, + { + operation: admissionv1.Create, + crdType: v1alpha1.CAPTenantKind, + }, + } + for _, test := range tests { + t.Run("Testing "+test.crdType+" invalidity with no CAPApp for operation "+string(test.operation), func(t *testing.T) { + crdName := cavName + if test.crdType == v1alpha1.CAPTenantKind { + crdName = catName + } + + request, recorder := getHttpRequest(test.operation, test.crdType, crdName, noUpdate, t) + + wh.Validate(recorder, request) + + admissionReview := admissionv1.AdmissionReview{} + bytes, err := io.ReadAll(recorder.Body) + if err != nil { + t.Fatal("io read error") + } + universalDeserializer.Decode(bytes, nil, &admissionReview) + if admissionReview.Response.Allowed || + admissionReview.Response.UID != uid || + admissionReview.APIVersion != apiVersion || + admissionReview.Response.Result.Message != fmt.Sprintf("%s %s no valid %s found for: %s.%s", InvalidationMessage, admissionReview.Kind, v1alpha1.CAPApplicationKind, metav1.NamespaceDefault, crdName) { + t.Fatal("validation response error") + } + }) + } +} + +func TestCavAndCatInvaliditySpecChange(t *testing.T) { + // valid CAPApplication + Ca := createCaCRO() + wh := &WebhookHandler{ + CrdClient: fakeCrdClient.NewSimpleClientset(Ca), + } + + tests := []struct { + operation admissionv1.Operation + crdType string + changeType updateType + }{ + { + operation: admissionv1.Update, + crdType: v1alpha1.CAPApplicationVersionKind, + changeType: appInstanceUpdate, + }, + { + operation: admissionv1.Update, + crdType: v1alpha1.CAPApplicationVersionKind, + changeType: registrySecretsUpdate, + }, + { + operation: admissionv1.Update, + crdType: v1alpha1.CAPApplicationVersionKind, + changeType: consumedBTPServicesUpdate, + }, + { + operation: admissionv1.Update, + crdType: v1alpha1.CAPApplicationVersionKind, + changeType: versionUpdate, + }, + { + operation: admissionv1.Update, + crdType: v1alpha1.CAPApplicationVersionKind, + changeType: imageUpdate, + }, + { + operation: admissionv1.Update, + crdType: v1alpha1.CAPTenantKind, + changeType: appInstanceUpdate, + }, + } + for _, test := range tests { + t.Run("Testing "+test.crdType+" invalidity with CAPApp instance change for operation "+string(test.operation), func(t *testing.T) { + crdName := cavName + if test.crdType == v1alpha1.CAPTenantKind { + crdName = catName + } + + request, recorder := getHttpRequest(test.operation, test.crdType, crdName, test.changeType, t) + + wh.Validate(recorder, request) + + admissionReview := admissionv1.AdmissionReview{} + bytes, err := io.ReadAll(recorder.Body) + if err != nil { + t.Fatal("io read error") + } + + universalDeserializer.Decode(bytes, nil, &admissionReview) + + expectedMessage := fmt.Sprintf("%s %s spec cannot be modified for: %s.%s", InvalidationMessage, admissionReview.Kind, metav1.NamespaceDefault, crdName) + if test.crdType == v1alpha1.CAPTenantKind { + expectedMessage = fmt.Sprintf("%s %s capApplicationInstance value cannot be modified for: %s.%s", InvalidationMessage, admissionReview.Kind, metav1.NamespaceDefault, crdName) + } + + if admissionReview.Response.Allowed || + admissionReview.Response.UID != uid || + admissionReview.APIVersion != apiVersion || + admissionReview.Response.Result.Message != expectedMessage { + t.Fatal("validation response error") + } + }) + } +} + +func TestCaValidity(t *testing.T) { + wh := &WebhookHandler{ + CrdClient: fakeCrdClient.NewSimpleClientset(), + } + tests := []struct { + operation admissionv1.Operation + tenantType string + }{ + { + operation: admissionv1.Delete, + }, + { + operation: admissionv1.Update, + }, + { + operation: admissionv1.Create, + }, + { + operation: admissionv1.Connect, + }, + } + for _, test := range tests { + t.Run("Testing CAPApplication validity for operation "+string(test.operation), func(t *testing.T) { + request, recorder := getHttpRequest(test.operation, v1alpha1.CAPApplicationKind, caName, noUpdate, t) + + wh.Validate(recorder, request) + + admissionReview := admissionv1.AdmissionReview{} + bytes, err := io.ReadAll(recorder.Body) + if err != nil { + t.Fatal("io read error") + } + universalDeserializer.Decode(bytes, nil, &admissionReview) + + if !admissionReview.Response.Allowed || + admissionReview.Response.UID != uid || + admissionReview.APIVersion != apiVersion || + admissionReview.Response.Result != nil { + t.Fatal("validation response error") + } + }) + } +} + +func TestCaInvalidity(t *testing.T) { + tests := []struct { + operation admissionv1.Operation + update updateType + }{ + { + operation: admissionv1.Update, + update: providerUpdate, + }, + } + for _, test := range tests { + t.Run("Testing CAPApplication invalidity for operation "+string(test.operation), func(t *testing.T) { + var crdObjects []runtime.Object + + wh := &WebhookHandler{ + CrdClient: fakeCrdClient.NewSimpleClientset(crdObjects...), + } + + request, recorder := getHttpRequest(test.operation, v1alpha1.CAPApplicationKind, caName, test.update, t) + + wh.Validate(recorder, request) + + admissionReview := admissionv1.AdmissionReview{} + bytes, err := io.ReadAll(recorder.Body) + if err != nil { + t.Fatal("io read error") + } + universalDeserializer.Decode(bytes, nil, &admissionReview) + + var errorMessage string + if test.update == providerUpdate { + errorMessage = fmt.Sprintf("%s %s provider details cannot be changed for: %s.%s", InvalidationMessage, admissionReview.Kind, metav1.NamespaceDefault, caName) + } + + if admissionReview.Response.Allowed || + admissionReview.Response.UID != uid || + admissionReview.APIVersion != apiVersion || + admissionReview.Response.Result.Message != errorMessage { + t.Fatal("validation response error") + } + }) + } +} + +func TestCavInvalidity(t *testing.T) { + Ca := createCaCRO() + wh := &WebhookHandler{ + CrdClient: fakeCrdClient.NewSimpleClientset(Ca), + } + tests := []struct { + operation admissionv1.Operation + duplicateWorkloadName bool + invalidDeploymentType bool + invalidJobType bool + onlyOneCAPTypeAllowed bool + onlyOneRouterTypeAllowed bool + onlyOneContentTypeAllowed bool + duplicatePortName bool + duplicatePortNumber bool + routerDestNameCAPChk bool + routerDestNameRouterChk bool + customTenantOpWithoutSequence bool + tenantOperationSequenceInvalid bool + invalidWorkloadInTenantOpSeq bool + missingTenantOpInSeqProvisioning bool + missingTenantOpInSeqUpgrade bool + missingTenantOpInSeqDeprovisioning bool + backlogItems []string + }{ + { + operation: admissionv1.Create, + duplicateWorkloadName: true, + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2338"}, + }, + { + operation: admissionv1.Create, + invalidDeploymentType: true, + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2338"}, + }, + { + operation: admissionv1.Create, + invalidJobType: true, + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2338"}, + }, + { + operation: admissionv1.Create, + onlyOneCAPTypeAllowed: true, + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2338"}, + }, + { + operation: admissionv1.Create, + onlyOneRouterTypeAllowed: true, + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2338"}, + }, + { + operation: admissionv1.Create, + onlyOneContentTypeAllowed: true, + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2338"}, + }, + { + operation: admissionv1.Create, + duplicatePortName: true, + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2339"}, + }, + { + operation: admissionv1.Create, + duplicatePortNumber: true, + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2339"}, + }, + { + operation: admissionv1.Create, + routerDestNameCAPChk: true, + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2339"}, + }, + { + operation: admissionv1.Create, + routerDestNameRouterChk: true, + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2339"}, + }, + { + operation: admissionv1.Create, + customTenantOpWithoutSequence: true, + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2405"}, + }, + { + operation: admissionv1.Create, + tenantOperationSequenceInvalid: true, + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2405"}, + }, + { + operation: admissionv1.Create, + invalidWorkloadInTenantOpSeq: true, + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2405"}, + }, + { + operation: admissionv1.Create, + missingTenantOpInSeqProvisioning: true, + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-3537"}, + }, + { + operation: admissionv1.Create, + missingTenantOpInSeqUpgrade: true, + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-3537"}, + }, + { + operation: admissionv1.Create, + missingTenantOpInSeqDeprovisioning: true, + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-3537"}, + }, + } + for _, test := range tests { + nameParts := []string{"Testing CAPApplicationversion invalidity for operation " + string(test.operation) + "; "} + testName := strings.Join(append(nameParts, test.backlogItems...), " ") + t.Run(testName, func(t *testing.T) { + admissionReview, err := createAdmissionRequest(test.operation, v1alpha1.CAPApplicationVersionKind, caName, noUpdate) + if err != nil { + t.Fatal("admission review error") + } + + crd := &ResponseCav{ + Metadata: Metadata{ + Name: cavName, + Namespace: metav1.NamespaceDefault, + }, + Spec: &v1alpha1.CAPApplicationVersionSpec{ + CAPApplicationInstance: caName, + Workloads: []v1alpha1.WorkloadDetails{ + { + Name: "cap-backend", + ConsumedBTPServices: []string{}, + DeploymentDefinition: &v1alpha1.DeploymentDetails{ + Type: v1alpha1.DeploymentCAP, + ContainerDetails: v1alpha1.ContainerDetails{ + Image: "foo", + }, + }, + }, + { + Name: "cap-router", + ConsumedBTPServices: []string{}, + DeploymentDefinition: &v1alpha1.DeploymentDetails{ + Type: v1alpha1.DeploymentRouter, + ContainerDetails: v1alpha1.ContainerDetails{ + Image: "foo", + }, + }, + }, + { + Name: "content", + ConsumedBTPServices: []string{}, + JobDefinition: &v1alpha1.JobDetails{ + Type: v1alpha1.JobContent, + ContainerDetails: v1alpha1.ContainerDetails{ + Image: "foo", + }, + }, + }, + }, + }, + Kind: v1alpha1.CAPApplicationVersionKind, + } + + if test.duplicateWorkloadName == true { + crd.Spec.Workloads = append(crd.Spec.Workloads, v1alpha1.WorkloadDetails{ + Name: "cap-backend", + ConsumedBTPServices: []string{}, + DeploymentDefinition: &v1alpha1.DeploymentDetails{ + Type: v1alpha1.DeploymentAdditional, + ContainerDetails: v1alpha1.ContainerDetails{ + Image: "foo", + }, + }, + }) + } else if test.invalidDeploymentType == true { + crd.Spec.Workloads[0].DeploymentDefinition.Type = "invalid" + } else if test.invalidJobType == true { + crd.Spec.Workloads[2].JobDefinition.Type = "invalid" + } else if test.onlyOneCAPTypeAllowed == true { + // add additional workload of type CAP + crd.Spec.Workloads = append(crd.Spec.Workloads, v1alpha1.WorkloadDetails{ + Name: "cap-backend-2", + ConsumedBTPServices: []string{}, + DeploymentDefinition: &v1alpha1.DeploymentDetails{ + Type: v1alpha1.DeploymentCAP, + ContainerDetails: v1alpha1.ContainerDetails{ + Image: "foo", + }, + }, + }) + } else if test.onlyOneRouterTypeAllowed == true { + // add additional workload of type Router + crd.Spec.Workloads = append(crd.Spec.Workloads, v1alpha1.WorkloadDetails{ + Name: "cap-router-2", + ConsumedBTPServices: []string{}, + DeploymentDefinition: &v1alpha1.DeploymentDetails{ + Type: v1alpha1.DeploymentRouter, + ContainerDetails: v1alpha1.ContainerDetails{ + Image: "foo", + }, + }, + }) + } else if test.onlyOneContentTypeAllowed == true { + // change existing Content to tenant Operation + crd.Spec.Workloads[2].JobDefinition.Type = v1alpha1.JobTenantOperation + } else if test.duplicatePortName == true { + crd.Spec.Workloads[0].DeploymentDefinition.Ports = []v1alpha1.Ports{ + {Name: "port-1", RouterDestinationName: "port-1-dest", Port: 4000}, {Name: "port-1", Port: 4004}, + } + } else if test.duplicatePortNumber == true { + crd.Spec.Workloads[0].DeploymentDefinition.Ports = []v1alpha1.Ports{ + {Name: "port-1", RouterDestinationName: "port-1-dest", Port: 4000}, {Name: "port-2", Port: 4000}, + } + } else if test.routerDestNameCAPChk == true { + crd.Spec.Workloads[0].DeploymentDefinition.Ports = []v1alpha1.Ports{ + {Name: "port-1", Port: 4000}, {Name: "port-2", Port: 4004}, + } + } else if test.routerDestNameRouterChk == true { + crd.Spec.Workloads[1].DeploymentDefinition.Ports = []v1alpha1.Ports{ + {Name: "port-1", RouterDestinationName: "port-1-dest", Port: 4000}, {Name: "port-2", Port: 4004}, + } + } else if test.customTenantOpWithoutSequence == true { + // add workload of type custom tenant operation + crd.Spec.Workloads = append(crd.Spec.Workloads, v1alpha1.WorkloadDetails{ + Name: "custom-tenant-operation", + ConsumedBTPServices: []string{}, + JobDefinition: &v1alpha1.JobDetails{ + Type: v1alpha1.JobCustomTenantOperation, + ContainerDetails: v1alpha1.ContainerDetails{ + Image: "foo", + }, + }, + }) + } else if test.tenantOperationSequenceInvalid == true { + // add workload of type custom tenant operation and tenant operation + crd.Spec.Workloads = append(crd.Spec.Workloads, v1alpha1.WorkloadDetails{ + Name: "tenant-operation", + ConsumedBTPServices: []string{}, + JobDefinition: &v1alpha1.JobDetails{ + Type: v1alpha1.JobTenantOperation, + ContainerDetails: v1alpha1.ContainerDetails{ + Image: "foo", + }, + }, + }) + crd.Spec.Workloads = append(crd.Spec.Workloads, v1alpha1.WorkloadDetails{ + Name: "custom-tenant-operation", + ConsumedBTPServices: []string{}, + JobDefinition: &v1alpha1.JobDetails{ + Type: v1alpha1.JobCustomTenantOperation, + ContainerDetails: v1alpha1.ContainerDetails{ + Image: "foo", + }, + }, + }) + + crd.Spec.TenantOperations = &v1alpha1.TenantOperations{ + Provisioning: []v1alpha1.TenantOperationWorkloadReference{ + {WorkloadName: "custom-tenant-operation"}, + }, + Deprovisioning: []v1alpha1.TenantOperationWorkloadReference{ + {WorkloadName: "custom-tenant-operation"}, + }, + Upgrade: []v1alpha1.TenantOperationWorkloadReference{ + {WorkloadName: "custom-tenant-operation"}, + }, + } + } else if test.invalidWorkloadInTenantOpSeq == true { + crd.Spec.Workloads = append(crd.Spec.Workloads, v1alpha1.WorkloadDetails{ + Name: "tenant-operation", + ConsumedBTPServices: []string{}, + JobDefinition: &v1alpha1.JobDetails{ + Type: v1alpha1.JobTenantOperation, + ContainerDetails: v1alpha1.ContainerDetails{ + Image: "foo", + }, + }, + }) + + crd.Spec.TenantOperations = &v1alpha1.TenantOperations{ + Provisioning: []v1alpha1.TenantOperationWorkloadReference{ + {WorkloadName: "tenant-operation"}, {WorkloadName: "custom-tenant-operation"}, + }, + Deprovisioning: []v1alpha1.TenantOperationWorkloadReference{ + {WorkloadName: "tenant-operation"}, + }, + Upgrade: []v1alpha1.TenantOperationWorkloadReference{ + {WorkloadName: "tenant-operation"}, {WorkloadName: "custom-tenant-operation"}, + }, + } + } else if test.missingTenantOpInSeqProvisioning == true || test.missingTenantOpInSeqUpgrade == true || test.missingTenantOpInSeqDeprovisioning == true { + crd.Spec.Workloads = append(crd.Spec.Workloads, v1alpha1.WorkloadDetails{ + Name: "tenant-operation", + ConsumedBTPServices: []string{}, + JobDefinition: &v1alpha1.JobDetails{ + Type: v1alpha1.JobTenantOperation, + ContainerDetails: v1alpha1.ContainerDetails{ + Image: "foo", + }, + }, + }) + crd.Spec.Workloads = append(crd.Spec.Workloads, v1alpha1.WorkloadDetails{ + Name: "custom-tenant-operation", + ConsumedBTPServices: []string{}, + JobDefinition: &v1alpha1.JobDetails{ + Type: v1alpha1.JobCustomTenantOperation, + ContainerDetails: v1alpha1.ContainerDetails{ + Image: "foo", + }, + }, + }) + + if test.missingTenantOpInSeqProvisioning == true { + crd.Spec.TenantOperations = &v1alpha1.TenantOperations{ + Provisioning: []v1alpha1.TenantOperationWorkloadReference{ + {WorkloadName: "custom-tenant-operation"}, + }, + Deprovisioning: []v1alpha1.TenantOperationWorkloadReference{ + {WorkloadName: "tenant-operation"}, {WorkloadName: "custom-tenant-operation"}, + }, + Upgrade: []v1alpha1.TenantOperationWorkloadReference{ + {WorkloadName: "tenant-operation"}, {WorkloadName: "custom-tenant-operation"}, + }, + } + } else if test.missingTenantOpInSeqUpgrade == true { + crd.Spec.TenantOperations = &v1alpha1.TenantOperations{ + Provisioning: []v1alpha1.TenantOperationWorkloadReference{ + {WorkloadName: "tenant-operation"}, {WorkloadName: "custom-tenant-operation"}, + }, + Deprovisioning: []v1alpha1.TenantOperationWorkloadReference{ + {WorkloadName: "tenant-operation"}, {WorkloadName: "custom-tenant-operation"}, + }, + Upgrade: []v1alpha1.TenantOperationWorkloadReference{ + {WorkloadName: "custom-tenant-operation"}, + }, + } + } else if test.missingTenantOpInSeqDeprovisioning == true { + crd.Spec.TenantOperations = &v1alpha1.TenantOperations{ + Provisioning: []v1alpha1.TenantOperationWorkloadReference{ + {WorkloadName: "tenant-operation"}, {WorkloadName: "custom-tenant-operation"}, + }, + Deprovisioning: []v1alpha1.TenantOperationWorkloadReference{ + {WorkloadName: "custom-tenant-operation"}, + }, + Upgrade: []v1alpha1.TenantOperationWorkloadReference{ + {WorkloadName: "tenant-operation"}, {WorkloadName: "custom-tenant-operation"}, + }, + } + } + + } + + rawBytes, _ := json.Marshal(crd) + admissionReview.Request.Object.Raw = rawBytes + bytesRequest, err := json.Marshal(admissionReview) + if err != nil { + t.Fatal("marshal error") + } + request := httptest.NewRequest(http.MethodGet, "/validate", bytes.NewBuffer(bytesRequest)) + recorder := httptest.NewRecorder() + + wh.Validate(recorder, request) + + admissionReviewRes := admissionv1.AdmissionReview{} + bytes, err := io.ReadAll(recorder.Body) + if err != nil { + t.Fatal("io read error") + } + universalDeserializer.Decode(bytes, nil, &admissionReviewRes) + + var errorMessage string + if test.duplicateWorkloadName == true { + errorMessage = fmt.Sprintf("%s %s duplicate workload name: cap-backend", InvalidationMessage, v1alpha1.CAPApplicationVersionKind) + } else if test.invalidDeploymentType == true { + errorMessage = fmt.Sprintf("%s %s invalid deployment definition type. Only supported - CAP, Router and Additional", InvalidationMessage, v1alpha1.CAPApplicationVersionKind) + } else if test.invalidJobType == true { + errorMessage = fmt.Sprintf("%s %s invalid job definition type. Only supported - Content, TenantOperation and CustomTenantOperation", InvalidationMessage, v1alpha1.CAPApplicationVersionKind) + } else if test.onlyOneCAPTypeAllowed == true { + errorMessage = fmt.Sprintf(DeploymentWorkloadCountErr, InvalidationMessage, v1alpha1.CAPApplicationVersionKind, v1alpha1.DeploymentCAP, 2, v1alpha1.DeploymentCAP) + } else if test.onlyOneRouterTypeAllowed == true { + errorMessage = fmt.Sprintf(DeploymentWorkloadCountErr, InvalidationMessage, v1alpha1.CAPApplicationVersionKind, v1alpha1.DeploymentRouter, 2, v1alpha1.DeploymentRouter) + } else if test.onlyOneContentTypeAllowed == true { + errorMessage = fmt.Sprintf(JobWorkloadCountErr, InvalidationMessage, v1alpha1.CAPApplicationVersionKind, v1alpha1.JobContent, 0, v1alpha1.JobContent) + } else if test.duplicatePortName == true { + errorMessage = fmt.Sprintf("%s %s duplicate port name: port-1 in workload - cap-backend", InvalidationMessage, v1alpha1.CAPApplicationVersionKind) + } else if test.duplicatePortNumber == true { + errorMessage = fmt.Sprintf("%s %s duplicate port number: 4000 in workload - cap-backend", InvalidationMessage, v1alpha1.CAPApplicationVersionKind) + } else if test.routerDestNameCAPChk == true { + errorMessage = fmt.Sprintf("%s %s routerDestinationName not defined in port configuration of workload - cap-backend", InvalidationMessage, v1alpha1.CAPApplicationVersionKind) + } else if test.routerDestNameRouterChk == true { + errorMessage = fmt.Sprintf("%s %s routerDestinationName should not be defined for workload of type Router - cap-router", InvalidationMessage, v1alpha1.CAPApplicationVersionKind) + } else if test.customTenantOpWithoutSequence == true { + errorMessage = fmt.Sprintf("%s %s - If a jobDefinition of type CustomTenantOperation is part of the workloads, then spec.tenantOperations must be specified", InvalidationMessage, v1alpha1.CAPApplicationVersionKind) + } else if test.tenantOperationSequenceInvalid == true { + errorMessage = fmt.Sprintf("%s %s workload tenant operation tenant-operation is not specified in spec.tenantOperations", InvalidationMessage, v1alpha1.CAPApplicationVersionKind) + } else if test.invalidWorkloadInTenantOpSeq == true { + errorMessage = fmt.Sprintf("%s %s custom-tenant-operation specified in spec.tenantOperations is not a valid workload of type TenantOperation or CustomTenantOperation", InvalidationMessage, v1alpha1.CAPApplicationVersionKind) + } else if test.missingTenantOpInSeqProvisioning == true { + errorMessage = fmt.Sprintf("%s %s - No tenant operation specified in spec.tenantOperation.provisioning", InvalidationMessage, v1alpha1.CAPApplicationVersionKind) + } else if test.missingTenantOpInSeqUpgrade == true { + errorMessage = fmt.Sprintf("%s %s - No tenant operation specified in spec.tenantOperation.upgrade", InvalidationMessage, v1alpha1.CAPApplicationVersionKind) + } else if test.missingTenantOpInSeqDeprovisioning == true { + errorMessage = fmt.Sprintf("%s %s - No tenant operation specified in spec.tenantOperation.deprovisioning", InvalidationMessage, v1alpha1.CAPApplicationVersionKind) + } + + if admissionReviewRes.Response.Allowed || admissionReviewRes.Response.Result.Message != errorMessage { + t.Fatal("validation response error") + } + }) + } +} diff --git a/cmd/web-hooks/main.go b/cmd/web-hooks/main.go new file mode 100644 index 0000000..a330cda --- /dev/null +++ b/cmd/web-hooks/main.go @@ -0,0 +1,77 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +package main + +import ( + "net/http" + "os" + "strconv" + + handler "github.com/sap/cap-operator/cmd/web-hooks/internal/handler" + "github.com/sap/cap-operator/internal/util" + "github.com/sap/cap-operator/pkg/client/clientset/versioned" + "k8s.io/klog/v2" +) + +type ServerParameters struct { + port int // webhook server port + certFile string // path to TLS certificate for https + keyFile string // path to TLS key matching for certificate + tlsEnabled bool // indicates if TLS is enabled +} + +var parameters ServerParameters + +func main() { + // check env for relevant values + portEnv := os.Getenv("WEBHOOK_PORT") + port := 8443 + var err error + + if portEnv != "" { + port, err = strconv.Atoi(portEnv) + if err != nil { + klog.Error("Error parsing Webhook server port: ", err.Error()) + } + } + + parameters.port = port + + t := os.Getenv("TLS_ENABLED") + tlsEnabled := false + + if t != "" { + tlsEnabled, err = strconv.ParseBool(t) + if err != nil { + klog.Error("Error parsing tls: ", err.Error()) + } + } + parameters.tlsEnabled = tlsEnabled + + parameters.certFile = os.Getenv("TLS_CERT") + parameters.keyFile = os.Getenv("TLS_KEY") + + if err != nil { + klog.Fatalf("Config build: ", err.Error()) + } + + config := util.GetConfig() + crdClient, err := versioned.NewForConfig(config) + if err != nil { + klog.Fatalf("could not create client for custom resources: %v", err.Error()) + } + + whHandler := &handler.WebhookHandler{ + CrdClient: crdClient, + } + + http.HandleFunc("/validate", whHandler.Validate) + + if parameters.tlsEnabled { + klog.Fatal(http.ListenAndServeTLS(":"+strconv.Itoa(parameters.port), parameters.certFile, parameters.keyFile, nil)) + } else { + klog.Fatal(http.ListenAndServe(":"+strconv.Itoa(parameters.port), nil)) + } +} diff --git a/crds/capapplication.yaml b/crds/capapplication.yaml new file mode 100644 index 0000000..cd8a2c6 --- /dev/null +++ b/crds/capapplication.yaml @@ -0,0 +1,133 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: capapplications.sme.sap.com +spec: + scope: Namespaced + names: + plural: capapplications + singular: capapplication + kind: CAPApplication + shortNames: ["ca"] + group: sme.sap.com + versions: + - name: v1alpha1 + served: true + storage: true + additionalPrinterColumns: + - name: Age + type: date + jsonPath: .metadata.creationTimestamp + - name: State + type: string + jsonPath: .status.state + subresources: + status: {} + schema: + openAPIV3Schema: + type: object + required: ["spec"] + properties: + spec: + type: object + required: + ["btpAppName", "globalAccountId", "provider", "domains", "btp"] + properties: + btpAppName: + type: string + globalAccountId: + type: string + domains: + type: object + required: ["primary", "istioIngressGatewayLabels"] + properties: + primary: + type: string + pattern: "^[a-z0-9-.]+$" + maxLength: 62 + secondary: + type: array + items: + type: string + pattern: "^[a-z0-9-.]+$" + dnsTarget: + type: string + pattern: "^[a-z0-9-.]*$" + istioIngressGatewayLabels: + type: array + items: + minItems: 1 + type: object + required: ["name", "value"] + properties: + name: + type: string + value: + type: string + minLength: 1 + provider: + type: object + required: ["subDomain", "tenantId"] + properties: + subDomain: + type: string + tenantId: + type: string + btp: + type: "object" + required: ["services"] + properties: + services: + type: array + items: + type: object + required: ["class", "name", "secret"] + properties: + class: + type: string + name: + type: string + secret: + type: string + status: + type: object + required: ["state"] + default: + state: "" + observedGeneration: -1 + properties: + observedGeneration: + type: integer + state: + type: string + enum: ["", "Consistent", "Processing", "Error", "Deleting"] + lastFullReconciliationTime: + type: string + format: datetime + conditions: + type: array + items: + type: object + required: ["type", "status"] + properties: + type: + type: string + enum: ["Ready", "AllTenantsReady", "LatestVersionReady"] + status: + type: string + enum: ["True", "False", "Unknown"] + lastUpdateTime: # Unused property + type: string + format: datetime + lastTransitionTime: + type: string + format: datetime + reason: + type: string + minLength: 1 + message: + type: string + observedGeneration: + type: integer + domainSpecHash: + type: string diff --git a/crds/capapplicationversion.yaml b/crds/capapplicationversion.yaml new file mode 100644 index 0000000..22ad825 --- /dev/null +++ b/crds/capapplicationversion.yaml @@ -0,0 +1,755 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: capapplicationversions.sme.sap.com +spec: + scope: Namespaced + names: + plural: capapplicationversions + singular: capapplicationversion + kind: CAPApplicationVersion + shortNames: ["cav"] + group: sme.sap.com + versions: + - name: v1alpha1 + served: true + storage: true + additionalPrinterColumns: + - name: Age + type: date + jsonPath: .metadata.creationTimestamp + - name: State + type: string + jsonPath: .status.state + subresources: + status: {} + schema: + openAPIV3Schema: + type: object + required: ["spec"] + properties: + spec: + type: object + required: ["capApplicationInstance", "workloads", "version"] + properties: + capApplicationInstance: + type: string + registrySecrets: + type: array + items: + type: string + workloads: + type: array + items: + type: object + required: ["name"] + oneOf: + - required: + - deploymentDefinition + - required: + - jobDefinition + properties: + consumedBTPServices: + type: array + items: + type: string + name: + type: string + pattern: "^[a-z]([-a-z0-9]*[a-z0-9])?$" + labels: + type: object + additionalProperties: + type: string + annotations: + type: object + additionalProperties: + type: string + deploymentDefinition: + type: object + required: ["type", "image"] + properties: + type: + type: string + image: + type: string + imagePullPolicy: + type: string + command: + type: array + items: + type: string + env: + type: array + items: + type: object + required: ["name"] + properties: + name: + type: string + value: + type: string + valueFrom: + type: object + properties: + secretKeyRef: + type: object + required: ["key"] + properties: + key: + type: string + name: + type: string + optional: + type: boolean + configMapKeyRef: + type: object + required: ["key"] + properties: + key: + type: string + name: + type: string + optional: + type: boolean + fieldRef: + type: object + required: ["fieldPath"] + properties: + apiVersion: + type: string + fieldPath: + type: string + resourceFieldRef: + type: object + required: ["resource"] + properties: + containerName: + type: string + resource: + type: string + divisor: + type: string + replicas: + type: integer + format: int32 + ports: + type: array + items: + type: object + properties: + appProtocol: + type: string + name: + type: string + networkPolicy: + type: string + enum: ["Application", "Cluster"] + port: + type: integer + format: int32 + routerDestinationName: + type: string + livenessProbe: + type: object + oneOf: + - required: + - exec + - required: + - grpc + - required: + - httpGet + - required: + - tcpSocket + properties: + exec: + type: object + required: ["command"] + properties: + command: + type: array + items: + type: string + httpGet: + type: object + required: ["port"] + properties: + path: + type: string + port: + type: integer + host: + type: string + scheme: + type: string + httpHeaders: + type: object + required: ["name", "value"] + properties: + name: + type: string + value: + type: string + tcpSocket: + type: object + required: ["port"] + properties: + port: + type: integer + host: + type: string + grpc: + type: object + required: ["port"] + properties: + port: + type: integer + service: + type: string + initialDelaySeconds: + type: integer + format: int32 + timeoutSeconds: + type: integer + format: int32 + periodSeconds: + type: integer + format: int32 + successThreshold: + type: integer + format: int32 + failureThreshold: + type: integer + format: int32 + readinessProbe: + type: object + oneOf: + - required: + - exec + - required: + - grpc + - required: + - httpGet + - required: + - tcpSocket + properties: + exec: + type: object + required: ["command"] + properties: + command: + type: array + items: + type: string + httpGet: + type: object + required: ["port"] + properties: + path: + type: string + port: + type: integer + host: + type: string + scheme: + type: string + httpHeaders: + type: object + required: ["name", "value"] + properties: + name: + type: string + value: + type: string + tcpSocket: + type: object + required: ["port"] + properties: + port: + type: integer + host: + type: string + grpc: + type: object + required: ["port"] + properties: + port: + type: integer + service: + type: string + initialDelaySeconds: + type: integer + format: int32 + timeoutSeconds: + type: integer + format: int32 + periodSeconds: + type: integer + format: int32 + successThreshold: + type: integer + format: int32 + failureThreshold: + type: integer + format: int32 + resources: + type: object + properties: + claims: + type: array + items: + type: object + properties: + name: + type: string + required: + - name + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + type: object + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + requests: + type: object + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + capabilities: + properties: + add: + items: + type: string + type: array + drop: + items: + type: string + type: array + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + podSecurityContext: + properties: + fsGroup: + format: int64 + type: integer + fsGroupChangePolicy: + type: string + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + supplementalGroups: + items: + format: int64 + type: integer + type: array + sysctls: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + jobDefinition: + type: object + required: ["type", "image"] + properties: + type: + type: string + image: + type: string + imagePullPolicy: + type: string + command: + type: array + items: + type: string + env: + type: array + items: + type: object + required: ["name"] + properties: + name: + type: string + value: + type: string + valueFrom: + type: object + properties: + secretKeyRef: + type: object + required: ["key"] + properties: + key: + type: string + name: + type: string + optional: + type: boolean + configMapKeyRef: + type: object + required: ["key"] + properties: + key: + type: string + name: + type: string + optional: + type: boolean + fieldRef: + type: object + required: ["fieldPath"] + properties: + apiVersion: + type: string + fieldPath: + type: string + resourceFieldRef: + type: object + required: ["resource"] + properties: + containerName: + type: string + resource: + type: string + divisor: + type: string + backoffLimit: + type: integer + format: int32 + ttlSecondsAfterFinished: + type: integer + format: int32 + resources: + type: object + properties: + claims: + type: array + items: + type: object + properties: + name: + type: string + required: + - name + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + type: object + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + requests: + type: object + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + capabilities: + properties: + add: + items: + type: string + type: array + drop: + items: + type: string + type: array + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + podSecurityContext: + properties: + fsGroup: + format: int64 + type: integer + fsGroupChangePolicy: + type: string + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + supplementalGroups: + items: + format: int64 + type: integer + type: array + sysctls: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + version: + type: string + tenantOperations: + type: object + properties: + provisioning: + type: array + items: + type: object + required: ["workloadName"] + properties: + workloadName: + type: string + pattern: "[a-z0-9-]*" + continueOnFailure: + type: boolean + deprovisioning: + type: array + items: + type: object + required: ["workloadName"] + properties: + workloadName: + type: string + pattern: "[a-z0-9-]*" + continueOnFailure: + type: boolean + upgrade: + type: array + items: + type: object + required: ["workloadName"] + properties: + workloadName: + type: string + pattern: "[a-z0-9-]*" + continueOnFailure: + type: boolean + status: + type: object + required: ["state"] + default: + state: "" + observedGeneration: -1 + properties: + observedGeneration: + type: integer + state: + type: string + enum: ["", "Ready", "Error", "Processing", "Deleting"] + finishedJobs: + type: array + items: + type: string + conditions: + type: array + items: + type: object + required: ["type", "status"] + properties: + type: + type: string + enum: ["Ready", "Error"] + status: + type: string + enum: ["True", "False", "Unknown"] + lastUpdateTime: # Unused property + type: string + format: datetime + lastTransitionTime: + type: string + format: datetime + reason: + type: string + minLength: 1 + message: + type: string + observedGeneration: + type: integer diff --git a/crds/captenant.yaml b/crds/captenant.yaml new file mode 100644 index 0000000..b9e11eb --- /dev/null +++ b/crds/captenant.yaml @@ -0,0 +1,95 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: captenants.sme.sap.com +spec: + scope: Namespaced + names: + plural: captenants + singular: captenant + kind: CAPTenant + shortNames: ["cat"] + group: sme.sap.com + versions: + - name: v1alpha1 + served: true + storage: true + additionalPrinterColumns: + - name: Age + type: date + jsonPath: .metadata.creationTimestamp + - name: State + type: string + jsonPath: .status.state + - name: Current Version + type: string + jsonPath: .status.currentCAPApplicationVersionInstance + subresources: + status: {} + schema: + openAPIV3Schema: + type: object + required: ["spec"] + properties: + spec: + type: object + required: ["capApplicationInstance", "subDomain", "tenantId"] + properties: + capApplicationInstance: + type: string + subDomain: + type: string + tenantId: + type: string + version: + type: string + versionUpgradeStrategy: + default: "always" + type: string + enum: ["always", "never"] + status: + type: object + required: ["state"] + default: + state: "" + observedGeneration: -1 + properties: + currentCAPApplicationVersionInstance: + type: string + observedGeneration: + type: integer + state: + type: string + enum: [ "", "Ready", "Provisioning", "Upgrading", "Deleting", "ProvisioningError", "UpgradeError"] + lastFullReconciliationTime: + type: string + format: datetime + previousCAPApplicationVersions: + type: array + items: + type: string + conditions: + type: array + items: + type: object + required: ["type", "status"] + properties: + type: + type: string + enum: ["Ready", "Error"] + status: + type: string + enum: ["True", "False", "Unknown"] + lastUpdateTime: # Unused property + type: string + format: datetime + lastTransitionTime: + type: string + format: datetime + reason: + type: string + minLength: 1 + message: + type: string + observedGeneration: + type: integer diff --git a/crds/captenantoperation.yaml b/crds/captenantoperation.yaml new file mode 100644 index 0000000..c272b3a --- /dev/null +++ b/crds/captenantoperation.yaml @@ -0,0 +1,106 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: captenantoperations.sme.sap.com +spec: + scope: Namespaced + names: + plural: captenantoperations + singular: captenantoperation + kind: CAPTenantOperation + shortNames: ["ctop"] + group: sme.sap.com + versions: + - name: v1alpha1 + served: true + storage: true + additionalPrinterColumns: + - name: Age + type: date + jsonPath: .metadata.creationTimestamp + - name: State + type: string + jsonPath: .status.state + subresources: + status: {} + schema: + openAPIV3Schema: + type: object + required: ["spec"] + properties: + spec: + type: object + required: + [ + "operation", + "tenantId", + "subDomain", + "capApplicationVersionInstance", + ] + properties: + operation: + type: string + enum: ["provisioning", "deprovisioning", "upgrade"] + subDomain: + type: string + tenantId: + type: string + capApplicationVersionInstance: + type: string + steps: + type: array + items: + type: object + required: ["name", "type"] + properties: + name: + type: string + pattern: "^[a-z]([-a-z0-9]*[a-z0-9])?$" + type: + type: string + enum: ["TenantOperation", "CustomTenantOperation"] + continueOnFailure: + type: boolean + status: + type: object + required: ["state"] + default: + state: "" + observedGeneration: -1 + properties: + currentStep: + type: integer + format: int32 + minimum: 0 + activeJob: + type: string + observedGeneration: + type: integer + state: + type: string + enum: ["", "Processing", "Completed", "Failed", "Deleting"] + conditions: + type: array + items: + type: object + required: ["type", "status"] + properties: + type: + type: string + enum: ["Ready"] + status: + type: string + enum: ["True", "False", "Unknown"] + lastUpdateTime: # Unused property + type: string + format: datetime + lastTransitionTime: + type: string + format: datetime + reason: + type: string + minLength: 1 + message: + type: string + observedGeneration: + type: integer diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..160d554 --- /dev/null +++ b/go.mod @@ -0,0 +1,79 @@ +module github.com/sap/cap-operator + +go 1.21 + +require ( + github.com/MicahParks/keyfunc/v2 v2.1.0 + github.com/cert-manager/cert-manager v1.12.3 + github.com/gardener/cert-management v0.10.10 + github.com/gardener/external-dns-management v0.15.8 + github.com/golang-jwt/jwt/v5 v5.0.0 + github.com/google/go-cmp v0.5.9 + github.com/google/uuid v1.3.0 + github.com/lestrrat-go/jwx/v2 v2.0.12 + golang.org/x/mod v0.12.0 + google.golang.org/protobuf v1.31.0 + istio.io/api v1.19.0-beta.0 + istio.io/client-go v1.18.1 + k8s.io/api v0.28.0 + k8s.io/apimachinery v0.28.0 + k8s.io/client-go v0.28.0 + k8s.io/code-generator v0.28.0 + k8s.io/klog/v2 v2.100.1 +) + +require github.com/google/gnostic-models v0.6.8 // indirect + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect + github.com/emicklei/go-restful/v3 v3.10.2 // indirect + github.com/evanphx/json-patch v5.6.0+incompatible // indirect + github.com/go-logr/logr v1.2.4 // indirect + github.com/go-openapi/jsonpointer v0.20.0 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.22.4 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/imdario/mergo v0.3.16 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/lestrrat-go/blackmagic v1.0.1 // indirect + github.com/lestrrat-go/httpcc v1.0.1 // indirect + github.com/lestrrat-go/httprc v1.0.4 // indirect + github.com/lestrrat-go/iter v1.0.2 // indirect + github.com/lestrrat-go/option v1.0.1 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/segmentio/asm v1.2.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/crypto v0.12.0 // indirect + golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb + golang.org/x/net v0.14.0 // indirect + golang.org/x/oauth2 v0.11.0 // indirect + golang.org/x/sys v0.11.0 // indirect + golang.org/x/term v0.11.0 // indirect + golang.org/x/text v0.12.0 // indirect + golang.org/x/time v0.3.0 // indirect + golang.org/x/tools v0.9.1 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/genproto v0.0.0-20230815205213-6bfd019c3878 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20230815205213-6bfd019c3878 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/apiextensions-apiserver v0.28.0 // indirect + k8s.io/gengo v0.0.0-20220902162205-c0856e24416d // indirect + k8s.io/kube-openapi v0.0.0-20230816210353-14e408962443 // indirect + k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect + sigs.k8s.io/gateway-api v0.7.1 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.3.0 + sigs.k8s.io/yaml v1.3.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e472e1e --- /dev/null +++ b/go.sum @@ -0,0 +1,251 @@ +github.com/MicahParks/keyfunc/v2 v2.1.0 h1:6ZXKb9Rp6qp1bDbJefnG7cTH8yMN1IC/4nf+GVjO99k= +github.com/MicahParks/keyfunc/v2 v2.1.0/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k= +github.com/cert-manager/cert-manager v1.12.3 h1:3gZkP7hHI2CjgX5qZ1Tm98YbHVXB2NGAZPVbOLb3AjU= +github.com/cert-manager/cert-manager v1.12.3/go.mod h1:/RYHUvK9cxuU5dbRyhb7g6am9jCcZc8huF3AnADE+nA= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/emicklei/go-restful/v3 v3.10.2 h1:hIovbnmBTLjHXkqEBUz3HGpXZdM7ZrE9fJIZIqlJLqE= +github.com/emicklei/go-restful/v3 v3.10.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= +github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/gardener/cert-management v0.10.10 h1:r8bjF3QEsC5iRfYd0PdjyOQFeJoWxYk8kVrpeHMeB+M= +github.com/gardener/cert-management v0.10.10/go.mod h1:XWcNx2Vur0MmmptKnR4Ema8zFDDnsDK2ovflUgFyoMs= +github.com/gardener/external-dns-management v0.15.8 h1:YQMjxmSfGbrh2Eim4jfoOrhI/3MACCMGYUwKAwlE+z8= +github.com/gardener/external-dns-management v0.15.8/go.mod h1:TK6nWd+j+e0Ypp7FDhYRnUQqqh9YxlOHBeEraepSdgA= +github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.20.0 h1:ESKJdU9ASRfaPNOPRx12IUyA1vn3R9GiE3KYD14BXdQ= +github.com/go-openapi/jsonpointer v0.20.0/go.mod h1:6PGzBjjIIumbLYysB73Klnms1mwnU4G3YHOECG3CedA= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= +github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= +github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.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-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= +github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +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.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80= +github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= +github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= +github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8= +github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= +github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= +github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= +github.com/lestrrat-go/jwx/v2 v2.0.12 h1:3d589+5w/b9b7S3DneICPW16AqTyYXB7VRjgluSDWeA= +github.com/lestrrat-go/jwx/v2 v2.0.12/go.mod h1:Mq4KN1mM7bp+5z/W5HS8aCNs5RKZ911G/0y2qUjAQuQ= +github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= +github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +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 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= +github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= +github.com/onsi/gomega v1.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU= +github.com/onsi/gomega v1.27.7/go.mod h1:1p8OOlwo2iUUDsHnOrjE5UKYJ+e3W8eQ3qSlRahPmr4= +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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= +github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +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/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.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.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +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.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb h1:mIKbk8weKhSeLH2GmUTrvx8CjkyJmnU1wFmg59CUjFA= +golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +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.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +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.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/oauth2 v0.11.0 h1:vPL4xzxBM4niKCW6g9whtaWVXTJf1U5e4aZxxFx/gbU= +golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk= +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.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +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-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo= +golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= +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= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20230815205213-6bfd019c3878 h1:Iveh6tGCJkHAjJgEqUQYGDGgbwmhjoAOz8kO/ajxefY= +google.golang.org/genproto v0.0.0-20230815205213-6bfd019c3878/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4= +google.golang.org/genproto/googleapis/api v0.0.0-20230815205213-6bfd019c3878 h1:WGq4lvB/mlicysM/dUT3SBvijH4D3sm/Ny1A4wmt2CI= +google.golang.org/genproto/googleapis/api v0.0.0-20230815205213-6bfd019c3878/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/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/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.8/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= +istio.io/api v1.19.0-beta.0 h1:ZbWXHd7kqug+TGBlzWJZwRcXHH4Sfn0jJv0WmNAMHio= +istio.io/api v1.19.0-beta.0/go.mod h1:dDMe1TsOtrRoUlBzdxqNolWXpXPQjLfbcXvqPMtQ6eo= +istio.io/client-go v1.18.1 h1:qSpKeJ0+3L9wAEfs30KaTWkifhz7YRmyXsOPnC+zMqk= +istio.io/client-go v1.18.1/go.mod h1:ha62DtaYYStYdisMZw9OG5iG92irhr2sWK7C38qCdqI= +k8s.io/api v0.28.0 h1:3j3VPWmN9tTDI68NETBWlDiA9qOiGJ7sdKeufehBYsM= +k8s.io/api v0.28.0/go.mod h1:0l8NZJzB0i/etuWnIXcwfIv+xnDOhL3lLW919AWYDuY= +k8s.io/apiextensions-apiserver v0.28.0 h1:CszgmBL8CizEnj4sj7/PtLGey6Na3YgWyGCPONv7E9E= +k8s.io/apiextensions-apiserver v0.28.0/go.mod h1:uRdYiwIuu0SyqJKriKmqEN2jThIJPhVmOWETm8ud1VE= +k8s.io/apimachinery v0.28.0 h1:ScHS2AG16UlYWk63r46oU3D5y54T53cVI5mMJwwqFNA= +k8s.io/apimachinery v0.28.0/go.mod h1:X0xh/chESs2hP9koe+SdIAcXWcQ+RM5hy0ZynB+yEvw= +k8s.io/client-go v0.28.0 h1:ebcPRDZsCjpj62+cMk1eGNX1QkMdRmQ6lmz5BLoFWeM= +k8s.io/client-go v0.28.0/go.mod h1:0Asy9Xt3U98RypWJmU1ZrRAGKhP6NqDPmptlAzK2kMc= +k8s.io/code-generator v0.28.0 h1:msdkRVJNVFgdiIJ8REl/d3cZsMB9HByFcWMmn13NyuE= +k8s.io/code-generator v0.28.0/go.mod h1:ueeSJZJ61NHBa0ccWLey6mwawum25vX61nRZ6WOzN9A= +k8s.io/gengo v0.0.0-20220902162205-c0856e24416d h1:U9tB195lKdzwqicbJvyJeOXV7Klv+wNAWENRnXEGi08= +k8s.io/gengo v0.0.0-20220902162205-c0856e24416d/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= +k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= +k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg= +k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/kube-openapi v0.0.0-20230816210353-14e408962443 h1:CAIciCnJnSOQxPd0xvpV6JU3D4AJvnYbImPpFpO9Hnw= +k8s.io/kube-openapi v0.0.0-20230816210353-14e408962443/go.mod h1:wZK2AVp1uHCp4VamDVgBP2COHZjqD1T68Rf0CM3YjSM= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/gateway-api v0.7.1 h1:Tts2jeepVkPA5rVG/iO+S43s9n7Vp7jCDhZDQYtPigQ= +sigs.k8s.io/gateway-api v0.7.1/go.mod h1:Xv0+ZMxX0lu1nSSDIIPEfbVztgNZ+3cfiYrJsa2Ooso= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.3.0 h1:UZbZAZfX0wV2zr7YZorDz6GXROfDFj6LvqCRm4VUVKk= +sigs.k8s.io/structured-merge-diff/v4 v4.3.0/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/hack/LICENSE_BOILERPLATE.txt b/hack/LICENSE_BOILERPLATE.txt new file mode 100644 index 0000000..d268da3 --- /dev/null +++ b/hack/LICENSE_BOILERPLATE.txt @@ -0,0 +1,4 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ \ No newline at end of file diff --git a/hack/api-reference/config.json b/hack/api-reference/config.json new file mode 100644 index 0000000..9b853bd --- /dev/null +++ b/hack/api-reference/config.json @@ -0,0 +1,20 @@ +{ + "hideMemberFields": [ + "TypeMeta" + ], + "hideTypePatterns": [ + "ParseError$", + "List$" + ], + "externalPackages": [ + { + "typeMatchPrefix": "^k8s\\.io/(api|apimachinery/pkg/apis)/", + "docsURLTemplate": "https://pkg.go.dev/{{.PackagePath}}#{{.TypeIdentifier}}" + } + ], + "typeDisplayNamePrefixOverrides": { + "k8s.io/api/": "Kubernetes ", + "k8s.io/apimachinery/pkg/apis/": "Kubernetes " + }, + "markdownDisabled": false +} diff --git a/hack/api-reference/generate.sh b/hack/api-reference/generate.sh new file mode 100644 index 0000000..57bc47a --- /dev/null +++ b/hack/api-reference/generate.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +set -eo pipefail + +# Change directory to the script dir (i.e. .../hack/api-reference/) +cd $(dirname "${BASH_SOURCE[0]}") + +echo "PWD: ${PWD}" + +gen-crd-api-reference-docs \ + -config config.json \ + -template-dir template \ + -api-dir ../../pkg/apis/sme.sap.com/v1alpha1 \ + -out-file ../../website/includes/api-reference.html diff --git a/hack/api-reference/template/members.tpl b/hack/api-reference/template/members.tpl new file mode 100644 index 0000000..a529c67 --- /dev/null +++ b/hack/api-reference/template/members.tpl @@ -0,0 +1,48 @@ +{{ define "members" }} + +{{ range .Members }} +{{ if not (hiddenMember .)}} + + + {{ fieldName . }}
+ + {{ if linkForType .Type }} + + {{ typeDisplayName .Type }} + + {{ else }} + {{ typeDisplayName .Type }} + {{ end }} + + + + {{ if fieldEmbedded . }} +

+ (Members of {{ fieldName . }} are embedded into this type.) +

+ {{ end}} + + {{ if isOptionalMember .}} + (Optional) + {{ end }} + + {{ safe (renderComments .CommentLines) }} + + {{ if and (eq (.Type.Name.Name) "ObjectMeta") }} + Refer to the Kubernetes API documentation for the fields of the + metadata field. + {{ end }} + + {{ if or (eq (fieldName .) "spec") }} +
+
+ + {{ template "members" .Type }} +
+ {{ end }} + + +{{ end }} +{{ end }} + +{{ end }} diff --git a/hack/api-reference/template/pkg.tpl b/hack/api-reference/template/pkg.tpl new file mode 100644 index 0000000..842ec93 --- /dev/null +++ b/hack/api-reference/template/pkg.tpl @@ -0,0 +1,49 @@ +{{ define "packages" }} + +{{ with .packages}} +

Packages:

+ +{{ end}} + +{{ range .packages }} +

+ {{- packageDisplayName . -}} +

+ + {{ with (index .GoPackages 0 )}} + {{ with .DocComments }} +
+ {{ safe (renderComments .) }} +
+ {{ end }} + {{ end }} + + Resource Types: +
    + {{- range (visibleTypes (sortedTypes .Types)) -}} + {{ if isExportedType . -}} +
  • + {{ typeDisplayName . }} +
  • + {{- end }} + {{- end -}} +
+ + {{ range (visibleTypes (sortedTypes .Types))}} + {{ template "type" . }} + {{ end }} +
+{{ end }} + +

+ Generated with gen-crd-api-reference-docs + {{ with .gitCommit }} on git commit {{ . }}{{end}}. +

+ +{{ end }} diff --git a/hack/api-reference/template/placeholder.go b/hack/api-reference/template/placeholder.go new file mode 100644 index 0000000..cc8f145 --- /dev/null +++ b/hack/api-reference/template/placeholder.go @@ -0,0 +1,2 @@ +// Placeholder file to make Go vendor this directory properly. +package template diff --git a/hack/api-reference/template/type.tpl b/hack/api-reference/template/type.tpl new file mode 100644 index 0000000..976e224 --- /dev/null +++ b/hack/api-reference/template/type.tpl @@ -0,0 +1,81 @@ +{{ define "type" }} + +

+ {{- .Name.Name }} + {{ if eq .Kind "Alias" }}({{.Underlying}} alias){{ end -}} +

+{{ with (typeReferences .) }} +

+ (Appears on: + {{- $prev := "" -}} + {{- range . -}} + {{- if $prev -}}, {{ end -}} + {{- $prev = . -}} + {{ typeDisplayName . }} + {{- end -}} + ) +

+{{ end }} + +
+ {{ safe (renderComments .CommentLines) }} +
+ +{{ with (constantsOfType .) }} + + + + + + + + + {{- range . -}} + + {{- /* + renderComments implicitly creates a

element, so we + add one to the display name as well to make the contents + of the two cells align evenly. + */ -}} +

+ + + {{- end -}} + +
ValueDescription

{{ typeDisplayName . }}

{{ safe (renderComments .CommentLines) }}
+{{ end }} + +{{ if .Members }} + + + + + + + + + {{ if isExportedType . }} + + + + + + + + + {{ end }} + {{ template "members" .}} + +
FieldDescription
+ apiVersion
+ string
+ + {{apiGroup .}} + +
+ kind
+ string +
{{.Name.Name}}
+{{ end }} + +{{ end }} diff --git a/hack/generate.sh b/hack/generate.sh new file mode 100644 index 0000000..efbedcf --- /dev/null +++ b/hack/generate.sh @@ -0,0 +1,65 @@ +#!/bin/bash + +set -o errexit +set -o nounset +set -o pipefail + +if ! which go >/dev/null; then + echo "Error: go not found in path" + exit 1 +fi + +if ! which jq >/dev/null; then + echo "Error: jq not found in path" + exit 1 +fi + +cd $(dirname "${BASH_SOURCE[0]}")/.. + +if [ ! -f go.mod ]; then + echo "Error: this is not a go module, is it?" + exit 1 +fi + +if [ -z "${CODEGEN_PKG:-}" ]; then + if [ -d ./vendor/k8s.io/code-generator ]; then + CODEGEN_PKG=./vendor/k8s.io/code-generator + else + CODEGEN_PKG=$(go mod download -json k8s.io/code-generator | jq -r '.Dir //empty') + fi +fi + +if [ -z "${GEN_PKG_PATH:-}" ]; then + GEN_PKG_PATH=$(go list -m)/pkg +fi + +echo "PWD: ${PWD}" +echo "CODEGEN_PKG: ${CODEGEN_PKG}" +echo "GEN_PKG_PATH: ${GEN_PKG_PATH}" + +rm -rf "${PWD}"/tmp +mkdir -p "${PWD}"/tmp/"${GEN_PKG_PATH}" + +ln -s "${PWD}"/pkg/apis "${PWD}"/tmp/"${GEN_PKG_PATH}"/apis + +source "${CODEGEN_PKG}/kube_codegen.sh" + +kube::codegen::gen_helpers \ + --input-pkg-root "${GEN_PKG_PATH}"/apis \ + --output-base "${PWD}"/tmp \ + --boilerplate ./hack/LICENSE_BOILERPLATE.txt + +kube::codegen::gen_client \ + --with-watch \ + --with-applyconfig \ + --input-pkg-root "${GEN_PKG_PATH}"/apis \ + --output-pkg-root "${GEN_PKG_PATH}"/client \ + --output-base "${PWD}"/tmp \ + --boilerplate ./hack/LICENSE_BOILERPLATE.txt + +echo "Moving generated code from ${PWD}/tmp/${GEN_PKG_PATH}/client to ${PWD}/pkg" +rm -rf "${PWD}"/pkg/client +mv "${PWD}"/tmp/"${GEN_PKG_PATH}"/client "${PWD}"/pkg + +echo "Done.. removing local /tmp dir" +rm -rf "${PWD}"/tmp \ No newline at end of file diff --git a/hack/helm-reference/generate.sh b/hack/helm-reference/generate.sh new file mode 100644 index 0000000..639b55b --- /dev/null +++ b/hack/helm-reference/generate.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +set -eo pipefail + +# Change dir to the root dir of this repo (should point to cap-operator) +cd $(dirname "${BASH_SOURCE[0]}")/../.. + +echo "PWD: ${PWD}" + +TEMPDIR=$(mktemp -d) +trap 'rm -rf "$TEMPDIR"' EXIT + +cp -r "$PWD"/../cap-operator-lifecycle/chart "$TEMPDIR" + +helm-docs -c "$TEMPDIR"/chart -s file +cp "$TEMPDIR"/chart/README.md "$PWD"/../cap-operator-lifecycle/chart + +cat > "$TEMPDIR"/chart/README.md.gotmpl < we simply skip reconciliation w/o errors + t.Error("no expected resources provided (no error from reconciliation)") + } + }) + + return err +} + +func verifyItemsForRequeue(expected map[int][]NamespacedResourceKey, result *ReconcileResult) error { + if result == nil && expected == nil { + return nil + } + if (result == nil || result.requeueResources == nil) && expected != nil { + return errors.New("expected items for requeue, found none") + } + if result != nil && expected == nil { + return errors.New("did not expect items for requeue") + } + for rid, ev := range expected { + if len(ev) == 0 { + continue + } + av, ok := result.requeueResources[rid] + if !ok { + return fmt.Errorf("expected items for requeue of type %s", KindMap[rid]) + } + avm := convertRequeueItemSliceToMap(av) + for _, i := range ev { + k := fmt.Sprintf("%s.%s", i.Namespace, i.Name) + if _, ok := avm[k]; !ok { + return fmt.Errorf("expected item %s of type %s for requeue", k, KindMap[rid]) + } + delete(avm, k) + } + if len(avm) > 0 { + return fmt.Errorf("did not expect the following items of type %s to be requeued: %s", KindMap[rid], getComaSeparatedKeys(avm, nil)) + } + delete(result.requeueResources, rid) + } + if len(result.requeueResources) > 0 { + return fmt.Errorf("did not expect the following item types to be requeued: %s", getComaSeparatedKeys(result.requeueResources, func(i int) string { return KindMap[i] })) + } + return nil +} + +func getComaSeparatedKeys[K constraints.Ordered, T any](m map[K]T, stringer func(key K) string) string { + s := []string{} + for k := range m { + var n string + if stringer == nil { + n = fmt.Sprintf("%v", k) + } else { + n = stringer(k) + } + s = append(s, n) + } + return strings.Join(s, ", ") +} + +func convertRequeueItemSliceToMap(s []RequeueItem) map[string]struct{} { + m := map[string]struct{}{} + for _, i := range s { + m[fmt.Sprintf("%s.%s", i.resourceKey.Namespace, i.resourceKey.Name)] = struct{}{} + } + return m +} + +func processTestData(t *testing.T, c *Controller, data TestData, dataType TestDataType) { + files := make([]string, 0) + if dataType == TestDataTypeInitial { + files = append(files, data.initialResources...) + } else { + files = append(files, data.expectedResources) + } + + var wg sync.WaitGroup + + var processFile = func(file string) { + defer wg.Done() + i, err := os.ReadFile(file) + if err != nil { + t.Error(err.Error()) + } + + fileContents := string(i) + splits := strings.Split(fileContents, "---") + for _, part := range splits { + if part == "\n" || part == "" { + continue + } + + if dataType == TestDataTypeInitial { + err = addInitialObjectToStore(t, []byte(part), c) + } else { + err = compareExpectedWithStore(t, []byte(part), c) + } + if err != nil { + t.Error(err.Error()) + } + } + } + + for _, f := range files { + wg.Add(1) + go processFile(f) + } + wg.Wait() +} + +func addInitialObjectToStore(t *testing.T, resource []byte, c *Controller) error { + decoder := scheme.Codecs.UniversalDeserializer().Decode + obj, _, err := decoder(resource, nil, nil) + if err != nil { + return err + } + + switch obj.(type) { + case *corev1.Secret, *corev1.Pod, *corev1.Namespace, *corev1.Service: + fakeClient, ok := c.kubeClient.(*k8sfake.Clientset) + if !ok { + return fmt.Errorf("controller is not using a fake clientset") + } + fakeClient.Tracker().Add(obj) + switch obj.(type) { + case *corev1.Secret: + err = c.kubeInformerFactory.Core().V1().Secrets().Informer().GetIndexer().Add(obj) + case *corev1.Pod: + err = c.kubeInformerFactory.Core().V1().Pods().Informer().GetIndexer().Add(obj) + case *corev1.Namespace: + err = c.kubeInformerFactory.Core().V1().Namespaces().Informer().GetIndexer().Add(obj) + case *corev1.Service: + err = c.kubeInformerFactory.Core().V1().Services().Informer().GetIndexer().Add(obj) + } + case *batchv1.Job: + fakeClient, ok := c.kubeClient.(*k8sfake.Clientset) + if !ok { + return fmt.Errorf("controller is not using a fake clientset") + } + fakeClient.Tracker().Add(obj) + err = c.kubeInformerFactory.Batch().V1().Jobs().Informer().GetIndexer().Add(obj) + case *gardenercertv1alpha1.Certificate: + fakeClient, ok := c.gardenerCertificateClient.(*gardenercertfake.Clientset) + if !ok { + return fmt.Errorf("controller is not using a fake clientset") + } + fakeClient.Tracker().Add(obj) + err = c.gardenerCertInformerFactory.Cert().V1alpha1().Certificates().Informer().GetIndexer().Add(obj) + case *certManagerv1.Certificate: + fakeClient, ok := c.certManagerCertificateClient.(*certManagerFake.Clientset) + if !ok { + return fmt.Errorf("controller is not using a fake clientset") + } + fakeClient.Tracker().Add(obj) + err = c.certManagerInformerFactory.Certmanager().V1().Certificates().Informer().GetIndexer().Add(obj) + case *gardenerdnsv1alpha1.DNSEntry: + fakeClient, ok := c.gardenerDNSClient.(*gardenerdnsfake.Clientset) + if !ok { + return fmt.Errorf("controller is not using a fake clientset") + } + fakeClient.Tracker().Add(obj) + err = c.gardenerDNSInformerFactory.Dns().V1alpha1().DNSEntries().Informer().GetIndexer().Add(obj) + case *istionwv1beta1.Gateway, *istionwv1beta1.VirtualService, *istionwv1beta1.DestinationRule: + fakeClient, ok := c.istioClient.(*istiofake.Clientset) + if !ok { + return fmt.Errorf("controller is not using a fake clientset") + } + metaObj, ok := getMetaObject(obj) + if !ok { + return fmt.Errorf("could not type cast event object to meta object") + } + switch obj.(type) { + case *istionwv1beta1.VirtualService: + fakeClient.Tracker().Create(schema.GroupVersionResource{Group: "networking.istio.io", Version: "v1beta1", Resource: "virtualservices"}, obj, metaObj.GetNamespace()) + err = c.istioInformerFactory.Networking().V1beta1().VirtualServices().Informer().GetIndexer().Add(obj) + case *istionwv1beta1.Gateway: + fakeClient.Tracker().Create(schema.GroupVersionResource{Group: "networking.istio.io", Version: "v1beta1", Resource: "gateways"}, obj, metaObj.GetNamespace()) + err = c.istioInformerFactory.Networking().V1beta1().Gateways().Informer().GetIndexer().Add(obj) + case *istionwv1beta1.DestinationRule: + fakeClient.Tracker().Create(schema.GroupVersionResource{Group: "networking.istio.io", Version: "v1beta1", Resource: "destinationrules"}, obj, metaObj.GetNamespace()) + err = c.istioInformerFactory.Networking().V1beta1().DestinationRules().Informer().GetIndexer().Add(obj) + } + case *v1alpha1.CAPApplication, *v1alpha1.CAPApplicationVersion, *v1alpha1.CAPTenant, *v1alpha1.CAPTenantOperation: + fakeClient, ok := c.crdClient.(*copfake.Clientset) + if !ok { + return fmt.Errorf("controller is not using a fake clientset") + } + fakeClient.Tracker().Add(obj) + switch obj.(type) { + case *v1alpha1.CAPApplication: + err = c.crdInformerFactory.Sme().V1alpha1().CAPApplications().Informer().GetIndexer().Add(obj) + case *v1alpha1.CAPApplicationVersion: + err = c.crdInformerFactory.Sme().V1alpha1().CAPApplicationVersions().Informer().GetIndexer().Add(obj) + case *v1alpha1.CAPTenant: + err = c.crdInformerFactory.Sme().V1alpha1().CAPTenants().Informer().GetIndexer().Add(obj) + case *v1alpha1.CAPTenantOperation: + err = c.crdInformerFactory.Sme().V1alpha1().CAPTenantOperations().Informer().GetIndexer().Add(obj) + } + default: + return fmt.Errorf("unknown object type") + } + + return err +} + +func compareExpectedWithStore(t *testing.T, resource []byte, c *Controller) error { + decoder := scheme.Codecs.UniversalDeserializer().Decode + expected, gvk, err := decoder(resource, nil, nil) + if err != nil { + return err + } + + mo, ok := expected.(metav1.Object) + if !ok { + return fmt.Errorf("expected object is not a meta object") + } + + var actual runtime.Object + switch expected.(type) { + case *corev1.Secret: + actual, err = c.kubeClient.(*k8sfake.Clientset).Tracker().Get(gvk.GroupVersion().WithResource("secrets"), mo.GetNamespace(), mo.GetName()) + case *corev1.Service: + actual, err = c.kubeClient.(*k8sfake.Clientset).Tracker().Get(gvk.GroupVersion().WithResource("services"), mo.GetNamespace(), mo.GetName()) + case *batchv1.Job: + actual, err = c.kubeClient.(*k8sfake.Clientset).Tracker().Get(gvk.GroupVersion().WithResource("jobs"), mo.GetNamespace(), mo.GetName()) + case *appsv1.Deployment: + actual, err = c.kubeClient.(*k8sfake.Clientset).Tracker().Get(gvk.GroupVersion().WithResource("deployments"), mo.GetNamespace(), mo.GetName()) + case *networkingv1.NetworkPolicy: + actual, err = c.kubeClient.(*k8sfake.Clientset).Tracker().Get(gvk.GroupVersion().WithResource("networkpolicies"), mo.GetNamespace(), mo.GetName()) + case *gardenercertv1alpha1.Certificate: + actual, err = c.gardenerCertificateClient.(*gardenercertfake.Clientset).Tracker().Get(gvk.GroupVersion().WithResource("certificates.cert.gardener.cloud"), mo.GetNamespace(), mo.GetName()) + case *certManagerv1.Certificate: + actual, err = c.certManagerCertificateClient.(*certManagerFake.Clientset).Tracker().Get(gvk.GroupVersion().WithResource("certificates.cert.gardener.cloud"), mo.GetNamespace(), mo.GetName()) + case *gardenerdnsv1alpha1.DNSEntry: + actual, err = c.gardenerDNSClient.(*gardenerdnsfake.Clientset).Tracker().Get(gvk.GroupVersion().WithResource("dnsentries"), mo.GetNamespace(), mo.GetName()) + case *istionwv1beta1.Gateway, *istionwv1beta1.VirtualService, *istionwv1beta1.DestinationRule: + fakeClient := c.istioClient.(*istiofake.Clientset) + switch expected.(type) { + case *istionwv1beta1.VirtualService: + actual, err = fakeClient.Tracker().Get(gvk.GroupVersion().WithResource("virtualservices"), mo.GetNamespace(), mo.GetName()) + case *istionwv1beta1.DestinationRule: + actual, err = fakeClient.Tracker().Get(gvk.GroupVersion().WithResource("destinationrules"), mo.GetNamespace(), mo.GetName()) + case *istionwv1beta1.Gateway: + actual, err = fakeClient.Tracker().Get(gvk.GroupVersion().WithResource("gateways"), mo.GetNamespace(), mo.GetName()) + } + case *v1alpha1.CAPApplication, *v1alpha1.CAPApplicationVersion, *v1alpha1.CAPTenant, *v1alpha1.CAPTenantOperation: + fakeClient := c.crdClient.(*copfake.Clientset) + switch expected.(type) { + case *v1alpha1.CAPApplication: + actual, err = fakeClient.Tracker().Get(gvk.GroupVersion().WithResource("capapplications"), mo.GetNamespace(), mo.GetName()) + case *v1alpha1.CAPApplicationVersion: + actual, err = fakeClient.Tracker().Get(gvk.GroupVersion().WithResource("capapplicationversions"), mo.GetNamespace(), mo.GetName()) + case *v1alpha1.CAPTenant: + actual, err = fakeClient.Tracker().Get(gvk.GroupVersion().WithResource("captenants"), mo.GetNamespace(), mo.GetName()) + case *v1alpha1.CAPTenantOperation: + actual, err = fakeClient.Tracker().Get(gvk.GroupVersion().WithResource("captenantoperations"), mo.GetNamespace(), mo.GetName()) + } + default: + return fmt.Errorf("unknown expected object type") + } + + if err == nil { + compareResourceFields(actual, expected, t, gvk.Kind, mo.GetNamespace(), mo.GetName()) + } else { + t.Error(err.Error()) + } + + return err +} + +func compareResourceFields(actual runtime.Object, expected runtime.Object, t *testing.T, kind string, namespace string, name string) { + if diff := cmp.Diff( + actual, expected, + cmp.FilterPath(func(p cmp.Path) bool { + // NOTE: do not compare the type metadata as this is not guaranteed to be filled from the fake client + return p.String() == "TypeMeta" + }, cmp.Ignore()), + cmp.FilterPath(func(p cmp.Path) bool { + // Ignore relevant Unexported fields introduced recently by istio in Spec + ps := p.String() + return ps == "Spec" || strings.HasPrefix(ps, "Spec.") + }, cmpopts.IgnoreUnexported( + networkingv1beta1.PortSelector{}, + networkingv1beta1.Destination{}, + networkingv1beta1.HTTPRouteDestination{}, + networkingv1beta1.StringMatch{}, + networkingv1beta1.HTTPMatchRequest{}, + networkingv1beta1.HTTPRoute{}, + networkingv1beta1.VirtualService{}, + networkingv1beta1.Server{}, + networkingv1beta1.Port{}, + networkingv1beta1.DestinationRule{}, + networkingv1beta1.TrafficPolicy{}, + networkingv1beta1.LoadBalancerSettings{}, + networkingv1beta1.LoadBalancerSettings_ConsistentHashLB{}, + networkingv1beta1.LoadBalancerSettings_ConsistentHashLB_HTTPCookie{}, + durationpb.Duration{}, + )), + cmp.FilterPath(func(p cmp.Path) bool { + // Ignore relevant Unexported fields introduced recently by istio in Status + ps := p.String() + return ps == "Status" || strings.HasPrefix(ps, "Status.") + }, cmpopts.IgnoreUnexported( + istiometav1alpha1.IstioStatus{}, + )), + ); diff != "" { + t.Errorf("expected and actual resource differs for %s %s.%s", kind, namespace, name) + t.Error(diff) + } +} diff --git a/internal/controller/controller.go b/internal/controller/controller.go new file mode 100644 index 0000000..1164da7 --- /dev/null +++ b/internal/controller/controller.go @@ -0,0 +1,281 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +package controller + +import ( + "context" + "errors" + "fmt" + "reflect" + "sync" + "time" + + certManager "github.com/cert-manager/cert-manager/pkg/client/clientset/versioned" + certManagerInformers "github.com/cert-manager/cert-manager/pkg/client/informers/externalversions" + gardenerCert "github.com/gardener/cert-management/pkg/client/cert/clientset/versioned" + gardenerCertInformers "github.com/gardener/cert-management/pkg/client/cert/informers/externalversions" + gardenerDNS "github.com/gardener/external-dns-management/pkg/client/dns/clientset/versioned" + gardenerDNSInformers "github.com/gardener/external-dns-management/pkg/client/dns/informers/externalversions" + "github.com/sap/cap-operator/pkg/client/clientset/versioned" + v1alpha1scheme "github.com/sap/cap-operator/pkg/client/clientset/versioned/scheme" + crdInformers "github.com/sap/cap-operator/pkg/client/informers/externalversions" + istio "istio.io/client-go/pkg/clientset/versioned" + istioscheme "istio.io/client-go/pkg/clientset/versioned/scheme" + istioInformers "istio.io/client-go/pkg/informers/externalversions" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/informers" + "k8s.io/client-go/kubernetes" + kubescheme "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/tools/events" + "k8s.io/client-go/util/workqueue" + "k8s.io/klog/v2" +) + +type Controller struct { + kubeClient kubernetes.Interface + crdClient versioned.Interface + istioClient istio.Interface + gardenerCertificateClient gardenerCert.Interface + certManagerCertificateClient certManager.Interface + gardenerDNSClient gardenerDNS.Interface + kubeInformerFactory informers.SharedInformerFactory + crdInformerFactory crdInformers.SharedInformerFactory + istioInformerFactory istioInformers.SharedInformerFactory + gardenerCertInformerFactory gardenerCertInformers.SharedInformerFactory + certManagerInformerFactory certManagerInformers.SharedInformerFactory + gardenerDNSInformerFactory gardenerDNSInformers.SharedInformerFactory + queues map[int]workqueue.RateLimitingInterface + eventBroadcaster events.EventBroadcaster + eventRecorder events.EventRecorder +} + +func NewController(client kubernetes.Interface, crdClient versioned.Interface, istioClient istio.Interface, gardenerCertificateClient gardenerCert.Interface, certManagerCertificateClient certManager.Interface, gardenerDNSClient gardenerDNS.Interface) *Controller { + queues := map[int]workqueue.RateLimitingInterface{ + ResourceCAPApplication: workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()), + ResourceCAPApplicationVersion: workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()), + ResourceCAPTenant: workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()), + ResourceCAPTenantOperation: workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()), + ResourceOperatorDomains: workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()), + } + + // Use 30mins as the default Resync interval for kube / proprietary resources + kubeInformerFactory := informers.NewSharedInformerFactory(client, 30*time.Minute) + istioInformerFactory := istioInformers.NewSharedInformerFactory(istioClient, 30*time.Minute) + + var gardenerCertInformerFactory gardenerCertInformers.SharedInformerFactory + var gardenerDNSInformerFactory gardenerDNSInformers.SharedInformerFactory + var certManagerInformerFactory certManagerInformers.SharedInformerFactory + switch certificateManager() { + case certManagerGardener: + gardenerCertInformerFactory = gardenerCertInformers.NewSharedInformerFactory(gardenerCertificateClient, 30*time.Minute) + case certManagerCertManagerIO: + certManagerInformerFactory = certManagerInformers.NewSharedInformerFactory(certManagerCertificateClient, 30*time.Minute) + } + switch dnsManager() { + case dnsManagerGardener: + gardenerDNSInformerFactory = gardenerDNSInformers.NewSharedInformerFactory(gardenerDNSClient, 30*time.Minute) + case dnsManagerKubernetes: + // no activity needed on our side so far + } + + // Use 60 as the default Resync interval for our custom resources (CAP CROs) + crdInformerFactory := crdInformers.NewSharedInformerFactory(crdClient, 60*time.Second) + + // initialize event recorder + scheme := runtime.NewScheme() + kubescheme.AddToScheme(scheme) + v1alpha1scheme.AddToScheme(scheme) + istioscheme.AddToScheme(scheme) + eventBroadcaster := events.NewBroadcaster(&events.EventSinkImpl{Interface: client.EventsV1()}) + eventBroadcaster.StartStructuredLogging(klog.Level(1)) + recorder := eventBroadcaster.NewRecorder(scheme, "cap-controller.sme.sap.com") + + c := &Controller{ + kubeClient: client, + crdClient: crdClient, + istioClient: istioClient, + gardenerCertificateClient: gardenerCertificateClient, + certManagerCertificateClient: certManagerCertificateClient, + gardenerDNSClient: gardenerDNSClient, + kubeInformerFactory: kubeInformerFactory, + crdInformerFactory: crdInformerFactory, + istioInformerFactory: istioInformerFactory, + gardenerCertInformerFactory: gardenerCertInformerFactory, + certManagerInformerFactory: certManagerInformerFactory, + gardenerDNSInformerFactory: gardenerDNSInformerFactory, + queues: queues, + eventBroadcaster: eventBroadcaster, + eventRecorder: recorder, + } + return c +} + +func throwInformerStartError(resources map[reflect.Type]bool) { + for resource, ok := range resources { + if !ok { + klog.Error("could not start informer for resource ", resource.String()) + } + } +} + +func (c *Controller) Start(ctx context.Context) { + // ensure queue shutdown + go func() { + <-ctx.Done() + for _, q := range c.queues { + q.ShutDown() + } + }() + + c.initializeInformers() + + // start event recorder + c.eventBroadcaster.StartRecordingToSink(ctx.Done()) + + // start informers and wait for cache sync + c.kubeInformerFactory.Start(ctx.Done()) + throwInformerStartError(c.kubeInformerFactory.WaitForCacheSync(ctx.Done())) + + c.crdInformerFactory.Start(ctx.Done()) + throwInformerStartError(c.crdInformerFactory.WaitForCacheSync(ctx.Done())) + + c.istioInformerFactory.Start(ctx.Done()) + throwInformerStartError(c.istioInformerFactory.WaitForCacheSync(ctx.Done())) + + switch certificateManager() { + case certManagerGardener: + c.gardenerCertInformerFactory.Start(ctx.Done()) + throwInformerStartError(c.gardenerCertInformerFactory.WaitForCacheSync(ctx.Done())) + case certManagerCertManagerIO: + c.certManagerInformerFactory.Start(ctx.Done()) + throwInformerStartError(c.certManagerInformerFactory.WaitForCacheSync(ctx.Done())) + } + + switch dnsManager() { + case dnsManagerGardener: + c.gardenerDNSInformerFactory.Start(ctx.Done()) + throwInformerStartError(c.gardenerDNSInformerFactory.WaitForCacheSync(ctx.Done())) + case dnsManagerKubernetes: + // no activity needed on our side so far + } + + // create context for worker queues + qCxt, qCancel := context.WithCancel(ctx) + defer qCancel() + + var wg sync.WaitGroup + for k := range c.queues { + wg.Add(1) + go func(key int) { + defer wg.Done() + err := c.processQueue(qCxt, key) + if err != nil { + klog.Error("worker queue ", key, " ended with error: ", err.Error()) + } + qCancel() // cancel context to inform other workers + }(k) + } + + // wait for workers + wg.Wait() +} + +func (c *Controller) processQueue(ctx context.Context, key int) error { + klog.Info("starting to process queue ", getResourceKindFromKey(key)) + for { + select { + case <-ctx.Done(): + klog.Info("context done; ending processing of queue ", getResourceKindFromKey(key)) + return nil + default: // fall through - to avoid blocking + err := c.processQueueItem(ctx, key) + if err != nil { + return err + } + } + } +} + +func (c *Controller) processQueueItem(ctx context.Context, key int) error { + q, ok := c.queues[key] + if !ok { + return fmt.Errorf("unknown queue; ending worker %d", key) + } + + klog.V(2).Info("current work queue (", getResourceKindFromKey(key), ") length: ", q.Len()) + + i, shutdown := q.Get() + if shutdown { + return fmt.Errorf("queue (%d) shutdown", key) // stop processing when the queue has been shutdown + } + + // [IMPORTANT] always mark the item as done (after processing it) + defer q.Done(i) + + var ( + err error + skipItem bool + result *ReconcileResult + ) + item, ok := i.(QueueItem) + if !ok { + klog.Error("unknown item found in queue ", getResourceKindFromKey(key)) + return nil // process next item + } + + attempts := q.NumRequeues(item) + klog.Info("processing ", item.ResourceKey.Namespace, ".", item.ResourceKey.Name, " of type ", getResourceKindFromKey(key), " (attempt ", attempts, ")") + + switch item.Key { + case ResourceCAPApplication: + result, err = c.reconcileCAPApplication(ctx, item, attempts) + case ResourceCAPApplicationVersion: + result, err = c.reconcileCAPApplicationVersion(ctx, item, attempts) + case ResourceCAPTenant: + result, err = c.reconcileCAPTenant(ctx, item, attempts) + case ResourceCAPTenantOperation: + result, err = c.reconcileCAPTenantOperation(ctx, item, attempts) + case ResourceOperatorDomains: + err = c.reconcileOperatorDomains(ctx, item, attempts) + default: + err = errors.New("unidentified queue item") + skipItem = true + } + // Handle reconcile errors + if err != nil { + klog.Error("queue processing error (", getResourceKindFromKey(key), "): ", err.Error()) + if !skipItem { + // add back to queue for re-processing + q.AddRateLimited(i) + return nil + } + } + + // Forget the item after processing it + // This just clears the rate limiter from tracking the item + q.Forget(i) + + if result != nil { + // requeue resources specified in the reconciliation result + c.processReconcileResult(result) + } + + return nil +} + +func (c *Controller) processReconcileResult(result *ReconcileResult) { + for i, items := range result.requeueResources { + q, ok := c.queues[i] + if !ok { + klog.Errorf("could not identify a resource queue with key %v", i) + return + } + for _, item := range items { + klog.Infof("(re)queueing %s.%s as %s after %s", item.resourceKey.Namespace, item.resourceKey.Name, KindMap[i], item.requeueAfter.String()) + // add back item to queue w/o rate limits for re-processing after specified duration + q.AddAfter(QueueItem{Key: i, ResourceKey: item.resourceKey}, item.requeueAfter) + } + } +} diff --git a/internal/controller/controller_test.go b/internal/controller/controller_test.go new file mode 100644 index 0000000..2a5ecf1 --- /dev/null +++ b/internal/controller/controller_test.go @@ -0,0 +1,292 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +package controller + +import ( + "context" + "reflect" + "strings" + "testing" + + "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/informers" + "k8s.io/client-go/informers/batch" + "k8s.io/client-go/informers/internalinterfaces" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/events" + "k8s.io/client-go/util/workqueue" + "k8s.io/klog/v2" +) + +type dummyInformerFactoryType struct { + informers.SharedInformerFactory + namespace string + tweakListOptions func(*metav1.ListOptions) +} + +func (f *dummyInformerFactoryType) WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool { + //Simulate error + return map[reflect.Type]bool{reflect.TypeOf(metav1.TypeMeta{}): false} +} + +func (f *dummyInformerFactoryType) Batch() batch.Interface { + return f.SharedInformerFactory.Batch() +} + +func (f *dummyInformerFactoryType) InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer { + return f.SharedInformerFactory.InformerFor(obj, newFunc) +} + +func TestController_processQueue(t *testing.T) { + tests := []struct { + name string + resource int + resourceName string + resourceNamespace string + earlyShutDown bool + expectError bool + errorString string + }{ + { + name: "Test Controller Start - process queue CAPApplication", + resource: ResourceCAPApplication, + resourceName: "test-res", + resourceNamespace: metav1.NamespaceDefault, + expectError: true, + errorString: "shutdown", + }, + { + name: "Test Controller Start - process queue Unknown resource", + resource: 9999, + expectError: true, + errorString: "unknown queue;", + }, + { + name: "Test Controller Start - process queue CAPApplication - queue shutdown", + resource: ResourceCAPApplication, + resourceName: "test-ca", + resourceNamespace: metav1.NamespaceDefault, + earlyShutDown: true, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := getTestController(testResources{preventStart: true}) + + dummyKubeInformerFactory := &dummyInformerFactoryType{c.kubeInformerFactory, tt.resourceNamespace, nil} + + testC := &Controller{ + kubeClient: c.kubeClient, + crdClient: c.crdClient, + istioClient: c.istioClient, + gardenerCertificateClient: c.gardenerCertificateClient, + gardenerDNSClient: c.gardenerDNSClient, + kubeInformerFactory: dummyKubeInformerFactory, + crdInformerFactory: c.crdInformerFactory, + istioInformerFactory: c.istioInformerFactory, + gardenerCertInformerFactory: c.gardenerCertInformerFactory, + gardenerDNSInformerFactory: c.gardenerDNSInformerFactory, + queues: c.queues, + eventBroadcaster: c.eventBroadcaster, + eventRecorder: events.NewFakeRecorder(10), + } + + // Create a background context that gets cancelled once the test run completes + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + testC.Start(ctx) + + // Manual API checks + var expectedRes error + if tt.earlyShutDown { + expectedRes = testC.processQueue(ctx, tt.resource) + } else { + expectedRes = testC.processQueue(context.TODO(), tt.resource) + } + + if !tt.expectError && expectedRes != nil { + t.Error("Unexpected result", expectedRes) + } else if tt.expectError && expectedRes == nil { + t.Error("Unexpected result", "error is nil") + } else if tt.expectError && expectedRes != nil { + res := strings.Count(expectedRes.Error(), tt.errorString) + if res < 1 { + t.Error("Unexpected result", expectedRes, "; expected to contain", tt.errorString) + } + } + }) + } +} + +func TestController_processQueueItem(t *testing.T) { + tests := []struct { + name string + createCA bool + createCAPTenant bool + resource int + resourceName string + resourceNamespace string + earlyShutDown bool + expectError bool + errorString string + expectRequeue bool + }{ + { + name: "Test Controller Start - process queue item CAPApplication", + resource: ResourceCAPApplication, + resourceName: "test-res", + resourceNamespace: metav1.NamespaceDefault, + expectError: false, + }, + { + name: "Test Controller Start - process queue item CAPApplication (Requeue)", + createCA: true, + resource: ResourceCAPApplication, + resourceName: "test-res", + resourceNamespace: metav1.NamespaceDefault, + expectError: false, + }, + { + name: "Test Controller Start - process queue item CAPApplicationVersion", + resource: ResourceCAPApplicationVersion, + resourceName: "test-res", + resourceNamespace: metav1.NamespaceDefault, + expectError: false, + }, + { + name: "Test Controller Start - process queue item CAPTenant", + resource: ResourceCAPTenant, + resourceName: "test-res", + resourceNamespace: metav1.NamespaceDefault, + expectError: false, + }, + { + name: "Test Controller Start - process queue item CAPTenantOperation", + resource: ResourceCAPTenantOperation, + resourceName: "test-res", + resourceNamespace: metav1.NamespaceDefault, + expectError: false, + }, + { + name: "Test Controller Start - process queue item unidentified queue item", + resource: 9, + resourceName: "test-res", + resourceNamespace: metav1.NamespaceDefault, + expectError: false, + }, + { + name: "Test Controller Start - process queue item Unknown item", + resource: 99, + expectError: false, + }, + { + name: "Test Controller Start - process queue item Unknown resource", + resource: 999, + expectError: true, + errorString: "unknown queue;", + }, + { + name: "Test Controller Start - process queue item CAPApplication - queue shutdown", + resource: ResourceCAPApplication, + resourceName: "test-ca", + resourceNamespace: metav1.NamespaceDefault, + earlyShutDown: true, + expectError: true, + errorString: "shutdown", + }, + { + name: "Test Controller Start - process queue item CAPTenant - reconciliation error and requeue", + createCAPTenant: true, + resource: ResourceCAPTenant, + resourceName: "ca-does-not-exist-provider", + resourceNamespace: metav1.NamespaceDefault, + expectError: false, + expectRequeue: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var ( + ca *v1alpha1.CAPApplication + cat *v1alpha1.CAPTenant + ) + if tt.createCA { + ca = createCaCRO(tt.resourceName, true) + } + if tt.createCAPTenant { + cat = createCatCRO("ca-does-not-exist", "provider", true) + } + + c := getTestController(testResources{cas: []*v1alpha1.CAPApplication{ca}, cats: []*v1alpha1.CAPTenant{cat}, preventStart: true}) + if tt.resource == 9 || tt.resource == 99 { + c.queues[tt.resource] = workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()) + } + + dummyKubeInformerFactory := &dummyInformerFactoryType{c.kubeInformerFactory, tt.resourceNamespace, nil} + + testC := &Controller{ + kubeClient: c.kubeClient, + crdClient: c.crdClient, + istioClient: c.istioClient, + gardenerCertificateClient: c.gardenerCertificateClient, + gardenerDNSClient: c.gardenerDNSClient, + kubeInformerFactory: dummyKubeInformerFactory, + crdInformerFactory: c.crdInformerFactory, + istioInformerFactory: c.istioInformerFactory, + gardenerCertInformerFactory: c.gardenerCertInformerFactory, + gardenerDNSInformerFactory: c.gardenerDNSInformerFactory, + queues: c.queues, + } + + // Create a background context that gets cancelled once the test run completes + ctx, cancel := context.WithCancel(context.Background()) + + item := QueueItem{Key: tt.resource, ResourceKey: NamespacedResourceKey{Namespace: tt.resourceNamespace, Name: tt.resourceName}} + + q := c.queues[tt.resource] + + // Manual API checks + var expectedRes error + if tt.earlyShutDown { + q.ShutDown() + cancel() + expectedRes = testC.processQueueItem(ctx, tt.resource) + } else { + if tt.resource < 4 || tt.resource == 9 { + q.Add(item) + } else if tt.resource == 99 { + q.Add(tt.resource) + } + expectedRes = testC.processQueueItem(context.TODO(), tt.resource) + } + + if !tt.expectError && expectedRes != nil { + t.Error("Unexpected result", expectedRes) + } else if tt.expectError && expectedRes == nil { + t.Error("Unexpected result", "error is nil") + } else if tt.expectError && expectedRes != nil { + res := strings.Count(expectedRes.Error(), tt.errorString) + if res < 1 { + t.Error("Unexpected result", expectedRes, "; expected to contain", tt.errorString) + } else { + klog.Info("Error res", expectedRes, " expected result: ", tt.errorString) + } + } else { + if tt.expectRequeue { + if q.NumRequeues(item) < 1 { + t.Errorf("expected item to be requeued after reconciliation error") + } + } + } + cancel() + }) + } +} diff --git a/internal/controller/informers.go b/internal/controller/informers.go new file mode 100644 index 0000000..52ae8b9 --- /dev/null +++ b/internal/controller/informers.go @@ -0,0 +1,244 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +package controller + +import ( + "reflect" + "time" + + "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/tools/cache" + "k8s.io/klog/v2" +) + +const ( + ResourceCAPTenant = iota + ResourceCAPApplicationVersion + ResourceCAPApplication + ResourceCAPTenantOperation + ResourceJob + ResourceSecret + ResourceGateway + ResourceCertificate + ResourceDNSEntry + ResourceOperatorDomains + ResourceVirtualService + ResourceDestinationRule +) + +const ( + OperatorDomains = "OperatorDomains" +) + +const queuing = "queuing " + +var ( + KindMap = map[int]string{ + ResourceCAPTenant: v1alpha1.CAPTenantKind, + ResourceCAPApplicationVersion: v1alpha1.CAPApplicationVersionKind, + ResourceCAPApplication: v1alpha1.CAPApplicationKind, + ResourceCAPTenantOperation: v1alpha1.CAPTenantOperationKind, + ResourceOperatorDomains: OperatorDomains, + } +) + +type NamespacedResourceKey struct { + Namespace string + Name string +} + +var QueueMapping map[int]map[int]string = map[int]map[int]string{ + ResourceCAPTenantOperation: {ResourceCAPTenantOperation: v1alpha1.CAPTenantOperationKind, ResourceCAPTenant: v1alpha1.CAPTenantKind}, + ResourceJob: {ResourceCAPTenantOperation: v1alpha1.CAPTenantOperationKind, ResourceCAPApplicationVersion: v1alpha1.CAPApplicationVersionKind}, + ResourceSecret: {ResourceCAPApplication: v1alpha1.CAPApplicationKind, ResourceCAPApplicationVersion: v1alpha1.CAPApplicationVersionKind}, + ResourceGateway: {ResourceCAPApplication: v1alpha1.CAPApplicationKind}, + ResourceCertificate: {ResourceCAPApplication: v1alpha1.CAPApplicationKind}, + ResourceDNSEntry: {ResourceCAPApplication: v1alpha1.CAPApplicationKind, ResourceCAPTenant: v1alpha1.CAPTenantKind}, + ResourceCAPTenant: {ResourceCAPTenant: v1alpha1.CAPTenantKind, ResourceCAPApplication: v1alpha1.CAPApplicationKind}, + ResourceVirtualService: {ResourceCAPTenant: v1alpha1.CAPTenantKind}, + ResourceDestinationRule: {ResourceCAPTenant: v1alpha1.CAPTenantKind}, + ResourceCAPApplicationVersion: {ResourceCAPApplicationVersion: v1alpha1.CAPApplicationVersionKind, ResourceCAPApplication: v1alpha1.CAPApplicationKind}, + ResourceCAPApplication: {ResourceCAPApplication: v1alpha1.CAPApplicationKind}, + ResourceOperatorDomains: {ResourceOperatorDomains: OperatorDomains}, +} + +type QueueItem struct { + Key int + ResourceKey NamespacedResourceKey +} + +func (c *Controller) initializeInformers() { + c.registerCAPTenantListeners() + c.registerCAPApplicationListeners() + c.registerCAPApplicationVersionListeners() + c.registerCAPTenantOperationListeners() + c.registerJobListeners() + c.registerSecretListeners() + c.registerGatewayListeners() + c.registerVirtualServiceListeners() + c.registerDestinationRuleListeners() + switch certificateManager() { + case certManagerGardener: + c.registerGardenerCertificateListeners() + case certManagerCertManagerIO: + c.registerCertManagerCertificateListeners() + } + switch dnsManager() { + case dnsManagerGardener: + c.registerGardenerDNSEntrytListeners() + case dnsManagerKubernetes: + // no activity needed on our side so far + } + klog.Info("informers initialized") +} + +func (c *Controller) getEventHandlerFuncsForResource(res int) cache.ResourceEventHandlerFuncs { + return cache.ResourceEventHandlerFuncs{ + AddFunc: func(new interface{}) { + c.enqueueModifiedResource(res, new, nil) + }, + UpdateFunc: func(old, new interface{}) { + c.enqueueModifiedResource(res, new, old) + }, + DeleteFunc: func(old interface{}) { + c.enqueueModifiedResource(res, old, nil) + }, + } +} + +func (c *Controller) registerCAPApplicationListeners() { + c.crdInformerFactory.Sme().V1alpha1().CAPApplications().Informer(). + AddEventHandler(c.getEventHandlerFuncsForResource(ResourceCAPApplication)) +} + +func (c *Controller) registerCAPApplicationVersionListeners() { + c.crdInformerFactory.Sme().V1alpha1().CAPApplicationVersions().Informer(). + AddEventHandler(c.getEventHandlerFuncsForResource(ResourceCAPApplicationVersion)) +} + +func (c *Controller) registerCAPTenantListeners() { + c.crdInformerFactory.Sme().V1alpha1().CAPTenants().Informer(). + AddEventHandler(c.getEventHandlerFuncsForResource(ResourceCAPTenant)) +} + +func (c *Controller) registerCAPTenantOperationListeners() { + c.crdInformerFactory.Sme().V1alpha1().CAPTenantOperations().Informer(). + AddEventHandler(c.getEventHandlerFuncsForResource(ResourceCAPTenantOperation)) +} + +func (c *Controller) registerJobListeners() { + c.kubeInformerFactory.Batch().V1().Jobs().Informer(). + AddEventHandler(c.getEventHandlerFuncsForResource(ResourceJob)) +} + +func (c *Controller) registerVirtualServiceListeners() { + c.istioInformerFactory.Networking().V1beta1().VirtualServices().Informer(). + AddEventHandler(c.getEventHandlerFuncsForResource(ResourceVirtualService)) +} + +func (c *Controller) registerDestinationRuleListeners() { + c.istioInformerFactory.Networking().V1beta1().DestinationRules().Informer(). + AddEventHandler(c.getEventHandlerFuncsForResource(ResourceDestinationRule)) +} + +func (c *Controller) registerSecretListeners() { + c.kubeInformerFactory.Core().V1().Secrets().Informer(). + AddEventHandler(c.getEventHandlerFuncsForResource(ResourceSecret)) +} + +func (c *Controller) registerGatewayListeners() { + c.istioInformerFactory.Networking().V1beta1().Gateways().Informer(). + AddEventHandler(c.getEventHandlerFuncsForResource(ResourceGateway)) +} + +func (c *Controller) registerGardenerCertificateListeners() { + c.gardenerCertInformerFactory.Cert().V1alpha1().Certificates().Informer(). + AddEventHandler(c.getEventHandlerFuncsForResource(ResourceCertificate)) +} + +func (c *Controller) registerCertManagerCertificateListeners() { + c.certManagerInformerFactory.Certmanager().V1().Certificates().Informer(). + AddEventHandler(c.getEventHandlerFuncsForResource(ResourceCertificate)) +} + +func (c *Controller) registerGardenerDNSEntrytListeners() { + c.gardenerDNSInformerFactory.Dns().V1alpha1().DNSEntries().Informer(). + AddEventHandler(c.getEventHandlerFuncsForResource(ResourceDNSEntry)) +} + +func (c *Controller) enqueueModifiedResource(sourceKey int, new, old interface{}) { + newObj, ok := getMetaObject(new) + if !ok { + return + } + + oldObj, ok := getMetaObject(old) + if ok && oldObj.GetResourceVersion() == newObj.GetResourceVersion() { + return // no changes in update + } + + mapping, ok := QueueMapping[sourceKey] + if !ok { + klog.Error("could not map modification event to a work queue for key: ", sourceKey) + return + } + + for dependentKey, dependentKind := range mapping { + q := c.queues[dependentKey] + + if dependentKey == sourceKey { + // when the change is directly on the CRO check for spec and annotation changes - omits status changes + if oldObj != nil && !hasReconciliationRelevantChanges(newObj, oldObj) { + continue // do not enqueue + } + klog.Info(queuing, newObj.GetNamespace(), ".", newObj.GetName(), " as ", dependentKind) + q.Add(QueueItem{Key: dependentKey, ResourceKey: NamespacedResourceKey{Name: newObj.GetName(), Namespace: newObj.GetNamespace()}}) + } else if owner, ok := getOwnerByKind(newObj.GetOwnerReferences(), dependentKind); ok { + klog.Info(queuing, newObj.GetNamespace(), ".", owner.Name, " as ", dependentKind) + q.Add(QueueItem{Key: dependentKey, ResourceKey: NamespacedResourceKey{Name: owner.Name, Namespace: newObj.GetNamespace()}}) + } else if owner, ok := getOwnerFromObjectMetadata(newObj, dependentKind); ok { + klog.Info(queuing, owner.Namespace, ".", owner.Name, " as ", dependentKind) + q.Add(QueueItem{Key: dependentKey, ResourceKey: NamespacedResourceKey{Name: owner.Name, Namespace: owner.Namespace}}) + } + } + + // Reconcile OperatorDomains just after all CAPApplication updates + if sourceKey == ResourceCAPApplication { + klog.Info(queuing, "all.domains", " as ", KindMap[ResourceOperatorDomains]) + // Reconcile Secondary domains via a dummy resource (separate reconciliation) after 1s + c.queues[ResourceOperatorDomains].AddAfter(QueueItem{Key: ResourceOperatorDomains, ResourceKey: NamespacedResourceKey{Namespace: metav1.NamespaceAll, Name: ""}}, 1*time.Second) + } +} + +func getMetaObject(obj interface{}) (metav1.Object, bool) { + if obj == nil { + return nil, false + } + + ok := true + metaObj, err := meta.Accessor(obj) + if err != nil { + klog.Error("could not type cast event object to meta object: ", err.Error()) + ok = false + } + return metaObj, ok +} + +func hasReconciliationRelevantChanges(newObj metav1.Object, oldObj metav1.Object) bool { + if oldObj.GetGeneration() != newObj.GetGeneration() { + // generation change denotes a change in the object spec + return true + } + + // annotation changes + if !reflect.DeepEqual(oldObj.GetAnnotations(), newObj.GetAnnotations()) { + return true + } + + // changes in owner reference + return !reflect.DeepEqual(oldObj.GetOwnerReferences(), newObj.GetOwnerReferences()) +} diff --git a/internal/controller/informers_test.go b/internal/controller/informers_test.go new file mode 100644 index 0000000..86f24ce --- /dev/null +++ b/internal/controller/informers_test.go @@ -0,0 +1,149 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +package controller + +import ( + "testing" + "time" + + "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/util/workqueue" +) + +var expectedResult = false + +type dummyType struct { + workqueue.RateLimitingInterface +} + +func (q *dummyType) Add(item interface{}) { + expectedResult = true +} + +func (q *dummyType) AddAfter(item interface{}, duration time.Duration) { + expectedResult = true +} + +func TestController_initializeInformers(t *testing.T) { + tests := []struct { + name string + expectedResult bool + invalidOwnerRef bool + res int + itemName string + itemNamespace string + }{ + { + name: "Test enqueueModifiedResource (ResourceCAPApplication)", + res: ResourceCAPApplication, + expectedResult: true, + itemName: "test-ca", + itemNamespace: corev1.NamespaceDefault, + }, + { + name: "Test enqueueModifiedResource (ResourceCAPApplicationVersion)", + res: ResourceCAPApplicationVersion, + expectedResult: true, + itemName: "test-cav", + itemNamespace: corev1.NamespaceDefault, + }, + { + name: "Test enqueueModifiedResource (ResourceCertificate) valid owner", + res: ResourceCertificate, + expectedResult: true, + itemName: "test-cert", + itemNamespace: corev1.NamespaceDefault, + }, + { + name: "Test enqueueModifiedResource (ResourceCertificate) invalid owner", + res: ResourceCertificate, + expectedResult: false, + invalidOwnerRef: true, + itemName: "test-cert", + itemNamespace: corev1.NamespaceDefault, + }, + { + name: "Test enqueueModifiedResource (unknown resource)", + res: 99, + expectedResult: false, + itemName: "test-unknown-resource", + itemNamespace: corev1.NamespaceDefault, + }, + { + name: "Test enqueueModifiedResource (invalid queue key)", + res: 999, + expectedResult: false, + itemName: "test-unknown-key", + itemNamespace: corev1.NamespaceDefault, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := getTestController(testResources{}) + expectedResult = false + + queues := map[int]workqueue.RateLimitingInterface{ + ResourceCAPApplication: &dummyType{}, + ResourceCAPApplicationVersion: &dummyType{}, + ResourceCAPTenant: &dummyType{}, + ResourceCAPTenantOperation: &dummyType{}, + ResourceOperatorDomains: &dummyType{}, + } + + testC := &Controller{ + kubeClient: c.kubeClient, + crdClient: c.crdClient, + istioClient: c.istioClient, + gardenerCertificateClient: c.gardenerCertificateClient, + gardenerDNSClient: c.gardenerDNSClient, + kubeInformerFactory: c.kubeInformerFactory, + crdInformerFactory: c.crdInformerFactory, + istioInformerFactory: c.istioInformerFactory, + gardenerCertInformerFactory: c.gardenerCertInformerFactory, + gardenerDNSInformerFactory: c.gardenerDNSInformerFactory, + queues: queues, + } + + testC.initializeInformers() + var res interface{} + switch tt.res { + case ResourceCAPApplication: + res = createCaCRO(tt.itemName, false) + case ResourceCAPApplicationVersion: + ca := createCaCRO(tt.itemName, false) + cav := createCavCRO(tt.itemName, v1alpha1.CAPApplicationVersionStateReady, defaultVersion) + cav.ObjectMeta.OwnerReferences = []metav1.OwnerReference{*metav1.NewControllerRef(ca, v1alpha1.SchemeGroupVersion.WithKind(v1alpha1.CAPApplicationKind))} + res = cav + case ResourceCertificate: + // set label on a pod to simulate certificate in a different namespace + cert := &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: tt.itemName, Annotations: map[string]string{ + AnnotationOwnerIdentifier: KindMap[ResourceCAPApplication] + "." + tt.itemNamespace + "." + tt.itemName, + }, Labels: map[string]string{ + LabelOwnerIdentifierHash: sha1Sum(KindMap[ResourceCAPApplication], tt.itemNamespace, tt.itemName), + }}} + // Invalid label + if tt.invalidOwnerRef { + cert.Annotations[AnnotationOwnerIdentifier] = tt.itemNamespace + "." + tt.itemName + cert.Labels[LabelOwnerIdentifierHash] = sha1Sum(tt.itemNamespace, tt.itemName) + } + res = cert + case 999: + res = &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: tt.itemName}} + } + // Add/delete + testC.enqueueModifiedResource(tt.res, res, nil) + if expectedResult != tt.expectedResult { + t.Error("Unexpected result", expectedResult) + } + // Update + testC.enqueueModifiedResource(tt.res, res, res) + if expectedResult != tt.expectedResult { + t.Error("Unexpected result", expectedResult) + } + }) + } +} diff --git a/internal/controller/reconcile-capapplication.go b/internal/controller/reconcile-capapplication.go new file mode 100644 index 0000000..1d809c7 --- /dev/null +++ b/internal/controller/reconcile-capapplication.go @@ -0,0 +1,408 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +package controller + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + corev1 "k8s.io/api/core/v1" + k8sErrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/klog/v2" +) + +const ( + CAPApplicationEventMissingSecrets = "MissingSecrets" + CAPApplicationEventPrimaryGatewayModified = "PrimaryGatewayModified" + CAPApplicationEventMissingIngressGatewayInfo = "MissingIngressGatewayInfo" + CAPApplicationEventProviderTenantCreated = "ProviderTenantCreated" + CAPApplicationEventNewCAVTriggeredTenantUpgrade = "NewCAVTriggeredTenantUpgrade" +) + +const ( + EventActionProcessingSecrets = "ProcessingSecrets" + EventActionProcessingDomainResources = "ProcessingDomainResources" + EventActionProviderTenantProcessing = "ProviderTenantProcessing" + EventActionCheckForVersion = "CheckForVersion" +) + +func (c *Controller) reconcileCAPApplication(ctx context.Context, item QueueItem, attempts int) (result *ReconcileResult, err error) { + lister := c.crdInformerFactory.Sme().V1alpha1().CAPApplications().Lister() + cached, err := lister.CAPApplications(item.ResourceKey.Namespace).Get(item.ResourceKey.Name) + if err != nil { + return nil, handleOperatorResourceErrors(err) + } + ca := cached.DeepCopy() + + // prepare annotations, labels, finalizers + if c.prepareCAPApplication(ctx, ca) { + if err = c.updateCAPApplication(ctx, ca); err == nil { + result = NewReconcileResultWithResource(ResourceCAPApplication, ca.Name, ca.Namespace, 0) + } + return + } + + defer func() { + if statusErr := c.updateCAPApplicationStatus(ctx, ca); statusErr != nil && err == nil { + err = statusErr + } + }() + + // check for deletion + if ca.DeletionTimestamp != nil { + return c.handleCAPApplicationDeletion(ctx, ca) + } + + if genChanged := (ca.Status.State == "Consistent" && ca.Status.ObservedGeneration != ca.Generation); ca.Status.State == "" || genChanged { + reason := "ApplicationProcessing" + message := "" + if genChanged { + reason = "ResourceDefinitionChanged" + message = "re-processing after spec update" + } + ca.SetStatusWithReadyCondition(v1alpha1.CAPApplicationStateProcessing, metav1.ConditionFalse, reason, message) + result = NewReconcileResultWithResource(ResourceCAPApplication, ca.Name, ca.Namespace, 0) + } else { + result, err = c.handleCAPApplicationDependentResources(ctx, ca, attempts) + } + + return c.checkAdditonalConditions(ctx, ca, result, err) +} + +func (c *Controller) handleCAPApplicationDependentResources(ctx context.Context, ca *v1alpha1.CAPApplication, attempts int) (requeue *ReconcileResult, err error) { + var processing bool + defer func() { + if processing { + if requeue == nil { + requeue = NewReconcileResult() + } + // requeue after 30s to check for consistency + requeue.AddResource(ResourceCAPApplication, ca.Name, ca.Namespace, 30*time.Second) + } + }() + + // step 1 - validate BTPServices + if processing, err = c.validateSecrets(ctx, ca, attempts); err != nil || processing { + return + } + + // step 2 - check for valid versions + cav, err := c.getLatestReadyCAPApplicationVersion(ctx, ca, true) + if err != nil { + // do not update the CAPApplication status - this is not an error reported by the version, but error while fetching the version + return + } + if cav == nil { + processing = true + ca.SetStatusWithReadyCondition(v1alpha1.CAPApplicationStateProcessing, metav1.ConditionFalse, "WaitingForReadyCAPApplicationVersion", "") + // Update additional condition `LatestVersionReady` to False + ca.SetStatusCondition(string(v1alpha1.ConditionTypeLatestVersionReady), metav1.ConditionFalse, "WaitingForReadyCAPApplicationVersion", "") + return + } + // We can already update LatestVersionReady to "true" at this point in time, but as this method is called several times, we do not do it here (during initial Provisioning as CA itself is may not be Consistent) + + // step 3 - queue domain handling + if requeue, err = c.handleDomains(ctx, ca); requeue != nil || err != nil { + return + } + + // step 4 - validate provider tenant, create if not available + if processing, err = c.reconcileCAPApplicationProviderTenant(ctx, ca, cav); err != nil || processing { + return + } + + // step 5 - check state of dependant resources + if processing, err = c.checkPrimaryDomainResources(ctx, ca); err != nil || processing { + return + } + + // step 6 - check and set consistent status + return c.verifyApplicationConsistent(ctx, ca) +} + +func (c *Controller) verifyApplicationConsistent(ctx context.Context, ca *v1alpha1.CAPApplication) (requeue *ReconcileResult, err error) { + if ca.Status.State != v1alpha1.CAPApplicationStateConsistent { + ca.SetStatusWithReadyCondition(v1alpha1.CAPApplicationStateConsistent, metav1.ConditionTrue, "VersionExists", "") + // Update additional condition `LatestVersionReady` to True + ca.SetStatusCondition(string(v1alpha1.ConditionTypeLatestVersionReady), metav1.ConditionTrue, "VersionExists", "") + // Update additional condition `AllTenantsReady` to True + ca.SetStatusCondition(string(v1alpha1.ConditionTypeAllTenantsReady), metav1.ConditionTrue, "ProviderTenantReady", "") + } + + // Check for newer CAPApplicationVersion + return nil, c.checkNewCAPApplicationVersion(ctx, ca) +} + +func (c *Controller) checkNewCAPApplicationVersion(ctx context.Context, ca *v1alpha1.CAPApplication) error { + cav, err := c.getLatestReadyCAPApplicationVersion(ctx, ca, false) + if err != nil { + return err + } + + // Get all relevant tenants + tenants, err := c.getRelevantTenantsForCA(ca) + if err != nil || len(tenants) == 0 { + return err + } + updated := false + for _, tenant := range tenants { + if tenant.Spec.VersionUpgradeStrategy == v1alpha1.VersionUpgradeStrategyTypeNever { + // Skip non relevant tenants + continue + } + // Assume we may have to update the tenant and prepare a copy + cat := tenant.DeepCopy() + + // Check version of tenant + if cat.Spec.Version != cav.Spec.Version { + // update CAPTenant Spec to point to the latest version + cat.Spec.Version = cav.Spec.Version + // Trigger update on CAPTenant (modifies Generation) --> which would reconcile the tenant + if _, err = c.crdClient.SmeV1alpha1().CAPTenants(ca.Namespace).Update(ctx, cat, metav1.UpdateOptions{}); err != nil { + return fmt.Errorf("could not update %s %s.%s: %w", v1alpha1.CAPTenantKind, cat.Namespace, cat.Name, err) + } + c.Event(tenant, ca, corev1.EventTypeNormal, CAPTenantEventAutoVersionUpdate, EventActionUpgrade, fmt.Sprintf("version updated to %s for initiating tenant upgrade", cav.Spec.Version)) + updated = true + } + } + if updated { + msg := fmt.Sprintf("new version %s.%s was used to trigger tenant upgrades", cav.Namespace, cav.Name) + ca.SetStatusWithReadyCondition(v1alpha1.CAPApplicationStateProcessing, metav1.ConditionFalse, CAPApplicationEventNewCAVTriggeredTenantUpgrade, msg) + ca.SetStatusCondition(string(v1alpha1.ConditionTypeLatestVersionReady), metav1.ConditionTrue, string(v1alpha1.ConditionTypeLatestVersionReady), "") + ca.SetStatusCondition(string(v1alpha1.ConditionTypeAllTenantsReady), metav1.ConditionFalse, "UpgradingTenants", "") + c.Event(ca, nil, corev1.EventTypeNormal, CAPApplicationEventNewCAVTriggeredTenantUpgrade, EventActionCheckForVersion, msg) + } + return nil +} + +func (c *Controller) checkAdditonalConditions(ctx context.Context, ca *v1alpha1.CAPApplication, result *ReconcileResult, err error) (*ReconcileResult, error) { + // In case of explicit Reconcile or errors return back with the original result + if result != nil || err != nil { + return result, err + } + + // Check and update additional status conditions + // Set ready Condition and Reason for Version check LatestVersionNotReady = True + readyCondition := metav1.ConditionTrue + readyReason := string(v1alpha1.ConditionTypeLatestVersionReady) + + // Get latest CAV (incl. ones that may not be ready) + cav, err := c.getLatestCAPApplicationVersion(ctx, ca) + if err != nil { + return nil, err + } + // When the latest CAV is not Ready --> LatestVersionNotReady = False + if cav.Status.State != v1alpha1.CAPApplicationVersionStateReady { + readyCondition = metav1.ConditionFalse + readyReason = "LatestVersionNotReady" + } + + // Update `LatestVersionReady` status condition + ca.SetStatusCondition(string(v1alpha1.ConditionTypeLatestVersionReady), readyCondition, readyReason, "") + + // Reset ready Condition and Reason for Tenant check AllTenantsReady --> True + readyCondition = metav1.ConditionTrue + readyReason = string(v1alpha1.ConditionTypeAllTenantsReady) + + // Get all relevant tenants + tenants, err := c.getRelevantTenantsForCA(ca) + if err != nil { + return nil, err + } + for _, tenant := range tenants { + // When a Tenant state is not Ready -or- when version of tenant (with VersionUpgradeStrategy = always) does not match the latest CAV version --> AllTenantsReady = False + if tenant.Status.State != v1alpha1.CAPTenantStateReady || (tenant.Spec.VersionUpgradeStrategy == v1alpha1.VersionUpgradeStrategyTypeAlways && cav.Spec.Version != tenant.Spec.Version) { + readyCondition = metav1.ConditionFalse + readyReason = "NotAllTenantsReady" + break + } + } + // Update `AllTenantsReady` status condition + ca.SetStatusCondition(string(v1alpha1.ConditionTypeAllTenantsReady), readyCondition, readyReason, "") + + return nil, nil +} + +func (c *Controller) updateCAPApplication(ctx context.Context, ca *v1alpha1.CAPApplication) error { + caUpdated, err := c.crdClient.SmeV1alpha1().CAPApplications(ca.Namespace).Update(ctx, ca, metav1.UpdateOptions{}) + // Update reference to the resource + if caUpdated != nil { + *ca = *caUpdated + } + return err +} + +func (c *Controller) updateCAPApplicationStatus(ctx context.Context, ca *v1alpha1.CAPApplication) error { + if isDeletionImminent(&ca.ObjectMeta) { + return nil + } + caUpdated, err := c.crdClient.SmeV1alpha1().CAPApplications(ca.Namespace).UpdateStatus(ctx, ca, metav1.UpdateOptions{}) + // update reference to the resource + if caUpdated != nil { + *ca = *caUpdated + } + return err +} + +// TODO: remove this entirely from CA soon +func (c *Controller) validateSecrets(ctx context.Context, ca *v1alpha1.CAPApplication, attempts int) (bool, error) { + err := c.checkSecretsExist(ca.Spec.BTP.Services, ca.Namespace) + + if err == nil { + return false, nil + } else if !k8sErrors.IsNotFound(err) { + // TODO -> clarify whether we need to set this in the status, as this is an error probably caused via api-server unavailability / connection issues + ca.SetStatusWithReadyCondition(v1alpha1.CAPApplicationStateError, metav1.ConditionFalse, "ProcessingSecretsError", err.Error()) + return false, err + } + + // waiting for secrets + message := fmt.Sprintf("waiting for secrets to get ready for %s %s.%s", v1alpha1.CAPApplicationKind, ca.Name, ca.Namespace) + klog.V(2).Info(message) + c.Event(ca, nil, corev1.EventTypeWarning, CAPApplicationEventMissingSecrets, EventActionProcessingSecrets, message) + ca.SetStatusWithReadyCondition(ca.Status.State, metav1.ConditionFalse, EventActionProcessingSecrets, message) + return true, nil +} + +func (c *Controller) getRelevantTenantsForCA(ca *v1alpha1.CAPApplication) ([]*v1alpha1.CAPTenant, error) { + tenantLabels := map[string]string{ + LabelBTPApplicationIdentifierHash: sha1Sum(ca.Spec.GlobalAccountId, ca.Spec.BTPAppName), + } + selector, err := labels.ValidatedSelectorFromSet(tenantLabels) + if err != nil { + return nil, err + } + + return c.crdInformerFactory.Sme().V1alpha1().CAPTenants().Lister().List(selector) +} + +func (c *Controller) reconcileCAPApplicationProviderTenant(ctx context.Context, ca *v1alpha1.CAPApplication, cav *v1alpha1.CAPApplicationVersion) (bool, error) { + providerTenantName := strings.Join([]string{ca.Name, ProviderTenantType}, "-") + tenant, err := c.crdInformerFactory.Sme().V1alpha1().CAPTenants().Lister().CAPTenants(ca.Namespace).Get(providerTenantName) + if err != nil { + if !k8sErrors.IsNotFound(err) { + ca.SetStatusWithReadyCondition(v1alpha1.CAPApplicationStateError, metav1.ConditionFalse, "ProviderTenantError", err.Error()) + return false, err + } + + // Create provider tenant + if tenant, err = c.crdClient.SmeV1alpha1().CAPTenants(ca.Namespace).Create( + ctx, &v1alpha1.CAPTenant{ + ObjectMeta: metav1.ObjectMeta{ + Name: providerTenantName, + Namespace: ca.Namespace, + Annotations: map[string]string{ + AnnotationBTPApplicationIdentifier: ca.Spec.GlobalAccountId + "." + ca.Spec.BTPAppName, + }, + Labels: map[string]string{ + LabelBTPApplicationIdentifierHash: sha1Sum(ca.Spec.GlobalAccountId, ca.Spec.BTPAppName), + LabelTenantType: ProviderTenantType, + LabelTenantId: ca.Spec.Provider.TenantId, + }, + }, + Spec: v1alpha1.CAPTenantSpec{ + CAPApplicationInstance: ca.Name, + BTPTenantIdentification: v1alpha1.BTPTenantIdentification{ + SubDomain: ca.Spec.Provider.SubDomain, + TenantId: ca.Spec.Provider.TenantId, + }, + Version: cav.Spec.Version, + }, + }, metav1.CreateOptions{}); err != nil { + ca.SetStatusWithReadyCondition(v1alpha1.CAPApplicationStateError, metav1.ConditionFalse, "ProviderTenantError", err.Error()) + return false, err + } + c.Event(ca, tenant, corev1.EventTypeNormal, CAPApplicationEventProviderTenantCreated, EventActionProviderTenantProcessing, fmt.Sprintf("created provider tenant %s.%s", tenant.Namespace, tenant.Name)) + } + if !isCROConditionReady(tenant.Status.GenericStatus) { + // Upgrade errors also handled + if tenant.Status.State == v1alpha1.CAPTenantStateProvisioningError || tenant.Status.State == v1alpha1.CAPTenantStateUpgradeError { + err = fmt.Errorf("provider %s in state %s for %s %s.%s", v1alpha1.CAPTenantKind, tenant.Status.State, v1alpha1.CAPApplicationKind, ca.Namespace, ca.Name) + ca.SetStatusWithReadyCondition(v1alpha1.CAPApplicationStateError, metav1.ConditionFalse, "ProviderTenantError", err.Error()) + return false, err + } + + msg := fmt.Sprintf("provider %v not ready for %v %v.%v; waiting for it to be ready", v1alpha1.CAPTenantKind, v1alpha1.CAPApplicationKind, ca.Namespace, ca.Name) + klog.Info(msg) + ca.SetStatusWithReadyCondition(v1alpha1.CAPApplicationStateProcessing, metav1.ConditionFalse, EventActionProviderTenantProcessing, msg) + return true, nil + } + + return false, nil +} + +func (c *Controller) handleCAPApplicationDeletion(ctx context.Context, ca *v1alpha1.CAPApplication) (*ReconcileResult, error) { + var err error + if ca.Status.State != v1alpha1.CAPApplicationStateDeleting { + ca.SetStatusWithReadyCondition(v1alpha1.CAPApplicationStateDeleting, metav1.ConditionFalse, "DeleteTriggered", "") + return NewReconcileResultWithResource(ResourceCAPApplication, ca.Name, ca.Namespace, 0), nil + } + + // TODO: cleanup domain resources via reconciliation + if err = c.deletePrimaryDomainCertificate(ctx, ca); err != nil && !k8sErrors.IsNotFound(err) { + return nil, err + } + + // delete CAPTenants - return if found in this loop, to verify deletion + var tenantFound bool + if tenantFound, err = c.deleteTenants(ctx, ca); tenantFound || err != nil { + return nil, err + } + + // delete CAPApplication + if removeFinalizer(&ca.Finalizers, FinalizerCAPApplication) { + return nil, c.updateCAPApplication(ctx, ca) + } + + return nil, nil +} + +func (c *Controller) deleteTenants(ctx context.Context, ca *v1alpha1.CAPApplication) (bool, error) { + tenants, err := c.getRelevantTenantsForCA(ca) + if err != nil { + return false, err + } + + // delete tenants - if not triggered yet + for _, tenant := range tenants { + if tenant.DeletionTimestamp == nil { + if err = c.crdClient.SmeV1alpha1().CAPTenants(ca.Namespace).Delete(ctx, tenant.Name, metav1.DeleteOptions{}); err != nil { + return true, err + } + } + } + + return len(tenants) > 0, nil +} + +func (c *Controller) prepareCAPApplication(ctx context.Context, ca *v1alpha1.CAPApplication) (update bool) { + // Do nothing when object is deleted + if ca.DeletionTimestamp != nil { + return false + } + // add Finalizer to prevent direct deletion + if ca.Finalizers == nil { + ca.Finalizers = []string{} + } + if addFinalizer(&ca.Finalizers, FinalizerCAPApplication) { + update = true + } + + // add Label/Annotation for BTP App + appMetadata := appMetadataIdentifiers{ + globalAccountId: ca.Spec.GlobalAccountId, + appName: ca.Spec.BTPAppName, + } + if updateLabelAnnotationMetadata(&ca.ObjectMeta, &appMetadata) { + update = true + } + + return update +} diff --git a/internal/controller/reconcile-capapplication_test.go b/internal/controller/reconcile-capapplication_test.go new file mode 100644 index 0000000..890a73b --- /dev/null +++ b/internal/controller/reconcile-capapplication_test.go @@ -0,0 +1,1128 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +package controller + +import ( + "context" + "os" + "testing" +) + +func TestCAPApplicationWithoutPreExistingLabelAndNilInitialState(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplication, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01"}}, + TestData{ + description: "Initial test without pre-existing label and nil initial state", + initialResources: []string{ + "testdata/capapplication/ca-01.initial.yaml", + }, + expectedResources: "testdata/capapplication/ca-01.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ResourceCAPApplication: {{Namespace: "default", Name: "test-cap-01"}}}, + }, + ) +} + +func TestCAPApplicationWithLabelsAndFinalizersEmptyState(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplication, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01"}}, + TestData{ + description: "application labels and finalizers set, empty state - expect processing state", + initialResources: []string{ + "testdata/capapplication/ca-42.initial.yaml", + }, + expectedResources: "testdata/capapplication/ca-42.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ResourceCAPApplication: {{Namespace: "default", Name: "test-cap-01"}}}, + }, + ) +} + +func TestCAPApplicationWithPreExistingLabelAndProcessingInitialState(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplication, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01"}}, + TestData{ + description: "Initial test with pre-existing label, processing initial state and invalid secret", + initialResources: []string{ + "testdata/capapplication/ca-02.initial.yaml", + }, + expectedResources: "testdata/capapplication/ca-02.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ResourceCAPApplication: {{Namespace: "default", Name: "test-cap-01"}}}, + }, + ) +} + +func TestCAPApplicationWithValidSecret(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplication, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01"}}, + TestData{ + description: "Test with pre-existing label, processing state and valid secret", + initialResources: []string{ + "testdata/capapplication/ca-03.initial.yaml", + "testdata/common/credential-secrets.yaml", + }, + expectedResources: "testdata/capapplication/ca-03.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ResourceCAPApplication: {{Namespace: "default", Name: "test-cap-01"}}}, + backlogItems: []string{ + "ERP4SMEPREPWORKAPPPLAT-2881", + }, + }, + ) +} + +func TestValidationOfBtpServicesWithoutSecrets(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplication, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01"}}, + TestData{ + description: "Invalid BTPServices with high attempts", + initialResources: []string{ + "testdata/capapplication/ca-02.initial.yaml", + }, + expectedResources: "testdata/capapplication/ca-02.expected.yaml", + attempts: 9999, + expectedRequeue: map[int][]NamespacedResourceKey{ResourceCAPApplication: {{Namespace: "default", Name: "test-cap-01"}}}, + }, + ) +} + +func TestValidationOfBtpServicesWithSecrets(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplication, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01"}}, + TestData{ + description: "Valid BTPServices with high attempts", + initialResources: []string{ + "testdata/capapplication/ca-03.initial.yaml", + "testdata/common/credential-secrets.yaml", + }, + expectedResources: "testdata/capapplication/ca-03.expected.yaml", + attempts: 9999, + expectedRequeue: map[int][]NamespacedResourceKey{ResourceCAPApplication: {{Namespace: "default", Name: "test-cap-01"}}}, + backlogItems: []string{ + "ERP4SMEPREPWORKAPPPLAT-2881", + }, + }, + ) +} + +func TestCavCatGateway_Case1(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplication, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01"}}, + TestData{ + description: "When CAPApplicationVersion - ready, CAPTenant - provisioning, gateway - available, certificate - ready, dnsEntry - ready", + initialResources: []string{ + "testdata/capapplication/ca-04.initial.yaml", + "testdata/common/capapplicationversion-v1.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplication/gateway.yaml", + "testdata/capapplication/istio-ingress-with-cert.yaml", + }, + expectedResources: "testdata/capapplication/ca-04.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ResourceCAPApplication: {{Namespace: "default", Name: "test-cap-01"}}}, + }, + ) +} + +func TestCavCatGateway_Case2(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplication, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01"}}, + TestData{ + description: "When CAPApplicationVersion - error, CAPTenant - provisioning, gateway - available, certificate - ready, dnsEntry - ready", + initialResources: []string{ + "testdata/capapplication/ca-05.initial.yaml", + "testdata/capapplication/ca-dns.yaml", + "testdata/capapplication/cav-error.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplication/gateway.yaml", + "testdata/capapplication/istio-ingress-with-cert.yaml", + }, + expectedResources: "testdata/capapplication/ca-05.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ResourceCAPApplication: {{Namespace: "default", Name: "test-cap-01"}}}, + backlogItems: []string{ + "ERP4SMEPREPWORKAPPPLAT-2881", + }, + }, + ) +} + +func TestCavCatGateway_Case3(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplication, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01"}}, + TestData{ + description: "When CAPApplicationVersion - ready, CAPTenant - ready, gateway - available, certificate - ready, dnsEntry - ready", + initialResources: []string{ + "testdata/capapplication/ca-06.initial.yaml", + "testdata/capapplication/ca-dns.yaml", + "testdata/common/capapplicationversion-v1.yaml", + "testdata/common/captenant-provider-ready.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplication/gateway.yaml", + "testdata/capapplication/istio-ingress-with-cert.yaml", + }, + expectedResources: "testdata/capapplication/ca-06.expected.yaml", + backlogItems: []string{ + "ERP4SMEPREPWORKAPPPLAT-2881", + }, + }, + ) +} + +func TestCavCatGateway_Case4(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplication, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01"}}, + TestData{ + description: "When CAPApplicationVersion - ready, CAPTenant - ready, gateway - available, certificate - NA, dnsEntry - NA", + initialResources: []string{ + "testdata/capapplication/ca-07.initial.yaml", + "testdata/common/capapplicationversion-v1.yaml", + "testdata/common/captenant-provider-ready.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplication/gateway.yaml", + "testdata/common/istio-ingress.yaml", + }, + expectedResources: "testdata/capapplication/ca-07.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ + ResourceOperatorDomains: {{Namespace: "", Name: ""}}, + ResourceCAPApplication: {{Namespace: "default", Name: "test-cap-01"}}, + ResourceCAPTenant: {{Namespace: "default", Name: "test-cap-01-provider"}}, + }, + }, + ) +} + +func TestCavCatGateway_Case5(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplication, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01"}}, + TestData{ + description: "When CAPApplicationVersion - ready, CAPTenant - ready, gateway - available, certificate - ready, dnsEntry - NA", + initialResources: []string{ + "testdata/capapplication/ca-08.initial.yaml", + "testdata/common/capapplicationversion-v1.yaml", + "testdata/common/captenant-provider-ready.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplication/gateway.yaml", + "testdata/common/istio-ingress.yaml", + "testdata/capapplication/istio-ingress-with-cert.yaml", + }, + expectedResources: "testdata/capapplication/ca-08.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ + ResourceOperatorDomains: {{Namespace: "", Name: ""}}, + ResourceCAPApplication: {{Namespace: "default", Name: "test-cap-01"}}, + ResourceCAPTenant: {{Namespace: "default", Name: "test-cap-01-provider"}}, + }, + }, + ) +} + +func TestCavCatGateway_Case6(t *testing.T) { + err := reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplication, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01"}}, + TestData{ + description: "When CAPApplicationVersion - ready, CAPTenant - error, gateway - available, certificate - ready, dnsEntry - ready", + initialResources: []string{ + "testdata/capapplication/ca-09.initial.yaml", + "testdata/capapplication/ca-dns.yaml", + "testdata/common/capapplicationversion-v1.yaml", + "testdata/capapplication/cat-provider-error.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplication/gateway.yaml", + "testdata/capapplication/istio-ingress-with-cert.yaml", + }, + expectedResources: "testdata/capapplication/ca-09.expected.yaml", + expectError: true, + }, + ) + + if err.Error() != "provider CAPTenant in state ProvisioningError for CAPApplication default.test-cap-01" { + t.Error("Wrong error message") + } +} + +func TestCavCatGateway_Case7(t *testing.T) { + err := reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplication, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01"}}, + TestData{ + description: "When CAPApplicationVersion - ready, CAPTenant - ready, gateway - available, certificate - error, dnsEntry - NA", + initialResources: []string{ + "testdata/capapplication/ca-10.initial.yaml", + "testdata/common/capapplicationversion-v1.yaml", + "testdata/common/captenant-provider-ready.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplication/gateway.yaml", + "testdata/capapplication/istio-ingress-with-cert-error.yaml", + }, + expectedResources: "testdata/capapplication/ca-10.expected.yaml", + expectError: true, + }, + ) + + if err.Error() != "Certificate in state Error for CAPApplication default.test-cap-01: cert message" { + t.Error("Wrong error message") + } +} + +func TestCavCatGateway_Case8(t *testing.T) { + err := reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplication, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01"}}, + TestData{ + description: "When CAPApplicationVersion - ready, CAPTenant - ready, gateway - available, certificate - ready, dnsEntry - error", + initialResources: []string{ + "testdata/capapplication/ca-11.initial.yaml", + "testdata/capapplication/ca-dns-error.yaml", + "testdata/common/capapplicationversion-v1.yaml", + "testdata/common/captenant-provider-ready.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplication/gateway.yaml", + "testdata/capapplication/istio-ingress-with-cert.yaml", + }, + expectedResources: "testdata/capapplication/ca-11.expected.yaml", + expectError: true, + }, + ) + + if err.Error() != "DNSEntry in state Error for CAPApplication default.test-cap-01: dns message" { + t.Error("Wrong error message") + } +} + +func TestCavCatGateway_Case9(t *testing.T) { + err := reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplication, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01"}}, + TestData{ + description: "When CAPApplicationVersion - ready, CAPTenant - upgrade error, gateway - available, certificate - ready, dnsEntry - ready", + initialResources: []string{ + "testdata/capapplication/ca-12.initial.yaml", + "testdata/capapplication/ca-dns.yaml", + "testdata/common/capapplicationversion-v1.yaml", + "testdata/capapplication/cat-provider-upgrade-error.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplication/gateway.yaml", + "testdata/capapplication/istio-ingress-with-cert.yaml", + }, + expectedResources: "testdata/capapplication/ca-12.expected.yaml", + expectError: true, + }, + ) + + if err.Error() != "provider CAPTenant in state UpgradeError for CAPApplication default.test-cap-01" { + t.Error("Wrong error message") + } +} + +func TestDeletion_Case1(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplication, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01"}}, + TestData{ + description: "Without deletionTimestamp and without finalizer set", + initialResources: []string{ + "testdata/capapplication/ca-13.initial.yaml", + "testdata/capapplication/cat-provider-no-finalizers-ready.yaml", + "testdata/capapplication/cat-consumer-no-finalizers-ready.yaml", + "testdata/capapplication/istio-ingress-with-cert.yaml", + }, + expectedResources: "testdata/capapplication/ca-13.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ResourceCAPApplication: {{Namespace: "default", Name: "test-cap-01"}}}, + }, + ) +} + +func TestDeletion_Case2(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplication, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01"}}, + TestData{ + description: "With deletionTimestamp and without finalizer set", + initialResources: []string{ + "testdata/capapplication/ca-14.initial.yaml", + "testdata/capapplication/cat-provider-no-finalizers-ready.yaml", + "testdata/capapplication/cat-consumer-no-finalizers-ready.yaml", + "testdata/capapplication/istio-ingress-with-cert.yaml", + }, + expectedResources: "testdata/capapplication/ca-14.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ResourceCAPApplication: {{Namespace: "default", Name: "test-cap-01"}}}, + }, + ) +} + +func TestDeletion_Case3(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplication, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01"}}, + TestData{ + description: "With deletionTimestamp and with finalizer set", + initialResources: []string{ + "testdata/capapplication/ca-15.initial.yaml", + "testdata/capapplication/cat-provider-no-finalizers-ready.yaml", + "testdata/capapplication/cat-consumer-no-finalizers-ready.yaml", + "testdata/capapplication/istio-ingress-with-cert-no-finalizers.yaml", + }, + expectedResources: "testdata/capapplication/ca-15.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ResourceCAPApplication: {{Namespace: "default", Name: "test-cap-01"}}}, + }, + ) +} + +func TestDeletion_Case4(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplication, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01"}}, + TestData{ + description: "With deletion triggered, no finalizer on certificate & provider tenant doesn't exist", + initialResources: []string{ + "testdata/capapplication/ca-16.initial.yaml", + "testdata/capapplication/istio-ingress-with-cert-no-finalizers.yaml", + }, + expectedResources: "testdata/capapplication/ca-16.expected.yaml", + }, + ) +} + +func TestDeletion_Case5(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplication, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01"}}, + TestData{ + description: "With deletion triggered, no finalizer on certificate & provider tenant exists", + initialResources: []string{ + "testdata/capapplication/ca-17.initial.yaml", + "testdata/capapplication/cat-provider-no-finalizers-ready.yaml", + "testdata/capapplication/cat-consumer-no-finalizers-ready.yaml", + "testdata/capapplication/istio-ingress-with-cert-no-finalizers.yaml", + }, + expectedResources: "testdata/capapplication/ca-17.expected.yaml", + }, + ) +} + +func TestDeletion_Case6(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplication, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01"}}, + TestData{ + description: "With deletion triggered, no certificate & provider tenant exists", + initialResources: []string{ + "testdata/capapplication/ca-18.initial.yaml", + "testdata/capapplication/cat-provider-no-finalizers-ready.yaml", + "testdata/capapplication/cat-consumer-no-finalizers-ready.yaml", + "testdata/common/istio-ingress.yaml", + }, + expectedResources: "testdata/capapplication/ca-18.expected.yaml", + }, + ) +} + +func TestDeletion_Case7(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplication, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01"}}, + TestData{ + description: "With deletion triggered, finalizer on certificate & provider tenant doesn't exist", + initialResources: []string{ + "testdata/capapplication/ca-19.initial.yaml", + "testdata/capapplication/istio-ingress-with-cert.yaml", + }, + expectedResources: "testdata/capapplication/ca-19.expected.yaml", + }, + ) +} + +func TestDeletion_Case8(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplication, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01"}}, + TestData{ + description: "With deletion triggered, finalizer on certificate & provider tenant exists", + initialResources: []string{ + "testdata/capapplication/ca-20.initial.yaml", + "testdata/capapplication/cat-provider-no-finalizers-ready.yaml", + "testdata/capapplication/cat-consumer-no-finalizers-ready.yaml", + "testdata/capapplication/istio-ingress-with-cert.yaml", + }, + expectedResources: "testdata/capapplication/ca-20.expected.yaml", + }, + ) +} + +func TestCertManagerCavCatGateway_Case1(t *testing.T) { + + os.Setenv(certManagerEnv, certManagerCertManagerIO) + + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplication, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01"}}, + TestData{ + description: "When CAPApplicationVersion - ready, CAPTenant - provisioning, gateway - available, certificate - ready, dnsEntry - ready", + initialResources: []string{ + "testdata/capapplication/ca-21.initial.yaml", + "testdata/capapplication/ca-dns.yaml", + "testdata/common/capapplicationversion-v1.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplication/gateway.yaml", + "testdata/capapplication/istio-ingress-with-certManager.yaml", + }, + expectedResources: "testdata/capapplication/ca-21.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ResourceCAPApplication: {{Namespace: "default", Name: "test-cap-01"}}}, + }, + ) + + os.Setenv(certManagerEnv, "") +} + +func TestCertManagerCavCatGateway_Case2(t *testing.T) { + + os.Setenv(certManagerEnv, certManagerCertManagerIO) + + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplication, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01"}}, + TestData{ + description: "When CAPApplicationVersion - error, CAPTenant - provisioning, gateway - available, certificate - ready, dnsEntry - ready", + initialResources: []string{ + "testdata/capapplication/ca-22.initial.yaml", + "testdata/capapplication/ca-dns.yaml", + "testdata/capapplication/cav-error.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplication/gateway.yaml", + "testdata/capapplication/istio-ingress-with-certManager.yaml", + }, + expectedResources: "testdata/capapplication/ca-22.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ResourceCAPApplication: {{Namespace: "default", Name: "test-cap-01"}}}, + backlogItems: []string{ + "ERP4SMEPREPWORKAPPPLAT-2881", + }, + }, + ) + + os.Setenv(certManagerEnv, "") +} + +func TestCertManagerCavCatGateway_Case3(t *testing.T) { + + os.Setenv(certManagerEnv, certManagerCertManagerIO) + + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplication, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01"}}, + TestData{ + description: "When CAPApplicationVersion - ready, CAPTenant - ready, gateway - available, certificate - ready, dnsEntry - ready", + initialResources: []string{ + "testdata/capapplication/ca-23.initial.yaml", + "testdata/capapplication/ca-dns.yaml", + "testdata/common/capapplicationversion-v1.yaml", + "testdata/capapplication/cat-provider-no-finalizers-ready.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplication/gateway.yaml", + "testdata/capapplication/istio-ingress-with-certManager.yaml", + }, + expectedResources: "testdata/capapplication/ca-23.expected.yaml", + backlogItems: []string{ + "ERP4SMEPREPWORKAPPPLAT-2881", + }, + }, + ) + + os.Setenv(certManagerEnv, "") +} + +func TestCertManagerCavCatGateway_Case4(t *testing.T) { + + os.Setenv(certManagerEnv, certManagerCertManagerIO) + + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplication, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01"}}, + TestData{ + description: "When CAPApplicationVersion - ready, CAPTenant - ready, gateway - available, certificate - NA, dnsEntry - NA", + initialResources: []string{ + "testdata/capapplication/ca-24.initial.yaml", + "testdata/common/capapplicationversion-v1.yaml", + "testdata/capapplication/cat-provider-no-finalizers-ready.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/common/istio-ingress.yaml", + }, + expectedResources: "testdata/capapplication/ca-24.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ + ResourceOperatorDomains: {{Namespace: "", Name: ""}}, + ResourceCAPApplication: {{Namespace: "default", Name: "test-cap-01"}}, + ResourceCAPTenant: {{Namespace: "default", Name: "test-cap-01-provider"}}, + }, + }, + ) + + os.Setenv(certManagerEnv, "") +} + +func TestCertManagerCavCatGateway_Case5(t *testing.T) { + + os.Setenv(certManagerEnv, certManagerCertManagerIO) + + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplication, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01"}}, + TestData{ + description: "When CAPApplicationVersion - ready, CAPTenant - ready, gateway - available, certificate - ready, dnsEntry - NA", + initialResources: []string{ + "testdata/capapplication/ca-25.initial.yaml", + "testdata/common/capapplicationversion-v1.yaml", + "testdata/capapplication/cat-provider-no-finalizers-ready.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplication/gateway.yaml", + "testdata/capapplication/istio-ingress-with-certManager.yaml", + }, + expectedResources: "testdata/capapplication/ca-25.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ + ResourceOperatorDomains: {{Namespace: "", Name: ""}}, + ResourceCAPApplication: {{Namespace: "default", Name: "test-cap-01"}}, + ResourceCAPTenant: {{Namespace: "default", Name: "test-cap-01-provider"}}, + }, + }, + ) + + os.Setenv(certManagerEnv, "") +} + +func TestCertManagerCavCatGateway_Case6(t *testing.T) { + + os.Setenv(certManagerEnv, certManagerCertManagerIO) + + err := reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplication, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01"}}, + TestData{ + description: "When CAPApplicationVersion - ready, CAPTenant - error, gateway - available, certificate - ready, dnsEntry - ready", + initialResources: []string{ + "testdata/capapplication/ca-26.initial.yaml", + "testdata/capapplication/ca-dns.yaml", + "testdata/common/capapplicationversion-v1.yaml", + "testdata/capapplication/cat-provider-no-finalizers-error.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplication/gateway.yaml", + "testdata/capapplication/istio-ingress-with-certManager.yaml", + }, + expectedResources: "testdata/capapplication/ca-26.expected.yaml", + expectError: true, + }, + ) + + os.Setenv(certManagerEnv, "") + + if err.Error() != "provider CAPTenant in state ProvisioningError for CAPApplication default.test-cap-01" { + t.Error("Wrong error message") + } +} + +func TestCertManagerCavCatGateway_Case7(t *testing.T) { + + os.Setenv(certManagerEnv, certManagerCertManagerIO) + + err := reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplication, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01"}}, + TestData{ + description: "When CAPApplicationVersion - ready, CAPTenant - ready, gateway - available, certificate - error, dnsEntry - NA", + initialResources: []string{ + "testdata/capapplication/ca-27.initial.yaml", + "testdata/common/capapplicationversion-v1.yaml", + "testdata/capapplication/cat-provider-no-finalizers-ready.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplication/gateway.yaml", + "testdata/capapplication/istio-ingress-with-certManager-error.yaml", + }, + expectedResources: "testdata/capapplication/ca-27.expected.yaml", + expectError: true, + }, + ) + + os.Setenv(certManagerEnv, "") + + if err.Error() != "Certificate in state not ready for CAPApplication default.test-cap-01: cert message" { + t.Error("Wrong error message") + } +} + +func TestCertManagerCavCatGateway_Case8(t *testing.T) { + + os.Setenv(certManagerEnv, certManagerCertManagerIO) + + err := reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplication, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01"}}, + TestData{ + description: "When CAPApplicationVersion - ready, CAPTenant - ready, gateway - available, certificate - ready, dnsEntry - error", + initialResources: []string{ + "testdata/capapplication/ca-28.initial.yaml", + "testdata/capapplication/ca-dns-error.yaml", + "testdata/common/capapplicationversion-v1.yaml", + "testdata/capapplication/cat-provider-no-finalizers-ready.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplication/gateway.yaml", + "testdata/capapplication/istio-ingress-with-certManager.yaml", + }, + expectedResources: "testdata/capapplication/ca-28.expected.yaml", + expectError: true, + }, + ) + + os.Setenv(certManagerEnv, "") + + if err.Error() != "DNSEntry in state Error for CAPApplication default.test-cap-01: dns message" { + t.Error("Wrong error message") + } +} + +func TestCertManagerDeletion_Case1(t *testing.T) { + + os.Setenv(certManagerEnv, certManagerCertManagerIO) + + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplication, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01"}}, + TestData{ + description: "When deletionTimestamp - false, finalizer set - false, certificate - ready, certificate finalizer - true, Provider & Consumer tenant with no finalizers - ready", + initialResources: []string{ + "testdata/capapplication/ca-34.initial.yaml", + "testdata/capapplication/cat-provider-no-finalizers-ready.yaml", + "testdata/capapplication/cat-consumer-no-finalizers-ready.yaml", + "testdata/capapplication/istio-ingress-with-certManager.yaml", + }, + expectedResources: "testdata/capapplication/ca-34.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ResourceCAPApplication: {{Namespace: "default", Name: "test-cap-01"}}}, + backlogItems: []string{ + "ERP4SMEPREPWORKAPPPLAT-3351", + }, + }, + ) + + os.Setenv(certManagerEnv, "") + +} + +func TestCertManagerDeletion_Case2(t *testing.T) { + + os.Setenv(certManagerEnv, certManagerCertManagerIO) + + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplication, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01"}}, + TestData{ + description: "When deletionTimestamp - true, finalizer set - false, certificate - ready, certificate finalizer - true, Provider & Consumer tenant with no finalizers - ready", + initialResources: []string{ + "testdata/capapplication/ca-35.initial.yaml", + "testdata/capapplication/cat-provider-no-finalizers-ready.yaml", + "testdata/capapplication/cat-consumer-no-finalizers-ready.yaml", + "testdata/capapplication/istio-ingress-with-certManager.yaml", + }, + expectedResources: "testdata/capapplication/ca-35.expected.yaml", + expectedRequeue: nil, //When no finalizer is set --> do not expect requeue + }, + ) + + os.Setenv(certManagerEnv, "") + +} + +func TestCertManagerDeletion_Case3(t *testing.T) { + + os.Setenv(certManagerEnv, certManagerCertManagerIO) + + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplication, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01"}}, + TestData{ + description: "When deletionTimestamp - true, finalizer set - true, certificate - ready, certificate finalizer - false, Provider & Consumer tenant with no finalizers - ready", + initialResources: []string{ + "testdata/capapplication/ca-36.initial.yaml", + "testdata/capapplication/cat-provider-no-finalizers-ready.yaml", + "testdata/capapplication/cat-consumer-no-finalizers-ready.yaml", + "testdata/capapplication/istio-ingress-with-certManager-no-finalizers.yaml", + }, + expectedResources: "testdata/capapplication/ca-36.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ResourceCAPApplication: {{Namespace: "default", Name: "test-cap-01"}}}, + }, + ) + + os.Setenv(certManagerEnv, "") + +} + +func TestCertManagerDeletion_Case4(t *testing.T) { + + os.Setenv(certManagerEnv, certManagerCertManagerIO) + + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplication, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01"}}, + TestData{ + description: "When deletionTimestamp - true, finalizer set - true, certificate - ready, certificate finalizer - false, Provider & Consumer tenant with no finalizers - NA", + initialResources: []string{ + "testdata/capapplication/ca-37.initial.yaml", + "testdata/capapplication/istio-ingress-with-certManager-no-finalizers.yaml", + }, + expectedResources: "testdata/capapplication/ca-37.expected.yaml", + }, + ) + + os.Setenv(certManagerEnv, "") + +} + +func TestCertManagerDeletion_Case5(t *testing.T) { + + os.Setenv(certManagerEnv, certManagerCertManagerIO) + + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplication, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01"}}, + TestData{ + description: "When deletionTimestamp - true, finalizer set - true, certificate - ready, certificate finalizer - false, Provider & Consumer tenant with no finalizers - ready", + initialResources: []string{ + "testdata/capapplication/ca-38.initial.yaml", + "testdata/capapplication/cat-provider-no-finalizers-ready.yaml", + "testdata/capapplication/cat-consumer-no-finalizers-ready.yaml", + "testdata/capapplication/istio-ingress-with-certManager-no-finalizers.yaml", + }, + expectedResources: "testdata/capapplication/ca-38.expected.yaml", + }, + ) + + os.Setenv(certManagerEnv, "") + +} + +func TestCertManagerDeletion_Case6(t *testing.T) { + + os.Setenv(certManagerEnv, certManagerCertManagerIO) + + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplication, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01"}}, + TestData{ + description: "When deletionTimestamp - true, finalizer set - true, certificate - NA, certificate finalizer - false, Provider & Consumer tenant with no finalizers - ready", + initialResources: []string{ + "testdata/capapplication/ca-39.initial.yaml", + "testdata/capapplication/cat-provider-no-finalizers-ready.yaml", + "testdata/capapplication/cat-consumer-no-finalizers-ready.yaml", + "testdata/common/istio-ingress.yaml", + }, + expectedResources: "testdata/capapplication/ca-39.expected.yaml", + }, + ) + + os.Setenv(certManagerEnv, "") + +} + +func TestCertManagerDeletion_Case7(t *testing.T) { + + os.Setenv(certManagerEnv, certManagerCertManagerIO) + + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplication, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01"}}, + TestData{ + description: "When deletionTimestamp - true, finalizer set - true, certificate - ready, certificate finalizer - true, Provider & Consumer tenant with no finalizers - NA", + initialResources: []string{ + "testdata/capapplication/ca-40.initial.yaml", + "testdata/capapplication/istio-ingress-with-certManager.yaml", + }, + expectedResources: "testdata/capapplication/ca-40.expected.yaml", + }, + ) + + os.Setenv(certManagerEnv, "") + +} + +func TestCertManagerDeletion_Case8(t *testing.T) { + + os.Setenv(certManagerEnv, certManagerCertManagerIO) + + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplication, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01"}}, + TestData{ + description: "When deletionTimestamp - true, finalizer set - true, certificate - ready, certificate finalizer - true, Provider & Consumer tenant with no finalizers - ready", + initialResources: []string{ + "testdata/capapplication/ca-41.initial.yaml", + "testdata/capapplication/cat-provider-no-finalizers-ready.yaml", + "testdata/capapplication/cat-consumer-no-finalizers-ready.yaml", + "testdata/capapplication/istio-ingress-with-certManager.yaml", + }, + expectedResources: "testdata/capapplication/ca-41.expected.yaml", + }, + ) + + os.Setenv(certManagerEnv, "") + +} + +func TestController_handleCAPApplicationConsistent_Case1(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplication, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01"}}, + TestData{ + description: "Consistent state", + initialResources: []string{ + "testdata/capapplication/ca-29.initial.yaml", + "testdata/common/capapplicationversion-v1.yaml", + "testdata/capapplication/cat-provider-no-finalizers-ready.yaml", + "testdata/capapplication/cat-consumer-no-finalizers-ready.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplication/gateway.yaml", + "testdata/capapplication/istio-ingress-with-cert.yaml", + "testdata/capapplication/ca-dns.yaml", + }, + expectedResources: "testdata/capapplication/ca-29.expected.yaml", + backlogItems: []string{ + "ERP4SMEPREPWORKAPPPLAT-2881", + }, + }, + ) +} + +func TestController_handleCAPApplicationConsistent_Case2(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplication, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01"}}, + TestData{ + description: "Consistent state with Generation mismatch", + initialResources: []string{ + "testdata/capapplication/ca-30.initial.yaml", + "testdata/common/capapplicationversion-v1.yaml", + "testdata/capapplication/cat-provider-no-finalizers-ready.yaml", + "testdata/capapplication/cat-consumer-no-finalizers-ready.yaml", + }, + expectedResources: "testdata/capapplication/ca-30.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ResourceCAPApplication: {{Namespace: "default", Name: "test-cap-01"}}}, + }, + ) +} + +func TestController_handleCAPApplicationConsistent_Case3(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplication, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01"}}, + TestData{ + description: "Consistent state with a CAV name update; one tenant with fixed version and never upgrade", + initialResources: []string{ + "testdata/capapplication/ca-31.initial.yaml", + "testdata/capapplication/cav-name-modified-ready.yaml", + "testdata/capapplication/cat-provider-no-finalizers-ready.yaml", + "testdata/capapplication/cat-consumer-upg-never-ready.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplication/gateway.yaml", + "testdata/capapplication/istio-ingress-with-cert.yaml", + "testdata/capapplication/ca-dns.yaml", + }, + expectedResources: "testdata/capapplication/ca-31.expected.yaml", + backlogItems: []string{ + "ERP4SMEPREPWORKAPPPLAT-2881", + }, + }, + ) +} + +func TestController_handleCAPApplicationConsistent_Case4(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplication, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01"}}, + TestData{ + description: "Consistent state with a CAV name update", + initialResources: []string{ + "testdata/capapplication/ca-32.initial.yaml", + "testdata/capapplication/cav-name-modified-ready.yaml", + "testdata/capapplication/cat-provider-no-finalizers-ready.yaml", + "testdata/capapplication/cat-consumer-no-finalizers-ready.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplication/gateway.yaml", + "testdata/capapplication/istio-ingress-with-cert.yaml", + "testdata/capapplication/ca-dns.yaml", + }, + expectedResources: "testdata/capapplication/ca-32.expected.yaml", + backlogItems: []string{ + "ERP4SMEPREPWORKAPPPLAT-2881", + }, + }, + ) +} + +func TestController_handleCAPApplicationConsistent_Case5(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplication, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01"}}, + TestData{ + description: "Consistent state with a CAV name update", + initialResources: []string{ + "testdata/capapplication/ca-33.initial.yaml", + "testdata/capapplication/cav-33-version-updated-ready.yaml", + "testdata/capapplication/cat-provider-no-finalizers-ready.yaml", + "testdata/capapplication/cat-consumer-no-finalizers-ready.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplication/gateway.yaml", + "testdata/capapplication/istio-ingress-with-cert.yaml", + "testdata/capapplication/ca-dns.yaml", + }, + expectedResources: "testdata/capapplication/ca-33.expected.yaml", + backlogItems: []string{ + "ERP4SMEPREPWORKAPPPLAT-2881", + }, + }, + ) +} + +func TestController_handleCAPApplicationConsistent_Case6(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplication, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01"}}, + TestData{ + description: "Consistent state with no certificate", + initialResources: []string{ + "testdata/capapplication/ca-29.initial.yaml", + "testdata/common/capapplicationversion-v1.yaml", + "testdata/capapplication/cat-provider-no-finalizers-ready.yaml", + "testdata/capapplication/cat-consumer-no-finalizers-ready.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplication/gateway.yaml", + "testdata/capapplication/istio-ingress-with-no-cert.yaml", + "testdata/capapplication/ca-dns.yaml", + }, + expectedResources: "testdata/capapplication/ca-29.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ResourceCAPApplication: {{Namespace: "default", Name: "test-cap-01"}}}, + backlogItems: []string{ + "ERP4SMEPREPWORKAPPPLAT-2976", + "ERP4SMEPREPWORKAPPPLAT-2881", + }, + }, + ) +} + +func TestProviderTenantCreationError(t *testing.T) { + err := reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplication, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01"}}, + TestData{ + description: "When CAPApplicationVersion - ready, CAPTenant - n/a, gateway - available, certificate - ready, dnsEntry - ready", + initialResources: []string{ + "testdata/capapplication/ca-43.initial.yaml", + "testdata/capapplication/ca-dns.yaml", + "testdata/common/capapplicationversion-v1.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplication/gateway.yaml", + "testdata/capapplication/istio-ingress-with-cert.yaml", + }, + expectedResources: "testdata/capapplication/ca-43.expected.yaml", + expectError: true, + mockErrorForResources: []ResourceAction{{Verb: "create", Group: "sme.sap.com", Version: "v1alpha1", Resource: "captenants", Namespace: "*", Name: "*"}}, + }, + ) + + if err.Error() != "mocked api error (captenants.sme.sap.com/v1alpha1)" { + t.Error("Wrong error message") + } +} + +func TestCAPApplicationPrimaryDomainDNSEntryNotReady(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplication, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01"}}, + TestData{ + description: "When CAPApplicationVersion - ready, CAPTenant - ready, gateway - available, certificate - ready, dnsEntry - not ready", + initialResources: []string{ + "testdata/capapplication/ca-06.initial.yaml", + "testdata/capapplication/ca-dns-not-ready.yaml", + "testdata/common/capapplicationversion-v1.yaml", + "testdata/common/captenant-provider-ready.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplication/gateway.yaml", + "testdata/capapplication/istio-ingress-with-cert.yaml", + }, + expectedResources: "testdata/capapplication/ca-44.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ResourceCAPApplication: {{Namespace: "default", Name: "test-cap-01"}}}, + }, + ) +} + +func TestCAPApplicationConsistentWithNewCAPApplicationVersionTenantUpdateError(t *testing.T) { + err := reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplication, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01"}}, + TestData{ + description: "Consistent state with a new CAV (ready); tenant update failure", + initialResources: []string{ + "testdata/capapplication/ca-31.initial.yaml", + "testdata/capapplication/cav-name-modified-ready.yaml", + "testdata/capapplication/cat-provider-no-finalizers-ready.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplication/gateway.yaml", + "testdata/capapplication/istio-ingress-with-cert.yaml", + "testdata/capapplication/ca-dns.yaml", + }, + expectError: true, + mockErrorForResources: []ResourceAction{{Verb: "update", Group: "sme.sap.com", Version: "v1alpha1", Resource: "captenants", Namespace: "*", Name: "*"}}, + }, + ) + if err.Error() != "could not update CAPTenant default.test-cap-01-provider: mocked api error (captenants.sme.sap.com/v1alpha1)" { + t.Error("error message is different from expected") + } +} + +func TestAdditionalConditionsTenantReadyUpgradeStrategyNever(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplication, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01"}}, + TestData{ + description: "test additional conditions with tenant having upgrade strategy never - and not on latest version", + initialResources: []string{ + "testdata/capapplication/ca-45.initial.yaml", + "testdata/common/capapplicationversion-v1.yaml", + "testdata/common/capapplicationversion-v2.yaml", + "testdata/common/captenant-provider-upgraded-ready.yaml", + "testdata/capapplication/cat-consumer-upg-never-ready.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplication/gateway.yaml", + "testdata/capapplication/istio-ingress-with-cert.yaml", + "testdata/capapplication/ca-dns.yaml", + }, + expectedResources: "testdata/capapplication/ca-29.expected.yaml", // expect - AllTenantsReady is "True" + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2881"}, + }, + ) +} + +func TestAdditionalConditionsWithTenantDeletingUpgradeStrategyNever(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplication, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01"}}, + TestData{ + description: "test additional conditions with tenant having upgrade strategy never, not on latest version and deleting", + initialResources: []string{ + "testdata/capapplication/ca-45.initial.yaml", + "testdata/common/capapplicationversion-v1.yaml", + "testdata/common/capapplicationversion-v2.yaml", + "testdata/common/captenant-provider-upgraded-ready.yaml", + "testdata/capapplication/cat-consumer-upg-never-deleting.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplication/gateway.yaml", + "testdata/capapplication/istio-ingress-with-cert.yaml", + "testdata/capapplication/ca-dns.yaml", + }, + expectedResources: "testdata/capapplication/ca-45.expected.yaml", // expect - AllTenantsReady is "False" + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2881"}, + }, + ) +} diff --git a/internal/controller/reconcile-capapplicationversion.go b/internal/controller/reconcile-capapplicationversion.go new file mode 100644 index 0000000..15dc0f2 --- /dev/null +++ b/internal/controller/reconcile-capapplicationversion.go @@ -0,0 +1,838 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +package controller + +import ( + "context" + "encoding/json" + "fmt" + "strconv" + "strings" + "time" + + "github.com/sap/cap-operator/internal/util" + "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + "golang.org/x/exp/slices" + appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" + k8sErrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/klog/v2" +) + +const ( + App = "app" +) + +const ( + CategoryWorkload = "Workload" + CategoryService = "Service" +) + +const ( + defaultServerPort = 4004 + defaultRouterPort = 5000 +) + +var trueVal = true + +type DeploymentParameters struct { + CA *v1alpha1.CAPApplication + CAV *v1alpha1.CAPApplicationVersion + OwnerRef *metav1.OwnerReference + WorkloadDetails v1alpha1.WorkloadDetails + VCAPSecretName string +} + +func (c *Controller) reconcileCAPApplicationVersion(ctx context.Context, item QueueItem, attempts int) (*ReconcileResult, error) { + lister := c.crdInformerFactory.Sme().V1alpha1().CAPApplicationVersions().Lister() + cached, err := lister.CAPApplicationVersions(item.ResourceKey.Namespace).Get(item.ResourceKey.Name) + if err != nil { + return nil, handleOperatorResourceErrors(err) + } + cav := cached.DeepCopy() + + // prepare owner refs, labels, finalizers + if update, err := c.prepareCAPApplicationVersion(ctx, cav); err != nil { + return nil, err + } else if update { + err := c.updateCAPApplicationVersion(ctx, cav) + if err != nil { + return nil, err + } + } + + // Handle Deletion + if cav.DeletionTimestamp != nil { + return c.deleteCAPApplicationVersion(ctx, cav) + } + + return c.handleCAPApplicationVersion(ctx, cav) +} + +func (c *Controller) updateCAPApplicationVersionStatus(ctx context.Context, cav *v1alpha1.CAPApplicationVersion, state v1alpha1.CAPApplicationVersionState, condition metav1.Condition) error { + cav.SetStatusWithReadyCondition(state, condition.Status, condition.Reason, condition.Message) + + cavUpdated, statusErr := c.crdClient.SmeV1alpha1().CAPApplicationVersions(cav.Namespace).UpdateStatus(ctx, cav, metav1.UpdateOptions{}) + // Update reference to the resource + if cavUpdated != nil { + *cav = *cavUpdated + } + if statusErr != nil { + klog.Error("could not update status of %s %s.%s: ", v1alpha1.CAPApplicationVersionKind, cav.Namespace, cav.Name, statusErr.Error()) + } + + return statusErr +} + +func (c *Controller) updateCAPApplicationVersion(ctx context.Context, cav *v1alpha1.CAPApplicationVersion) error { + cavUpdated, err := c.crdClient.SmeV1alpha1().CAPApplicationVersions(cav.Namespace).Update(ctx, cav, metav1.UpdateOptions{}) + // Update reference to the resource + if cavUpdated != nil { + *cav = *cavUpdated + } + return err +} + +func (c *Controller) handleCAPApplicationVersion(ctx context.Context, cav *v1alpha1.CAPApplicationVersion) (*ReconcileResult, error) { + ca, _ := c.getCachedCAPApplication(cav.Namespace, cav.Spec.CAPApplicationInstance) + + // Check for valid secrets + err := c.checkSecretsExist(ca.Spec.BTP.Services, ca.Namespace) + + if err != nil { + // Requeue after 10s to check if secrets exist + return NewReconcileResultWithResource(ResourceCAPApplicationVersion, cav.Name, cav.Namespace, 10*time.Second), c.updateCAPApplicationVersionStatus(ctx, cav, v1alpha1.CAPApplicationVersionStateProcessing, metav1.Condition{Type: string(v1alpha1.ConditionTypeReady), Status: "False", Reason: "WaitingForSecrets"}) + } + + // If Valid secrets exists proceed with processing deployment + var statusErr error + switch cav.Status.State { + case "": + statusErr = c.updateCAPApplicationVersionStatus(ctx, cav, v1alpha1.CAPApplicationVersionStateProcessing, metav1.Condition{Type: string(v1alpha1.ConditionTypeReady), Status: "False", Reason: "ReadyForProcessing"}) + case v1alpha1.CAPApplicationVersionStateError: + var errorCondition metav1.Condition + if len(cav.Status.Conditions) > 0 { + errorCondition = *cav.Status.Conditions[0].DeepCopy() // keep the error condition while re-processing + } else { + errorCondition = metav1.Condition{Type: string(v1alpha1.ConditionTypeReady), Status: "False", Reason: "RetryProcessing"} + } + statusErr = c.updateCAPApplicationVersionStatus(ctx, cav, v1alpha1.CAPApplicationVersionStateProcessing, errorCondition) + } + + if statusErr != nil { + return nil, statusErr + } + + return nil, c.processDeployments(ctx, ca, cav) +} + +func (c *Controller) processDeployments(ctx context.Context, ca *v1alpha1.CAPApplication, cav *v1alpha1.CAPApplicationVersion) error { + // TODO: handle create/update of individual deployments/jobs (so far these are just created and never updated, as we expect secrets don't change!) + + // Handle Content job + err := c.handleContentDeployJob(ca, cav) + if err != nil { + c.updateCAPApplicationVersionStatus(ctx, cav, v1alpha1.CAPApplicationVersionStateError, metav1.Condition{Type: string(v1alpha1.ConditionTypeReady), Status: "False", Reason: "ErrorInContentDeploymentJob", Message: err.Error()}) + return err + } + + // Create AppRouter Deployment + err = c.updateApprouterDeployment(ca, cav) + if err != nil { + c.updateCAPApplicationVersionStatus(ctx, cav, v1alpha1.CAPApplicationVersionStateError, metav1.Condition{Type: string(v1alpha1.ConditionTypeReady), Status: "False", Reason: "ErrorInAppRouterDeployment", Message: err.Error()}) + return err + } + + // Create Server Deployment + err = c.updateServerDeployment(ca, cav) + if err != nil { + c.updateCAPApplicationVersionStatus(ctx, cav, v1alpha1.CAPApplicationVersionStateError, metav1.Condition{Type: string(v1alpha1.ConditionTypeReady), Status: "False", Reason: "ErrorInServerDeployment", Message: err.Error()}) + return err + } + + // Create All Services + err = c.updateServices(ca, cav) + if err != nil { + c.updateCAPApplicationVersionStatus(ctx, cav, v1alpha1.CAPApplicationVersionStateError, metav1.Condition{Type: string(v1alpha1.ConditionTypeReady), Status: "False", Reason: "ErrorInServerService", Message: err.Error()}) + return err + } + + // Create Additional Deployments + err = c.updateAdditionalDeployment(ca, cav) + if err != nil { + c.updateCAPApplicationVersionStatus(ctx, cav, v1alpha1.CAPApplicationVersionStateError, metav1.Condition{Type: string(v1alpha1.ConditionTypeReady), Status: "False", Reason: "ErrorInJobWorkerDeployment", Message: err.Error()}) + return err + } + + // Create NetworkPolicy + err = c.updateNetworkPolicies(ca, cav) + if err != nil { + c.updateCAPApplicationVersionStatus(ctx, cav, v1alpha1.CAPApplicationVersionStateError, metav1.Condition{Type: string(v1alpha1.ConditionTypeReady), Status: "False", Reason: "ErrorInNetworkPolicy", Message: err.Error()}) + return err + } + + // Check for status of the Workloads + processing, err := c.checkWorkloadStatus(ctx, cav) + if err != nil { + c.updateCAPApplicationVersionStatus(ctx, cav, v1alpha1.CAPApplicationVersionStateError, metav1.Condition{Type: string(v1alpha1.ConditionTypeReady), Status: "False", Reason: "ErrorInWorkloadStatus", Message: err.Error()}) + return err + } else if processing { + return nil + } + + // TODO: wait until the deployments are actually "Ready"! + return c.updateCAPApplicationVersionStatus(ctx, cav, v1alpha1.CAPApplicationVersionStateReady, metav1.Condition{Type: string(v1alpha1.ConditionTypeReady), Status: "True", Reason: "CreatedDeployments"}) +} + +// #region Content Deploy Job +func (c *Controller) handleContentDeployJob(ca *v1alpha1.CAPApplication, cav *v1alpha1.CAPApplicationVersion) error { + workload := getRelevantJob(v1alpha1.JobContent, cav) + if res := validateEnv(workload.JobDefinition.Env, restrictedEnvNames); res != "" { + return errorEnv(workload.Name, res) + } + + var vcapSecretName string + jobName := cav.Name + "-" + strings.ToLower(string(workload.JobDefinition.Type)) + // If Job has already executed --> exit + if cav.CheckFinishedJobs(jobName) { + return nil + } + // Get the contentDeploy job with the name expected for this CAV instance + contentDeployJob, err := c.kubeInformerFactory.Batch().V1().Jobs().Lister().Jobs(cav.Namespace).Get(jobName) + // If the resource doesn't exist, we'll create it + if k8sErrors.IsNotFound(err) { + // Get ServiceInfos for consumed BTP services + consumedServiceInfos := getConsumedServiceInfos(getConsumedServiceMap(workload.ConsumedBTPServices), ca.Spec.BTP.Services) + + // Create ownerRef to CAV + ownerRef := *metav1.NewControllerRef(cav, v1alpha1.SchemeGroupVersion.WithKind(v1alpha1.CAPApplicationVersionKind)) + + // Get VCAP secret name + vcapSecretName, err = createVCAPSecret(jobName, cav.Namespace, ownerRef, consumedServiceInfos, c.kubeClient) + + if err == nil { + contentDeployJob, err = c.kubeClient.BatchV1().Jobs(cav.Namespace).Create(context.TODO(), newContentDeploymentJob(ca, cav, workload, ownerRef, vcapSecretName), metav1.CreateOptions{}) + } + } + + return doChecks(err, contentDeployJob, cav, workload.Name) +} + +// newContentDeploymentJob creates a Content Deployment Job for the CAV resource. It also sets the appropriate OwnerReferences. +func newContentDeploymentJob(ca *v1alpha1.CAPApplication, cav *v1alpha1.CAPApplicationVersion, workload *v1alpha1.WorkloadDetails, ownerRef metav1.OwnerReference, vcapSecretName string) *batchv1.Job { + labels := copyMaps(workload.Labels, map[string]string{ + LabelDisableKarydia: "true", + }) + + annotations := copyMaps(workload.Annotations, map[string]string{ + AnnotationIstioSidecarInject: "false", + }) + + return &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: cav.Name + "-" + strings.ToLower(string(workload.JobDefinition.Type)), + Namespace: cav.Namespace, + Annotations: workload.Annotations, + OwnerReferences: []metav1.OwnerReference{ + ownerRef, + }, + }, + Spec: batchv1.JobSpec{ + BackoffLimit: workload.JobDefinition.BackoffLimit, + TTLSecondsAfterFinished: workload.JobDefinition.TTLSecondsAfterFinished, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: annotations, + Labels: labels, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: workload.Name, + Image: workload.JobDefinition.Image, + ImagePullPolicy: workload.JobDefinition.ImagePullPolicy, + Command: workload.JobDefinition.Command, + Env: append([]corev1.EnvVar{ + {Name: EnvCAPOpAppVersion, Value: cav.Spec.Version}, + }, workload.JobDefinition.Env...), + EnvFrom: getEnvFrom(vcapSecretName), + Resources: workload.JobDefinition.Resources, + SecurityContext: workload.JobDefinition.SecurityContext, + }, + }, + SecurityContext: workload.JobDefinition.PodSecurityContext, + ImagePullSecrets: convertToLocalObjectReferences(cav.Spec.RegistrySecrets), + RestartPolicy: corev1.RestartPolicyOnFailure, + }, + }, + }, + } +} + +//#endregion + +// #region Server +func (c *Controller) updateServerDeployment(ca *v1alpha1.CAPApplication, cav *v1alpha1.CAPApplicationVersion) error { + serverWorkloads := getDeployments(v1alpha1.DeploymentCAP, cav) + for _, serverWorkload := range serverWorkloads { + err := c.updateDeployment(ca, cav, &serverWorkload) + if err != nil { + return err + } + } + return nil +} + +//#endregion + +// #region AppRouter +func (c *Controller) updateApprouterDeployment(ca *v1alpha1.CAPApplication, cav *v1alpha1.CAPApplicationVersion) error { + routerWorkload := getRelevantDeployment(v1alpha1.DeploymentRouter, cav) + return c.updateDeployment(ca, cav, routerWorkload) +} + +//#endregion + +// #region JobWorker +func (c *Controller) updateAdditionalDeployment(ca *v1alpha1.CAPApplication, cav *v1alpha1.CAPApplicationVersion) error { + additionalWorkloads := getDeployments(v1alpha1.DeploymentAdditional, cav) + for _, workload := range additionalWorkloads { + err := c.updateDeployment(ca, cav, &workload) + if err != nil { + return err + } + } + return nil +} + +//#endregion + +// #region Service +func (c *Controller) updateServices(ca *v1alpha1.CAPApplication, cav *v1alpha1.CAPApplicationVersion) error { + workloadServicePortInfos := getRelevantServicePortInfo(cav) + for _, workloadServicePortInfo := range workloadServicePortInfos { + // Get the Service with the name specified in CustomDeployment.spec + service, err := c.kubeClient.CoreV1().Services(cav.Namespace).Get(context.TODO(), workloadServicePortInfo.WorkloadName+ServiceSuffix, metav1.GetOptions{}) + // If the resource doesn't exist, we'll create it + if k8sErrors.IsNotFound(err) { + service, err = c.kubeClient.CoreV1().Services(cav.Namespace).Create(context.TODO(), newService(ca, cav, workloadServicePortInfo), metav1.CreateOptions{}) + } + + err = doChecks(err, service, cav, workloadServicePortInfo.WorkloadName+ServiceSuffix) + if err != nil { + return err + } + } + return nil +} + +// newService creates a new Service for a CAV resource. It also sets the appropriate OwnerReferences. +func newService(ca *v1alpha1.CAPApplication, cav *v1alpha1.CAPApplicationVersion, workloadServicePortInfo servicePortInfo) *corev1.Service { + var ports []corev1.ServicePort + + for _, port := range workloadServicePortInfo.Ports { + ports = append(ports, corev1.ServicePort{Name: port.Name, Port: port.Port, AppProtocol: port.AppProtocol}) + } + + matchlabels := getLabels(ca, cav, CategoryWorkload, workloadServicePortInfo.DeploymentType, workloadServicePortInfo.WorkloadName, false) + + workload := getWorkloadByName(workloadServicePortInfo.WorkloadName[len(cav.Name)+1:], cav) + + annotations := copyMaps(workload.Annotations, getAnnotations(ca, cav, true)) + + labels := copyMaps(workload.Labels, getLabels(ca, cav, CategoryService, workloadServicePortInfo.DeploymentType, workloadServicePortInfo.WorkloadName+ServiceSuffix, true)) + + return &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: workloadServicePortInfo.WorkloadName + ServiceSuffix, + Namespace: cav.Namespace, + Labels: labels, + Annotations: annotations, + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(cav, v1alpha1.SchemeGroupVersion.WithKind(v1alpha1.CAPApplicationVersionKind)), + }, + }, + Spec: corev1.ServiceSpec{ + Ports: ports, + Selector: matchlabels, + }, + } +} + +// #endregion Service + +// #region NetworkPolicy +func (c *Controller) updateNetworkPolicies(ca *v1alpha1.CAPApplication, cav *v1alpha1.CAPApplicationVersion) error { + var ( + spec networkingv1.NetworkPolicySpec + err error + ) + + // The app pod specific NetworkPolicy + spec = getAppPodNetworkPolicySpec(ca, cav) + err = c.createNetworkPolicy(cav.Name, spec, cav) + if err != nil { + return err + } + + // The app ingress (to router) NetworkPolicy + spec = getAppIngressNetworkPolicySpec(ca, cav) + err = c.createNetworkPolicy(cav.Name+"--in", spec, cav) + if err != nil { + return err + } + + // (Tech)Port specific network policy (just clusterWide for now) + // Get all the relevant service info (that includes ports exposed clusterwide) + workloadServicePortInfos := getRelevantServicePortInfo(cav) + for _, workloadServicePortInfo := range workloadServicePortInfos { + if len(workloadServicePortInfo.ClusterPorts) > 0 { + // Create a network policy for the workload if at least 1 clusterwide exposed port exists. + spec = getPortSpecificNetworkPolicySpec(workloadServicePortInfo, ca, cav) + err = c.createNetworkPolicy(workloadServicePortInfo.WorkloadName, spec, cav) + if err != nil { + return err + } + } + } + return nil +} + +// check and create a new NetworkPolicy for the given workload/CAV resource. It also sets the appropriate OwnerReferences. +func (c *Controller) createNetworkPolicy(name string, spec networkingv1.NetworkPolicySpec, cav *v1alpha1.CAPApplicationVersion) error { + networkPolicy, err := c.kubeClient.NetworkingV1().NetworkPolicies(cav.Namespace).Get(context.TODO(), name, metav1.GetOptions{}) + // If the resource doesn't exist, we'll create it + if k8sErrors.IsNotFound(err) { + networkPolicy, err = c.kubeClient.NetworkingV1().NetworkPolicies(cav.Namespace).Create(context.TODO(), &networkingv1.NetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: cav.Namespace, + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(cav, v1alpha1.SchemeGroupVersion.WithKind(v1alpha1.CAPApplicationVersionKind)), + }, + }, + Spec: spec, + }, metav1.CreateOptions{}) + } + return doChecks(err, networkPolicy, cav, "NetworkPolicy") +} + +func getAppPodNetworkPolicySpec(ca *v1alpha1.CAPApplication, cav *v1alpha1.CAPApplicationVersion) networkingv1.NetworkPolicySpec { + return networkingv1.NetworkPolicySpec{ + PolicyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeIngress}, + Ingress: []networkingv1.NetworkPolicyIngressRule{{ + From: []networkingv1.NetworkPolicyPeer{ + // Enable communication across all workload pods with the same version + { + PodSelector: &metav1.LabelSelector{MatchLabels: getLabels(ca, cav, CategoryWorkload, "", "", false)}, + }, + }, + }}, + // Target all workloads of the app + PodSelector: metav1.LabelSelector{MatchLabels: getLabels(ca, cav, CategoryWorkload, "", "", false)}, + } +} + +func getAppIngressNetworkPolicySpec(ca *v1alpha1.CAPApplication, cav *v1alpha1.CAPApplicationVersion) networkingv1.NetworkPolicySpec { + return networkingv1.NetworkPolicySpec{ + PolicyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeIngress}, + Ingress: []networkingv1.NetworkPolicyIngressRule{{ + From: []networkingv1.NetworkPolicyPeer{ + // Enable ingress traffic to the router via istio-ingress gateway + { + NamespaceSelector: &metav1.LabelSelector{}, + PodSelector: &metav1.LabelSelector{MatchLabels: getIngressGatewayLabels(ca)}, + }, + }, + }}, + // Target all workloads of the app + PodSelector: metav1.LabelSelector{MatchLabels: getLabels(ca, cav, CategoryWorkload, string(v1alpha1.DeploymentRouter), "", false)}, + } +} + +func getPortSpecificNetworkPolicySpec(workloadServicePortInfo servicePortInfo, ca *v1alpha1.CAPApplication, cav *v1alpha1.CAPApplicationVersion) networkingv1.NetworkPolicySpec { + ports := []networkingv1.NetworkPolicyPort{} + for _, port := range workloadServicePortInfo.ClusterPorts { + ports = append(ports, networkingv1.NetworkPolicyPort{Port: &intstr.IntOrString{IntVal: port}}) + } + return networkingv1.NetworkPolicySpec{ + PolicyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeIngress}, + Ingress: []networkingv1.NetworkPolicyIngressRule{{ + Ports: ports, + From: []networkingv1.NetworkPolicyPeer{ + // Enable ingress traffic to these ports from any pod in the cluster + { + NamespaceSelector: &metav1.LabelSelector{}, + PodSelector: &metav1.LabelSelector{}, + }, + }, + }}, + // Target the relevant workload whose port(s) needs to be exposed cluster wide + PodSelector: metav1.LabelSelector{MatchLabels: getLabels(ca, cav, CategoryWorkload, workloadServicePortInfo.DeploymentType, workloadServicePortInfo.WorkloadName, false)}, + } +} + +// #endregion NetworkPolicy + +// #region Deployments + +func (c *Controller) updateDeployment(ca *v1alpha1.CAPApplication, cav *v1alpha1.CAPApplicationVersion, workload *v1alpha1.WorkloadDetails) error { + if res := validateEnv(workload.DeploymentDefinition.Env, restrictedEnvNames); res != "" { + return errorEnv(workload.Name, res) + } + + var vcapSecretName string + deploymentName := cav.Name + "-" + strings.ToLower(string(workload.Name)) + // Get the workloadDeployment with the name specified in CustomDeployment.spec + workloadDeployment, err := c.kubeClient.AppsV1().Deployments(cav.Namespace).Get(context.TODO(), deploymentName, metav1.GetOptions{}) + // If the resource doesn't exist, we'll create it + if k8sErrors.IsNotFound(err) { + // Get ServiceInfos for consumed BTP services + consumedServiceInfos := getConsumedServiceInfos(getConsumedServiceMap(workload.ConsumedBTPServices), ca.Spec.BTP.Services) + + // Create ownerRef to CAV + ownerRef := *metav1.NewControllerRef(cav, v1alpha1.SchemeGroupVersion.WithKind(v1alpha1.CAPApplicationVersionKind)) + + // Get VCAP secret name + vcapSecretName, err = createVCAPSecret(deploymentName, cav.Namespace, ownerRef, consumedServiceInfos, c.kubeClient) + + if err == nil { + workloadDeployment, err = c.kubeClient.AppsV1().Deployments(cav.Namespace).Create(context.TODO(), newDeployment(ca, cav, workload, ownerRef, vcapSecretName), metav1.CreateOptions{}) + } + } + + return doChecks(err, workloadDeployment, cav, workload.Name) +} + +// newDeployment creates a new generic Deployment for a CAV resource based on the type. It also sets the appropriate OwnerReferences. +func newDeployment(ca *v1alpha1.CAPApplication, cav *v1alpha1.CAPApplicationVersion, workload *v1alpha1.WorkloadDetails, ownerRef metav1.OwnerReference, vcapSecretName string) *appsv1.Deployment { + params := &DeploymentParameters{ + CA: ca, + CAV: cav, + OwnerRef: &ownerRef, + WorkloadDetails: *workload, + VCAPSecretName: vcapSecretName, + } + + return createDeployment(params) +} + +func createDeployment(params *DeploymentParameters) *appsv1.Deployment { + workloadName := params.CAV.Name + "-" + strings.ToLower(params.WorkloadDetails.Name) + annotations := copyMaps(params.WorkloadDetails.Annotations, getAnnotations(params.CA, params.CAV, true)) + labels := copyMaps(params.WorkloadDetails.Labels, getLabels(params.CA, params.CAV, CategoryWorkload, string(params.WorkloadDetails.DeploymentDefinition.Type), workloadName, true)) + + return &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: workloadName, + Namespace: params.CAV.Namespace, + OwnerReferences: []metav1.OwnerReference{ + *params.OwnerRef, + }, + Annotations: annotations, + Labels: labels, + }, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: labels, + }, + Replicas: params.WorkloadDetails.DeploymentDefinition.Replicas, // will automatically default to 1 (if pointer is nil) + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: annotations, + Labels: labels, + }, + Spec: corev1.PodSpec{ + ImagePullSecrets: convertToLocalObjectReferences(params.CAV.Spec.RegistrySecrets), + Containers: getContainer(params), + SecurityContext: params.WorkloadDetails.DeploymentDefinition.PodSecurityContext, + }, + }, + }, + } +} + +func getContainer(params *DeploymentParameters) []corev1.Container { + container := corev1.Container{ + Name: params.WorkloadDetails.Name, + Image: params.WorkloadDetails.DeploymentDefinition.Image, + ImagePullPolicy: params.WorkloadDetails.DeploymentDefinition.ImagePullPolicy, + Command: params.WorkloadDetails.DeploymentDefinition.Command, + Env: getEnv(params), + EnvFrom: getEnvFrom(params.VCAPSecretName), + LivenessProbe: params.WorkloadDetails.DeploymentDefinition.LivenessProbe, + ReadinessProbe: params.WorkloadDetails.DeploymentDefinition.ReadinessProbe, + Resources: params.WorkloadDetails.DeploymentDefinition.Resources, + SecurityContext: params.WorkloadDetails.DeploymentDefinition.SecurityContext, + } + return []corev1.Container{container} +} + +func getEnv(params *DeploymentParameters) []corev1.EnvVar { + env := []corev1.EnvVar{ + {Name: EnvCAPOpAppVersion, Value: params.CAV.Spec.Version}, + } + env = append(env, params.WorkloadDetails.DeploymentDefinition.Env...) + + if params.WorkloadDetails.DeploymentDefinition.Type == v1alpha1.DeploymentRouter { + // Add destinations env for `Router` + appendDestinationsEnv(params.CAV, &env) + } + + return env +} + +func appendDestinationsEnv(cav *v1alpha1.CAPApplicationVersion, env *[]corev1.EnvVar) { + var ( + destEnvIndex int = -1 + destMap map[string]RouterDestination = map[string]RouterDestination{} + ) + + destEnvIndex = slices.IndexFunc(*env, func(currentEnv corev1.EnvVar) bool { return currentEnv.Name == "destinations" }) + + if destEnvIndex > -1 { + if destinations, err := util.ParseJSON[[]RouterDestination]([]byte((*env)[destEnvIndex].Value)); err == nil { + for _, d := range *destinations { + destMap[d.Name] = d + } + } // else -> in case of parsing error continue with only the workload destinations + } + + destinations := []RouterDestination{} + portInfos := getRelevantServicePortInfo(cav) + for _, portInfo := range portInfos { + for _, destinationInfo := range portInfo.Destinations { + dest := getDestination(destMap, destinationInfo, portInfo) + destinations = append(destinations, dest) + delete(destMap, destinationInfo.DestinationName) + } + } + + for _, d := range destMap { + destinations = append(destinations, d) + } + serialized, _ := json.Marshal(destinations) + if destEnvIndex > -1 { + (*env)[destEnvIndex].Value = string(serialized) + } else { + *env = append(*env, corev1.EnvVar{Name: "destinations", Value: string(serialized)}) + } +} + +func getDestination(destMap map[string]RouterDestination, destinationInfo destinationInfo, portInfo servicePortInfo) RouterDestination { + dest, ok := destMap[destinationInfo.DestinationName] + if !ok { + dest = RouterDestination{ + Name: destinationInfo.DestinationName, + URL: "http://" + portInfo.WorkloadName + ServiceSuffix + ":" + strconv.Itoa(int(destinationInfo.Port)), + ForwardAuthToken: true, + } + } else { + // Overwrite just the URL from existing destination configuration + dest.URL = "http://" + portInfo.WorkloadName + ServiceSuffix + ":" + strconv.Itoa(int(destinationInfo.Port)) + } + return dest +} + +// endregion Deployments + +func getEnvFrom(vcapServiceName string) []corev1.EnvFromSource { + return []corev1.EnvFromSource{ + { + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: vcapServiceName, + }, + Optional: &trueVal, + }, + }, + } +} + +func (c *Controller) prepareCAPApplicationVersion(ctx context.Context, cav *v1alpha1.CAPApplicationVersion) (update bool, err error) { + // Do nothing when object is deleted + if cav.DeletionTimestamp != nil { + return false, nil + } + ca, err := c.getCachedCAPApplication(cav.Namespace, cav.Spec.CAPApplicationInstance) + if err != nil { + return false, err + } + if _, ok := getOwnerByKind(cav.OwnerReferences, v1alpha1.CAPApplicationKind); !ok { + // create owner reference - CAPApplication + cav.OwnerReferences = append(cav.OwnerReferences, *metav1.NewControllerRef(ca, v1alpha1.SchemeGroupVersion.WithKind(v1alpha1.CAPApplicationKind))) + update = true + } + + if addCAPApplicationVersionLabels(cav, ca) { + update = true + } + + if cav.DeletionTimestamp == nil { + // Finalizer to prevent direct deletion of CAPApplicationVersion + if cav.Finalizers == nil { + cav.Finalizers = []string{} + } + if addFinalizer(&cav.Finalizers, FinalizerCAPApplicationVersion) { + update = true + } + } + + return update, nil +} + +// Annotations +func getAnnotations(ca *v1alpha1.CAPApplication, cav *v1alpha1.CAPApplicationVersion, ownerInfo bool) map[string]string { + annotations := map[string]string{ + AnnotationBTPApplicationIdentifier: strings.Join([]string{ca.Spec.GlobalAccountId, ca.Spec.BTPAppName}, "."), + } + + if ownerInfo { + annotations[AnnotationOwnerIdentifier] = cav.Namespace + "." + cav.Name + + } + + return annotations +} + +// Labels +func getLabels(ca *v1alpha1.CAPApplication, cav *v1alpha1.CAPApplicationVersion, category string, workloadType string, workloadName string, additionalDetails bool) map[string]string { + labels := map[string]string{ + App: ca.Spec.BTPAppName, + LabelBTPApplicationIdentifierHash: sha1Sum(ca.Spec.GlobalAccountId, ca.Spec.BTPAppName), + LabelCAVVersion: cav.Spec.Version, + LabelResourceCategory: category, + } + + addIfNotEmpty := func(k, v string) { + if v != "" { + labels[k] = v + } + } + addIfNotEmpty(LabelWorkloadType, workloadType) + addIfNotEmpty(LabelWorkloadName, workloadName) + + if additionalDetails { + labels[LabelOwnerIdentifierHash] = sha1Sum(cav.Namespace, cav.Name) + labels[LabelOwnerGeneration] = strconv.FormatInt(cav.Generation, 10) + } + + return labels +} + +func addCAPApplicationVersionLabels(cav *v1alpha1.CAPApplicationVersion, ca *v1alpha1.CAPApplication) (updated bool) { + appMetadata := appMetadataIdentifiers{ + globalAccountId: ca.Spec.GlobalAccountId, + appName: ca.Spec.BTPAppName, + ownerInfo: &ownerInfo{ + ownerNamespace: ca.Namespace, + ownerName: ca.Name, + ownerGeneration: ca.Generation, + }, + } + if updateLabelAnnotationMetadata(&cav.ObjectMeta, &appMetadata) { + updated = true + } + return updated +} + +// Check if an error occured or if owner references are correct +func doChecks(err error, obj metav1.Object, cav *v1alpha1.CAPApplicationVersion, res string) error { + // If an error occurs during Get/Create, we'll requeue the item so we can + // attempt processing again later. This could have been caused by a + // temporary network failure, or any other transient reason. + if err != nil { + return err + } + + // Check if the Deployment is not controlled by this CustomDeployment resource + _, ok := getOwnerByKind(obj.GetOwnerReferences(), v1alpha1.CAPApplicationVersionKind) + if !ok { + return fmt.Errorf("%s could not be identified for the resource %s %s: %s.%s", v1alpha1.CAPApplicationVersionKind, res, obj.GetName(), cav.Namespace, cav.Name) + } + + return nil +} + +// checkWorkloadStatus --> This just checks the contentDeployJob status for now +// TODO: Checks for issues with Deployment(s)! +func (c *Controller) checkWorkloadStatus(ctx context.Context, cav *v1alpha1.CAPApplicationVersion) (bool, error) { + workload := getRelevantJob(v1alpha1.JobContent, cav) + job := cav.Name + "-" + strings.ToLower(string(workload.JobDefinition.Type)) + + // Get the contentDeploy job with the name expected for this CAV instance + contentDeployJob, err := c.kubeInformerFactory.Batch().V1().Jobs().Lister().Jobs(cav.Namespace).Get(job) + if err != nil { + return false, err + } + + // check for completion or failure and accordingly set the cav state by returning error msg + for _, condition := range contentDeployJob.Status.Conditions { + if condition.Type == batchv1.JobComplete && condition.Status == corev1.ConditionTrue { + cav.SetStatusFinishedJobs(job) + return false, nil + } else if condition.Type == batchv1.JobFailed && condition.Status == corev1.ConditionTrue { + cav.SetStatusFinishedJobs(job) + return false, fmt.Errorf("%s", condition.Message) + } + } + + // Assume Job is being processed! + return true, nil +} + +func (c *Controller) getRelevantTenantsForCAV(cav *v1alpha1.CAPApplicationVersion) []*v1alpha1.CAPTenant { + var tenants []*v1alpha1.CAPTenant + // Get CAPApplication instance + ca, _ := c.getCachedCAPApplication(cav.Namespace, cav.Spec.CAPApplicationInstance) + if ca != nil { + // Get all tenants in the namespace for the CAPApplication + allTenants, _ := c.crdInformerFactory.Sme().V1alpha1().CAPTenants().Lister().CAPTenants(cav.Namespace).List(labels.SelectorFromSet(map[string]string{LabelBTPApplicationIdentifierHash: sha1Sum(ca.Spec.GlobalAccountId, ca.Spec.BTPAppName)})) + // Filter out relevant tenants for the CAPApplicationVersion + for _, tenant := range allTenants { + // If a tenant is already on a given version -or- is being provisioned/upgraded to a version, it is relevant for this CAPApplicationVersion + if tenant.Status.CurrentCAPApplicationVersionInstance == cav.Name || tenant.Spec.Version == cav.Spec.Version { + tenants = append(tenants, tenant) + } + } + } + return tenants +} + +func (c *Controller) deleteCAPApplicationVersion(ctx context.Context, cav *v1alpha1.CAPApplicationVersion) (*ReconcileResult, error) { + // Update State if it is not set yet + if cav.Status.State != v1alpha1.CAPApplicationVersionStateDeleting { + var deleteCondition metav1.Condition + if len(cav.Status.Conditions) > 0 { + deleteCondition = *cav.Status.Conditions[0].DeepCopy() // Reuse the existing condition during deletion + } else { + deleteCondition = metav1.Condition{Type: string(v1alpha1.ConditionTypeReady), Status: "False"} + } + // Set the reason for Deletion + deleteCondition.Reason = "DeleteTriggered" + err := c.updateCAPApplicationVersionStatus(ctx, cav, v1alpha1.CAPApplicationVersionStateDeleting, deleteCondition) + if err != nil { + return nil, err + } + } + + tenants := c.getRelevantTenantsForCAV(cav) + + // Check if tenants exists + if len(tenants) > 0 { + // Requeue after 10s to check if all tenants are gone + return NewReconcileResultWithResource(ResourceCAPApplicationVersion, cav.Name, cav.Namespace, 10*time.Second), nil + } else if removeFinalizer(&cav.Finalizers, FinalizerCAPApplicationVersion) { // All tenants are gone --> remove finalizer and process deletion + return nil, c.updateCAPApplicationVersion(ctx, cav) + } + + // No finalizer exists + return nil, nil +} diff --git a/internal/controller/reconcile-capapplicationversion_test.go b/internal/controller/reconcile-capapplicationversion_test.go new file mode 100644 index 0000000..6e9aa4c --- /dev/null +++ b/internal/controller/reconcile-capapplicationversion_test.go @@ -0,0 +1,540 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +package controller + +import ( + "context" + "testing" +) + +func TestCAV_WithoutCAPApplicationAndVersion(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplicationVersion, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-cav-v1"}}, + TestData{ + description: "capapplication version with invalid capapplication and deleted capapplicationversion reference", + expectResourceNotFound: true, + expectError: false, //No errors when no CAV to skip requeues + }, + ) +} + +func TestCAV_WithoutCAPApplication(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplicationVersion, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-cav-v1"}}, + TestData{ + description: "capapplication version with invalid capapplication reference", + initialResources: []string{"testdata/capapplicationversion/cav-invalid-ca.yaml"}, + expectError: true, + }, + ) +} + +func TestCAV_MissingSecrets(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplicationVersion, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-cav-v1"}}, + TestData{ + description: "capapplication version with missing secrets", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/capapplicationversion/cav-empty-status.yaml", + }, + expectedResources: "testdata/capapplicationversion/expected/cav-missing-secrets.yaml", + expectError: false, // CAV is requeued + expectedRequeue: map[int][]NamespacedResourceKey{ResourceCAPApplicationVersion: {{Namespace: "default", Name: "test-cap-01-cav-v1"}}}, + backlogItems: []string{ + "ERP4SMEPREPWORKAPPPLAT-3351", + }, + }, + ) +} + +func TestCAV_EmptyStatusProcessing(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplicationVersion, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-cav-v1"}}, + TestData{ + description: "capapplication version with empty status", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplicationversion/cav-empty-status.yaml", + "testdata/capapplicationversion/content-job-pending.yaml", + }, + expectedResources: "testdata/capapplicationversion/expected/cav-processing.yaml", + }, + ) +} + +func TestCAV_ErrorStatusProcessing(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplicationVersion, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-cav-v1"}}, + TestData{ + description: "capapplication version with error (e.g. api-server error) status to processing", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplicationversion/cav-error-status.yaml", + "testdata/capapplicationversion/content-job-pending.yaml", + }, + expectedResources: "testdata/capapplicationversion/expected/cav-error-processing.yaml", + }, + ) +} + +func TestCAV_ErrorWithConditionStatusProcessing(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplicationVersion, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-cav-v1"}}, + TestData{ + description: "capapplication version with unknown error condition (e.g. api-server error) status to processing", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplicationversion/cav-error-condition-status.yaml", + "testdata/capapplicationversion/content-job-pending.yaml", + }, + expectedResources: "testdata/capapplicationversion/expected/cav-error-condition-processing.yaml", + }, + ) +} + +func TestCAV_ContentJobMissing(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplicationVersion, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-cav-v1"}}, + TestData{ + description: "capapplication version with content job missing", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplicationversion/cav-processing.yaml", + }, + expectedResources: "testdata/capapplicationversion/expected/cav-missing-content-job.yaml", + expectError: true, // job.batch "test-cap-01-cav-v1-content-job" not found + }, + ) +} + +func TestCAV_ContentJobPending(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplicationVersion, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-cav-v1"}}, + TestData{ + description: "capapplication version with pending content job", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplicationversion/cav-processing.yaml", + "testdata/capapplicationversion/content-job-pending.yaml", + }, + expectedResources: "testdata/capapplicationversion/expected/cav-processing.yaml", + }, + ) +} + +func TestCAV_ContentJobFailed(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplicationVersion, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-cav-v1"}}, + TestData{ + description: "capapplication version with failed content job", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplicationversion/cav-processing.yaml", + "testdata/capapplicationversion/content-job-failed.yaml", + }, + expectedResources: "testdata/capapplicationversion/expected/cav-failed-content-job.yaml", + expectError: true, + }, + ) +} + +func TestCAV_ContentJobFailedReconcilation(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplicationVersion, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-cav-v1"}}, + TestData{ + description: "Reconcile error capapplication version with failed content job", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplicationversion/cav-failed-content-job.yaml", + "testdata/capapplicationversion/content-job-failed.yaml", + }, + expectedResources: "testdata/capapplicationversion/expected/cav-failed-content-job.yaml", + expectError: true, + }, + ) +} + +func TestCAV_ContentJobCompletedFromProcessing(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplicationVersion, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-cav-v1"}}, + TestData{ + description: "capapplication version with completed content job", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplicationversion/cav-processing.yaml", + "testdata/capapplicationversion/content-job-completed.yaml", + }, + expectedResources: "testdata/capapplicationversion/expected/cav-ready-content-job.yaml", + }, + ) +} + +func TestCAV_ContentJobCompletedExisting(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplicationVersion, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-cav-v1"}}, + TestData{ + description: "capapplication version with completed content job", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplicationversion/cav-processing-job-finished.yaml", + "testdata/capapplicationversion/content-job-completed.yaml", + }, + expectedResources: "testdata/capapplicationversion/expected/cav-ready-content-job.yaml", + }, + ) +} + +func TestCAV_InvalidEnvConfigContent(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplicationVersion, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-cav-v1"}}, + TestData{ + description: "capapplication version with invalid content env config", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplicationversion/cav-invalid-env-content.yaml", + }, + expectedResources: "testdata/capapplicationversion/expected/cav-failed-env-content.yaml", + expectError: true, + }, + ) +} + +func TestCAV_WithRouterDestinationsEnv(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplicationVersion, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-cav-v1"}}, + TestData{ + description: "capapplication version with invalid router env config", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplicationversion/content-job-completed.yaml", + "testdata/capapplicationversion/cav-merged-destinations-router.yaml", + }, + expectedResources: "testdata/capapplicationversion/expected/cav-ready-merged-destinations-router.yaml", + backlogItems: []string{ + "ERP4SMEPREPWORKAPPPLAT-3386", // merge existing `destinations` operator workload configuration by just overwriting the URL! + }, + }, + ) +} + +func TestCAV_InvalidEnvConfigCAPServer(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplicationVersion, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-cav-v1"}}, + TestData{ + description: "capapplication version with invalid cap server env config", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplicationversion/cav-invalid-env-cap.yaml", + }, + expectedResources: "testdata/capapplicationversion/expected/cav-failed-env-cap.yaml", + expectError: true, + }, + ) +} + +func TestCAV_InvalidEnvConfigJobWorker(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplicationVersion, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-cav-v1"}}, + TestData{ + description: "capapplication version with invalid job worker env config", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplicationversion/cav-invalid-env-job-worker.yaml", + }, + expectedResources: "testdata/capapplicationversion/expected/cav-failed-env-job-worker.yaml", + expectError: true, + }, + ) +} + +func TestCAV_ValidEnvConfigOverall(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplicationVersion, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-cav-v1"}}, + TestData{ + description: "capapplication version with valid overall env config", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplicationversion/content-job-completed.yaml", + "testdata/capapplicationversion/cav-valid-env-config.yaml", + }, + expectedResources: "testdata/capapplicationversion/expected/cav-ready-valid-env-config.yaml", + backlogItems: []string{ + "ERP4SMEPREPWORKAPPPLAT-2048", // Overall the generic workloads should run fine with this change + "ERP4SMEPREPWORKAPPPLAT-3226", // imagePullPolicy + }, + }, + ) +} + +func TestCAV_CustomLabels(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplicationVersion, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-cav-v1"}}, + TestData{ + description: "capapplication version with valid overall config with custom labels", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplicationversion/content-job-completed.yaml", + "testdata/capapplicationversion/cav-custom-labels.yaml", + }, + expectedResources: "testdata/capapplicationversion/expected/cav-ready-custom-labels-config.yaml", + backlogItems: []string{ + "ERP4SMEPREPWORKAPPPLAT-2187", // Custom labels also tested here + }, + }, + ) +} + +func TestCAV_CustomDestinationConfig(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplicationVersion, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-cav-v1"}}, + TestData{ + description: "capapplication version with custom destination config", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplicationversion/content-job-completed.yaml", + "testdata/capapplicationversion/cav-custom-destination-config.yaml", + }, + expectedResources: "testdata/capapplicationversion/expected/cav-ready-custom-destination-config.yaml", + backlogItems: []string{ + "ERP4SMEPREPWORKAPPPLAT-1843", + }, + }, + ) +} + +func TestCAV_DeletingWithReadyTenants(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplicationVersion, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-cav-v1"}}, + TestData{ + description: "capapplication deleting with valid ready tenants", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplicationversion/content-job-completed.yaml", + "testdata/common/captenant-provider-ready.yaml", + "testdata/capapplicationversion/cav-ready-deleting.yaml", + }, + expectedResources: "testdata/capapplicationversion/expected/cav-deleting.yaml", + expectError: false, // cav is requeued until dependants are gone + expectedRequeue: map[int][]NamespacedResourceKey{ResourceCAPApplicationVersion: {{Namespace: "default", Name: "test-cap-01-cav-v1"}}}, + }, + ) +} + +func TestCAV_DeletingWithUpgradingVersionTenants(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplicationVersion, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-cav-v1"}}, + TestData{ + description: "capapplication deleting with valid upgrading (version dependant) tenants", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplicationversion/content-job-completed.yaml", + "testdata/capapplicationversion/cat-provider-version.yaml", + "testdata/capapplicationversion/cav-ready-deleting.yaml", + }, + expectedResources: "testdata/capapplicationversion/expected/cav-deleting.yaml", + expectError: false, // cav is requeued until dependants are gone + expectedRequeue: map[int][]NamespacedResourceKey{ResourceCAPApplicationVersion: {{Namespace: "default", Name: "test-cap-01-cav-v1"}}}, + }, + ) +} + +func TestCAV_DeletedWithNoTenants(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplicationVersion, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-cav-v1"}}, + TestData{ + description: "capapplication deleting with no relevant tenants", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplicationversion/content-job-completed.yaml", + "testdata/capapplicationversion/cav-ready-deleting.yaml", + }, + expectedResources: "testdata/capapplicationversion/expected/cav-deleted.yaml", //finalizers removed + }, + ) +} + +func TestCAV_DeletedWithUnknownStatusNoFinalizers(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplicationVersion, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-cav-v1"}}, + TestData{ + description: "capapplication deleting with unknown status and no finalizers", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplicationversion/content-job-completed.yaml", + "testdata/capapplicationversion/cat-provider-version.yaml", + "testdata/capapplicationversion/cav-unknown-deleting.yaml", + }, + expectedResources: "testdata/capapplicationversion/expected/cav-deleted-unknown.yaml", //finalizers not existing + }, + ) +} + +func TestCAV_ProbesResources(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplicationVersion, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-cav-v1"}}, + TestData{ + description: "capapplication version with probes and resources", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplicationversion/content-job-completed.yaml", + "testdata/capapplicationversion/cav-probes-and-resources.yaml", + }, + expectedResources: "testdata/capapplicationversion/expected/cav-ready-probes-and-resources.yaml", + backlogItems: []string{ + "ERP4SMEPREPWORKAPPPLAT-2237", // Probes and resources should be applied to deployments and jobs + }, + }, + ) +} + +func TestCAV_AppNetworkPolicy(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplicationVersion, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-cav-v1"}}, + TestData{ + description: "capapplication version with default network policies", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplicationversion/content-job-completed.yaml", + "testdata/capapplicationversion/cav-probes-and-resources.yaml", + }, + expectedResources: "testdata/capapplicationversion/expected/cav-ready-app-netpol.yaml", + backlogItems: []string{ + "ERP4SMEPREPWORKAPPPLAT-2638", // Default network policy w/o cluster type ports + "ERP4SMEPREPWORKAPPPLAT-2707", //No N/w policies exist + "ERP4SMEPREPWORKAPPPLAT-2707", // Split n/w policies + }, + }, + ) +} + +func TestCAV_ClusterPortNetworkPolicy(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplicationVersion, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-cav-v1"}}, + TestData{ + description: "capapplication version with cluster network policy ports", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplicationversion/content-job-completed.yaml", + "testdata/capapplicationversion/cav-cluster-netpol-port.yaml", + }, + expectedResources: "testdata/capapplicationversion/expected/cav-ready-cluster-netpol-port.yaml", + backlogItems: []string{ + "ERP4SMEPREPWORKAPPPLAT-2638", // Network policy for cluster-wide "tech" ports + "ERP4SMEPREPWORKAPPPLAT-2707", // No fallback cluster network policy + "ERP4SMEPREPWORKAPPPLAT-2707", // Split n/w policies + }, + }, + ) +} + +func TestCAV_SecurityContext(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplicationVersion, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-cav-v1"}}, + TestData{ + description: "capapplication version with container security context", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplicationversion/content-job-completed.yaml", + "testdata/capapplicationversion/cav-security-context.yaml", + }, + expectedResources: "testdata/capapplicationversion/expected/cav-ready-security-context.yaml", + backlogItems: []string{ + "ERP4SMEPREPWORKAPPPLAT-2573", // Security Context for containers + }, + }, + ) +} + +func TestCAV_PodSecurityContext(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplicationVersion, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-cav-v1"}}, + TestData{ + description: "capapplication version with pod security context", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplicationversion/content-job-completed.yaml", + "testdata/capapplicationversion/cav-pod-security-context.yaml", + }, + expectedResources: "testdata/capapplicationversion/expected/cav-ready-pod-security-context.yaml", + backlogItems: []string{ + "ERP4SMEPREPWORKAPPPLAT-2573", // Security Context for containers + }, + }, + ) +} + +func TestCAV_Annotations(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplicationVersion, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-cav-v1"}}, + TestData{ + description: "capapplication version with annotations", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/capapplicationversion/content-job-completed.yaml", + "testdata/capapplicationversion/cav-annotations.yaml", + }, + expectedResources: "testdata/capapplicationversion/expected/cav-ready-annotations.yaml", + backlogItems: []string{ + "ERP4SMEPREPWORKAPPPLAT-2885", // Annotations supported + }, + }, + ) +} diff --git a/internal/controller/reconcile-captenant.go b/internal/controller/reconcile-captenant.go new file mode 100644 index 0000000..43c5681 --- /dev/null +++ b/internal/controller/reconcile-captenant.go @@ -0,0 +1,697 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +package controller + +import ( + "context" + "fmt" + "strconv" + "time" + + "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/klog/v2" +) + +type IdentifiedCAPTenantOperations struct { + active []v1alpha1.CAPTenantOperation + processed []v1alpha1.CAPTenantOperation +} + +type CAPTenantOperationTypeSelector string + +type CAPTenantStateHandlerFunc func(ctx context.Context, c *Controller, cat *v1alpha1.CAPTenant, target StateCondition, ctop *v1alpha1.CAPTenantOperation) (*ReconcileResult, error) + +type StateCondition struct { + state v1alpha1.CAPTenantState + conditionReason string + conditionStatus metav1.ConditionStatus +} + +type TargetStateHandler struct { + target StateCondition + handler CAPTenantStateHandlerFunc +} + +type StatusInfo struct { + failed TargetStateHandler + completed TargetStateHandler + processing TargetStateHandler +} + +const ( + CAPTenantOperationTypeSelectorAll CAPTenantOperationTypeSelector = "All" + CAPTenantOperationTypeSelectorUpgrade CAPTenantOperationTypeSelector = CAPTenantOperationTypeSelector(v1alpha1.CAPTenantOperationTypeUpgrade) + CAPTenantOperationTypeSelectorProvisioning CAPTenantOperationTypeSelector = CAPTenantOperationTypeSelector(v1alpha1.CAPTenantOperationTypeProvisioning) + CAPTenantOperationTypeSelectorDeprovisioning CAPTenantOperationTypeSelector = CAPTenantOperationTypeSelector(v1alpha1.CAPTenantOperationTypeDeprovisioning) +) + +const ( + CAPTenantEventProcessingStarted = "ProcessingStarted" + CAPTenantEventProvisioningFailed = "ProvisioningFailed" + CAPTenantEventProvisioningCompleted = "ProvisioningCompleted" + CAPTenantEventProvisioningOperationCreated = "ProvisioningOperationCreated" + CAPTenantEventDeprovisioningFailed = "DeprovisioningFailed" + CAPTenantEventDeprovisioningCompleted = "DeprovisioningCompleted" + CAPTenantEventDeprovisioningOperationCreated = "DeprovisioningOperationCreated" + CAPTenantEventUpgradeFailed = "UpgradeFailed" + CAPTenantEventUpgradeCompleted = "UpgradeCompleted" + CAPTenantEventUpgradeOperationCreated = "UpgradeOperationCreated" + CAPTenantEventTenantNetworkingModified = "TenantNetworkingModified" + CAPTenantEventVirtualServiceModificationFailed = "VirtualServiceModificationFailed" + CAPTenantEventDestinationRuleModificationFailed = "DestinationRuleModificationFailed" + CAPTenantEventInvalidReference = "InvalidReference" + CAPTenantEventAutoVersionUpdate = "AutoVersionUpdate" +) + +const ( + EventActionReconcileTenantNetworking = "ReconcileTenantNetworking" + EventActionPrepare = "Prepare" + EventActionUpgrade = "Upgrade" +) + +// maps tenant operation types (and their status) to CAPTenant status changes +var TenantOperationStatusMap = map[v1alpha1.CAPTenantOperationType]StatusInfo{ + v1alpha1.CAPTenantOperationTypeProvisioning: { + failed: TargetStateHandler{ + target: StateCondition{state: v1alpha1.CAPTenantStateProvisioningError, conditionReason: CAPTenantEventProvisioningFailed, conditionStatus: metav1.ConditionFalse}, + handler: handleFailingTenantOperation, + }, + completed: TargetStateHandler{ + target: StateCondition{state: v1alpha1.CAPTenantStateReady, conditionReason: CAPTenantEventProvisioningCompleted, conditionStatus: metav1.ConditionTrue}, + handler: handleCompletedProvisioningUpgradeOperation, + }, + processing: TargetStateHandler{ + target: StateCondition{state: v1alpha1.CAPTenantStateProvisioning, conditionReason: CAPTenantEventProvisioningOperationCreated, conditionStatus: metav1.ConditionFalse}, + handler: handleWaitingForTenantOperation, + }, + }, + v1alpha1.CAPTenantOperationTypeUpgrade: { // NOTE: during upgrades the ready condition status remains "True" as the tenant is in use + failed: TargetStateHandler{ + target: StateCondition{state: v1alpha1.CAPTenantStateUpgradeError, conditionReason: CAPTenantEventUpgradeFailed, conditionStatus: metav1.ConditionTrue}, + handler: handleFailingTenantOperation, + }, + completed: TargetStateHandler{ + target: StateCondition{state: v1alpha1.CAPTenantStateReady, conditionReason: CAPTenantEventUpgradeCompleted, conditionStatus: metav1.ConditionTrue}, + handler: handleCompletedProvisioningUpgradeOperation, + }, + processing: TargetStateHandler{ + target: StateCondition{state: v1alpha1.CAPTenantStateUpgrading, conditionReason: CAPTenantEventUpgradeOperationCreated, conditionStatus: metav1.ConditionTrue}, + handler: handleWaitingForTenantOperation, + }, + }, + v1alpha1.CAPTenantOperationTypeDeprovisioning: { + failed: TargetStateHandler{ + target: StateCondition{state: v1alpha1.CAPTenantStateDeleting, conditionReason: CAPTenantEventDeprovisioningFailed, conditionStatus: metav1.ConditionFalse}, + handler: handleFailingTenantOperation, + }, + completed: TargetStateHandler{ + target: StateCondition{state: v1alpha1.CAPTenantStateDeleting, conditionReason: CAPTenantEventDeprovisioningCompleted, conditionStatus: metav1.ConditionFalse}, + handler: removeTenantFinalizers, + }, + processing: TargetStateHandler{ + target: StateCondition{state: v1alpha1.CAPTenantStateDeleting, conditionReason: CAPTenantEventDeprovisioningOperationCreated, conditionStatus: metav1.ConditionFalse}, + handler: handleWaitingForTenantOperation, + }, + }, +} + +func getTenantReconcileResultConsideringDeletion(cat *v1alpha1.CAPTenant, fallback *ReconcileResult) *ReconcileResult { + if cat.DeletionTimestamp != nil && cat.Status.State != v1alpha1.CAPTenantStateDeleting { + return NewReconcileResultWithResource(ResourceCAPTenant, cat.Name, cat.Namespace, 15*time.Second) + } + return fallback +} + +var handleWaitingForTenantOperation = func(ctx context.Context, c *Controller, cat *v1alpha1.CAPTenant, target StateCondition, ctop *v1alpha1.CAPTenantOperation) (*ReconcileResult, error) { + // NOTE: not returning a requeue item is ok, as changes in CAPTenantOperation status will queue the item via the informer + cat.SetStatusWithReadyCondition(target.state, target.conditionStatus, target.conditionReason, fmt.Sprintf("waiting for %s %s.%s of type %s to complete", v1alpha1.CAPTenantOperationKind, ctop.Namespace, ctop.Name, ctop.Spec.Operation)) + return NewReconcileResultWithResource(ResourceCAPTenant, cat.Name, cat.Namespace, 15*time.Second), nil // requeue while the tenant operation is being processed +} + +var handleCompletedProvisioningUpgradeOperation = func(ctx context.Context, c *Controller, cat *v1alpha1.CAPTenant, target StateCondition, ctop *v1alpha1.CAPTenantOperation) (*ReconcileResult, error) { + message := fmt.Sprintf("%s %s.%s successfully completed", v1alpha1.CAPTenantOperationKind, ctop.Namespace, ctop.Name) + c.Event(cat, ctop, corev1.EventTypeNormal, target.conditionReason, string(target.state), message) + + ca, err := c.crdInformerFactory.Sme().V1alpha1().CAPApplications().Lister().CAPApplications(cat.Namespace).Get(cat.Spec.CAPApplicationInstance) + if err != nil { + return nil, err + } + // check for dns entries only when there are secondary domains + if len(ca.Spec.Domains.Secondary) > 0 { + // Check if all Tenant DNSEntries are Ready + processing, err := c.checkTenantDNSEntries(ctx, cat) + if err != nil { + klog.Error("error with DNS Entries for CAPTenant ", cat.Namespace, ".", cat.Name) + return nil, err + } + if processing { + // requeue to iterate this check after a delay + return NewReconcileResultWithResource(ResourceCAPTenant, cat.Name, cat.Namespace, 10*time.Second), nil + } + } + + // check and reconcile tenant virtual service + // adjust virtual service only when tenant is finalizing (after provisioning or upgrade) + requeue, err := c.reconcileTenantNetworking(ctx, cat, ctop.Spec.CAPApplicationVersionInstance, ca) + if err != nil || requeue != nil { + return requeue, err + } + + // the ObservedGeneration of the tenant should be updated here (when Ready) + cat.SetStatusWithReadyCondition(target.state, target.conditionStatus, target.conditionReason, message) + cat.SetStatusCAPApplicationVersion(ctop.Spec.CAPApplicationVersionInstance) + return getTenantReconcileResultConsideringDeletion(cat, nil), nil +} + +var handleFailingTenantOperation = func(ctx context.Context, c *Controller, cat *v1alpha1.CAPTenant, target StateCondition, ctop *v1alpha1.CAPTenantOperation) (*ReconcileResult, error) { + var ( + message string + related runtime.Object = nil + ) + if ctop == nil { + message = fmt.Sprintf("could not identify %s for tenant state %s", v1alpha1.CAPTenantOperationKind, cat.Status.State) + } else { + message = fmt.Sprintf("%s %s.%s failed", v1alpha1.CAPTenantOperationKind, ctop.Namespace, ctop.Name) + related = ctop + } + + c.Event(cat, related, corev1.EventTypeWarning, target.conditionReason, string(target.state), message) + cat.SetStatusWithReadyCondition(target.state, target.conditionStatus, target.conditionReason, message) + return getTenantReconcileResultConsideringDeletion(cat, nil), nil +} + +var removeTenantFinalizers = func(ctx context.Context, c *Controller, cat *v1alpha1.CAPTenant, target StateCondition, ctop *v1alpha1.CAPTenantOperation) (*ReconcileResult, error) { + if ctop != nil { + c.Event(cat, ctop, corev1.EventTypeNormal, target.conditionReason, string(target.state), fmt.Sprintf("%s of %s %s.%s successfully completed; attempting to remove finalizers", ctop.Spec.Operation, v1alpha1.CAPTenantKind, cat.Namespace, cat.Name)) + } + + // remove known finalizer + if removeFinalizer(&cat.Finalizers, FinalizerCAPTenant) { + return c.updateCAPTenant(ctx, cat, false) + } + return nil, nil +} + +func (c *Controller) reconcileCAPTenant(ctx context.Context, item QueueItem, attempts int) (requeue *ReconcileResult, err error) { + cached, err := c.crdInformerFactory.Sme().V1alpha1().CAPTenants().Lister().CAPTenants(item.ResourceKey.Namespace).Get(item.ResourceKey.Name) + if err != nil { + return nil, handleOperatorResourceErrors(err) + } + cat := cached.DeepCopy() + + defer func() { + if statusErr := c.updateCAPTenantStatus(ctx, cat); statusErr != nil && err != nil { + err = statusErr + } + }() + + // prepare owner refs, labels, finalizers + if update, err := c.prepareCAPTenant(ctx, cat); err != nil { + return nil, err + } else if update { + return c.updateCAPTenant(ctx, cat, true) + } + + // Skip processing until the right version is set on the CAPTenant (via CAPApplication) + // This indirectly ensures that we do not create duplicate tenant operations for consumer tenant provisioning scenarios! + if cat.Spec.Version == "" { + klog.Info(v1alpha1.CAPTenantKind, " without version detected, skip processing until version is set on ", cat.Namespace, ".", cat.Name) + return requeue, nil + } + + if cat.DeletionTimestamp == nil { + // Create relevant DNSEntries for this tenant. DNS entries are checked before setting the tenant as ready + if err = c.reconcileTenantDNSEntries(ctx, cat); err != nil { + return + } + } + + // create and track CAPTenantOperations based on state, deletion timestamp, version change etc. + requeue, err = c.handleTenantOperationsForCAPTenant(ctx, cat) + if err != nil || requeue != nil { + return + } + + if cat.DeletionTimestamp == nil && cat.Status.CurrentCAPApplicationVersionInstance != "" { + requeue, err = c.reconcileTenantNetworking(ctx, cat, cat.Status.CurrentCAPApplicationVersionInstance, nil) + } + + return +} + +func (c *Controller) updateCAPTenant(ctx context.Context, cat *v1alpha1.CAPTenant, requeue bool) (result *ReconcileResult, err error) { + var catUpdated *v1alpha1.CAPTenant + catUpdated, err = c.crdClient.SmeV1alpha1().CAPTenants(cat.Namespace).Update(ctx, cat, metav1.UpdateOptions{}) + // Update reference to the resource + if catUpdated != nil { + *cat = *catUpdated + } + if requeue { + result = NewReconcileResultWithResource(ResourceCAPTenant, cat.Name, cat.Namespace, 0) + } + return +} + +func findLatestCreatedTenantOperation(ops []v1alpha1.CAPTenantOperation, selector CAPTenantOperationTypeSelector) (latest *v1alpha1.CAPTenantOperation) { + for _, op := range ops { + // workaround to fix pointer resolution after loop -> https://stackoverflow.com/questions/45967305/copying-the-address-of-a-loop-variable-in-go + ctop := op + if selector != CAPTenantOperationTypeSelectorAll && CAPTenantOperationTypeSelector(ctop.Spec.Operation) != selector { + continue + } + if latest == nil || ctop.CreationTimestamp.After(latest.CreationTimestamp.Time) { + latest = &ctop + } + } + + return latest +} + +func findCAPTenantOperationTypeFromProcessingState(state v1alpha1.CAPTenantState) v1alpha1.CAPTenantOperationType { + var opType v1alpha1.CAPTenantOperationType + for k, v := range TenantOperationStatusMap { + if v.processing.target.state == state { + opType = k + break + } + } + return opType +} + +func isTenantOperationConditionFailed(ctop *v1alpha1.CAPTenantOperation) bool { + ready := ctop.Status.GenericStatus.Conditions[0] + // NOTE: check reason != StepCompleted, instead of == StepFailed (operation could fail because of multiple reasons) + if ready.Status == metav1.ConditionTrue && ready.Reason != CAPTenantOperationConditionReasonStepCompleted { + return true + } + return false +} + +func (c *Controller) handleTenantOperationsForCAPTenant(ctx context.Context, cat *v1alpha1.CAPTenant) (*ReconcileResult, error) { + ops, err := c.getCAPTenantOperationsByType(ctx, cat, CAPTenantOperationTypeSelectorAll) + if err != nil { + return nil, err + } + + // [1] wait for active operations to complete + if len(ops.active) > 0 { + if len(ops.active) > 1 { + klog.Warningf("identified multiple active CAPTenantOperations for %s %s.%s", v1alpha1.CAPTenantKind, cat.Namespace, cat.Name) + } + ctop := findLatestCreatedTenantOperation(ops.active, CAPTenantOperationTypeSelectorAll) + targetInfo := TenantOperationStatusMap[ctop.Spec.Operation].processing + return targetInfo.handler(ctx, c, cat, targetInfo.target, ctop) + } + + // [2] look for operations which have recently finished and set status accordingly + if cat.Status.State == v1alpha1.CAPTenantStateProvisioning || cat.Status.State == v1alpha1.CAPTenantStateUpgrading || cat.Status.State == v1alpha1.CAPTenantStateDeleting { + opType := findCAPTenantOperationTypeFromProcessingState(cat.Status.State) + ctop := findLatestCreatedTenantOperation(ops.processed, CAPTenantOperationTypeSelector(opType)) + var targetInfo TargetStateHandler + if ctop != nil { + if isTenantOperationConditionFailed(ctop) { // operation state is failed or the operation does not exist + targetInfo = TenantOperationStatusMap[opType].failed + } else { // operation state is completed + targetInfo = TenantOperationStatusMap[opType].completed + } + return targetInfo.handler(ctx, c, cat, targetInfo.target, ctop) + } // else => fall through to create new operation - also for deprovisioning + } + + // [3] create new tenant operations when necessary + return c.handleNewTenantOperations(ctx, cat, ops) +} + +func (c *Controller) handleNewTenantOperations(ctx context.Context, cat *v1alpha1.CAPTenant, ops *IdentifiedCAPTenantOperations) (*ReconcileResult, error) { + // (1)) process deletion when Deletion timestamp is set + if cat.DeletionTimestamp != nil { + if cat.Status.CurrentCAPApplicationVersionInstance == "" { + // there is no valid version so far -> deprovisioning is not required + return removeTenantFinalizers(ctx, c, cat, TenantOperationStatusMap[v1alpha1.CAPTenantOperationTypeDeprovisioning].completed.target, nil) + } else { + return c.triggerTenantOperation(ctx, cat, v1alpha1.CAPTenantOperationTypeDeprovisioning) + } + } + + // (2) Check whether a new tenant operation is required (in case of ProvisioningError / UpgradeError) + if isRequired, err := c.isNewTenantOperationRequired(ctx, cat, ops); err != nil || !isRequired { + return nil, err + } + + // (3) start provisioning when there is no current version + if cat.Status.CurrentCAPApplicationVersionInstance == "" { + return c.triggerTenantOperation(ctx, cat, v1alpha1.CAPTenantOperationTypeProvisioning) + } + + // (4) check for newer version to upgrade + return c.tryForTenantUpgrade(ctx, cat) +} + +func (c *Controller) isNewTenantOperationRequired(ctx context.Context, cat *v1alpha1.CAPTenant, ops *IdentifiedCAPTenantOperations) (bool, error) { + if cat.Status.State != v1alpha1.CAPTenantStateProvisioningError && cat.Status.State != v1alpha1.CAPTenantStateUpgradeError { + return true, nil + } + + // find existing operation (processed) - there can be no active operations at this point in the code (active operations have already been considered) + var opType v1alpha1.CAPTenantOperationType + if cat.Status.State == v1alpha1.CAPTenantStateProvisioningError { + opType = v1alpha1.CAPTenantOperationTypeProvisioning + } else { + opType = v1alpha1.CAPTenantOperationTypeUpgrade + } + ctop := findLatestCreatedTenantOperation(ops.processed, CAPTenantOperationTypeSelector(opType)) + if ctop == nil { + // a new tenant operation is to be created when there are none existing (or older ones have been deleted manually) + return true, nil + } + + cav, err := c.getCAPApplicationVersionForTenantOperationType(ctx, cat, opType) + if err != nil || cav == nil { + return false, err + } + + // a new tenant operation needs to be created when there is a different CAPApplicationVersion available for the lifecycle operation + return ctop.Spec.CAPApplicationVersionInstance != cav.Name, nil +} + +// create a CAPTenantOperation instance of a specific type (provisioning/deprovisioning/upgrade) +func (c *Controller) createCAPTenantOperation(ctx context.Context, cat *v1alpha1.CAPTenant, opType v1alpha1.CAPTenantOperationType) (*v1alpha1.CAPTenantOperation, error) { + // delete all previous tenant operations of the current type + labelsMap := map[string]string{ + LabelOwnerIdentifierHash: sha1Sum(cat.Namespace, cat.Name), + LabelTenantOperationType: string(opType), + } + selector, err := labels.ValidatedSelectorFromSet(labelsMap) + if err != nil { + return nil, err + } + err = c.crdClient.SmeV1alpha1().CAPTenantOperations(cat.Namespace).DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{LabelSelector: selector.String()}) + if err != nil { + return nil, fmt.Errorf("deletion of previous %ss of type %s failed: %s", v1alpha1.CAPTenantOperationKind, opType, err.Error()) + } + + // get CAPApplicationVersion to be used for the TenantOperation job + cav, err := c.getCAPApplicationVersionForTenantOperationType(ctx, cat, opType) + if err != nil { + return nil, err + } + + // determine operation steps + steps, err := deriveStepsForTenantOperation(cav, opType) + if err != nil { + return nil, err + } + + // create CAPTenantOperation + ctop := &v1alpha1.CAPTenantOperation{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: cat.Namespace, + GenerateName: cat.Name + "-", + OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(cat, v1alpha1.SchemeGroupVersion.WithKind(v1alpha1.CAPTenantKind))}, + Finalizers: []string{FinalizerCAPTenantOperation}, + }, + Spec: v1alpha1.CAPTenantOperationSpec{ + Operation: opType, + BTPTenantIdentification: v1alpha1.BTPTenantIdentification{SubDomain: cat.Spec.SubDomain, TenantId: cat.Spec.TenantId}, + CAPApplicationVersionInstance: cav.Name, + Steps: steps, + }, + } + addCAPTenantOperationLabels(ctop, cat) // NOTE: this is very important to do here as subsequent reconciliation of tenant will be inconsistent otherwise + return c.crdClient.SmeV1alpha1().CAPTenantOperations(cat.Namespace).Create(ctx, ctop, metav1.CreateOptions{}) +} + +func deriveStepsForTenantOperation(cav *v1alpha1.CAPApplicationVersion, opType v1alpha1.CAPTenantOperationType) (steps []v1alpha1.CAPTenantOperationStep, err error) { + defaultSteps := func() { + // if there are no specified steps, add only one step of type TenantOperation + workload := getRelevantJob(v1alpha1.JobTenantOperation, cav) + if workload == nil { // fallback to CAP workload + workload = getRelevantDeployment(v1alpha1.DeploymentCAP, cav) + } + if workload == nil { + // cannot proceed further without an identified workload + err = fmt.Errorf("could not find workload of type %s or %s in %s %s.%s", v1alpha1.JobTenantOperation, v1alpha1.DeploymentCAP, v1alpha1.CAPApplicationVersionKind, cav.Namespace, cav.Name) + } else { + steps = []v1alpha1.CAPTenantOperationStep{{Name: workload.Name, Type: v1alpha1.JobTenantOperation}} + } + } + + if cav.Spec.TenantOperations == nil { + defaultSteps() + return + } + + var ops []v1alpha1.TenantOperationWorkloadReference + switch opType { + case v1alpha1.CAPTenantOperationTypeProvisioning: + ops = cav.Spec.TenantOperations.Provisioning + case v1alpha1.CAPTenantOperationTypeDeprovisioning: + ops = cav.Spec.TenantOperations.Deprovisioning + case v1alpha1.CAPTenantOperationTypeUpgrade: + ops = cav.Spec.TenantOperations.Upgrade + } + + if len(ops) == 0 { + defaultSteps() + return + } + + steps = []v1alpha1.CAPTenantOperationStep{} + addedTenantOprJob := false + for _, entry := range ops { + workload := getWorkloadByName(entry.WorkloadName, cav) + switch workload.JobDefinition.Type { + case v1alpha1.JobTenantOperation: + addedTenantOprJob = true + fallthrough + case v1alpha1.JobCustomTenantOperation: + // continuing the tenant operation on failure is not possible for tenant operation jobs + continueOnFailure := (workload.JobDefinition.Type != v1alpha1.JobTenantOperation) && entry.ContinueOnFailure + steps = append(steps, v1alpha1.CAPTenantOperationStep{Name: entry.WorkloadName, Type: workload.JobDefinition.Type, ContinueOnFailure: continueOnFailure}) + default: + continue // ignore all other types (even if specified) + } + } + if !addedTenantOprJob { // ensure step of type TenantOperation is added + return nil, fmt.Errorf("specified steps for operation %s does not contain a step of type %s", opType, v1alpha1.JobTenantOperation) + } + return +} + +// trigger CAPTenantOperation creation and change status of tenant +func (c *Controller) triggerTenantOperation(ctx context.Context, cat *v1alpha1.CAPTenant, opType v1alpha1.CAPTenantOperationType) (*ReconcileResult, error) { + // Create CAPTenantOperation + ctop, err := c.createCAPTenantOperation(ctx, cat, opType) + if err != nil { + return nil, err + } + + // call status handler + targetInfo := TenantOperationStatusMap[opType].processing + c.Event(cat, ctop, corev1.EventTypeNormal, targetInfo.target.conditionReason, string(targetInfo.target.state), fmt.Sprintf("%s %s.%s of type %s created", v1alpha1.CAPTenantOperationKind, ctop.Namespace, ctop.Name, ctop.Spec.Operation)) + return targetInfo.handler(ctx, c, cat, targetInfo.target, ctop) +} + +func (c *Controller) getCAPTenantOperationsByType(ctx context.Context, cat *v1alpha1.CAPTenant, operationTypeSelector CAPTenantOperationTypeSelector) (*IdentifiedCAPTenantOperations, error) { + labelsMap := map[string]string{ + LabelOwnerIdentifierHash: sha1Sum(cat.Namespace, cat.Name), + } + if operationTypeSelector != CAPTenantOperationTypeSelectorAll { + labelsMap[LabelTenantOperationType] = string(operationTypeSelector) + } + selector, err := labels.ValidatedSelectorFromSet(labelsMap) + if err != nil { + return nil, err + } + + // NOTE: do not use cache for listing (this is not a very frequent operation) + ops, err := c.crdClient.SmeV1alpha1().CAPTenantOperations(cat.Namespace).List(ctx, metav1.ListOptions{LabelSelector: selector.String()}) + if err != nil { + return nil, err + } + + var results = IdentifiedCAPTenantOperations{active: []v1alpha1.CAPTenantOperation{}, processed: []v1alpha1.CAPTenantOperation{}} + for _, ctop := range ops.Items { + if isCROConditionReady(ctop.Status.GenericStatus) { + results.processed = append(results.processed, ctop) + } else { + results.active = append(results.active, ctop) + } + } + + return &results, nil +} + +func (c *Controller) getCAPApplicationVersionForTenantOperationType(ctx context.Context, cat *v1alpha1.CAPTenant, opType v1alpha1.CAPTenantOperationType) (*v1alpha1.CAPApplicationVersion, error) { + // get owning CAPApplication + ca, _ := c.getCachedCAPApplication(cat.Namespace, cat.Spec.CAPApplicationInstance) + + // get CAPApplication version + switch opType { + case v1alpha1.CAPTenantOperationTypeProvisioning, v1alpha1.CAPTenantOperationTypeUpgrade: // for provisioning or upgrade - use the relevant CAPApplicationVersion + // get relevant CAPApplicationVersion + cav, err := c.getRelevantCAPApplicationVersion(ctx, ca, cat.Spec.Version) + if err != nil { + return nil, err + } + klog.V(2).Info("identified CAPApplicationVersion ", cav.Namespace, ".", cav.Name, " (", cav.Spec.Version, ") for ", opType, " of CAPTenant ", cat.Namespace, ".", cat.Name) + return cav, nil + case v1alpha1.CAPTenantOperationTypeDeprovisioning: // for deletion - use the current CAPApplicationVersion (from status) + if cat.Status.CurrentCAPApplicationVersionInstance == "" { + return nil, fmt.Errorf("cannot identify %s for %s %s.%s", v1alpha1.CAPApplicationVersionKind, v1alpha1.CAPTenantKind, cat.Namespace, cat.Name) + } + cav, err := c.crdClient.SmeV1alpha1().CAPApplicationVersions(cat.Namespace).Get(ctx, cat.Status.CurrentCAPApplicationVersionInstance, metav1.GetOptions{}) + if err != nil { + return nil, err + } + // verify status of CAPApplicationVersion + if !isCROConditionReady(cav.Status.GenericStatus) { + return nil, fmt.Errorf("%s %s.%s is not %s to be used for %s", v1alpha1.CAPApplicationVersionKind, cav.Namespace, cav.Name, v1alpha1.CAPApplicationVersionStateReady, opType) + } + // verify owner reference + if cav.Spec.CAPApplicationInstance != ca.Name { + return nil, fmt.Errorf("found deviating owner references for %s %s.%s and %s %s.%s", v1alpha1.CAPApplicationVersionKind, cav.Namespace, cav.Name, v1alpha1.CAPTenantKind, cat.Namespace, cat.Name) + } + return cav, nil + } + return nil, fmt.Errorf("unknown error when resolving %s for %s %s.%s", v1alpha1.CAPApplicationVersionKind, v1alpha1.CAPTenantKind, cat.Namespace, cat.Name) +} + +func addCAPTenantLabels(cat *v1alpha1.CAPTenant, ca *v1alpha1.CAPApplication) (updated bool) { + appMetadata := appMetadataIdentifiers{ + globalAccountId: ca.Spec.GlobalAccountId, + appName: ca.Spec.BTPAppName, + ownerInfo: &ownerInfo{ + ownerNamespace: ca.Namespace, + ownerName: ca.Name, + ownerGeneration: ca.Generation, + }, + } + if updateLabelAnnotationMetadata(&cat.ObjectMeta, &appMetadata) { + updated = true + } + if _, ok := cat.ObjectMeta.Labels[LabelTenantId]; !ok { + cat.ObjectMeta.Labels[LabelTenantId] = cat.Spec.TenantId + updated = true + } + if _, ok := cat.ObjectMeta.Labels[LabelTenantType]; !ok { + if ca.Spec.Provider.TenantId == cat.Spec.TenantId { + cat.ObjectMeta.Labels[LabelTenantType] = ProviderTenantType + } else { + cat.ObjectMeta.Labels[LabelTenantType] = ConsumerTenantType + } + updated = true + } + return updated +} + +func (c *Controller) prepareCAPTenant(ctx context.Context, cat *v1alpha1.CAPTenant) (update bool, err error) { + // Do nothing when object is deleted + if cat.DeletionTimestamp != nil { + return false, nil + } + ca, err := c.getCachedCAPApplication(cat.Namespace, cat.Spec.CAPApplicationInstance) + if err != nil { + msg := fmt.Sprintf("invalid %s reference", v1alpha1.CAPApplicationKind) + c.Event(cat, nil, corev1.EventTypeWarning, CAPTenantEventInvalidReference, EventActionPrepare, msg) + return false, err + } + + // create owner reference - CAPApplication + if _, ok := getOwnerByKind(cat.OwnerReferences, v1alpha1.CAPApplicationKind); !ok { + cat.OwnerReferences = append(cat.OwnerReferences, *metav1.NewControllerRef(ca, v1alpha1.SchemeGroupVersion.WithKind(v1alpha1.CAPApplicationKind))) + update = true + } + + if addCAPTenantLabels(cat, ca) { + update = true + } + + if cat.DeletionTimestamp == nil { + // set finalizers if not added + if cat.Finalizers == nil { + cat.Finalizers = []string{} + } + if addFinalizer(&cat.Finalizers, FinalizerCAPTenant) { + update = true + } + } + return update, nil +} + +func (c *Controller) tryForTenantUpgrade(ctx context.Context, cat *v1alpha1.CAPTenant) (*ReconcileResult, error) { + // try for upgrade only when upgrade strategy is not 'never' + if cat.Spec.VersionUpgradeStrategy == v1alpha1.VersionUpgradeStrategyTypeNever { + return nil, nil + } + + ca, err := c.getCachedCAPApplication(cat.Namespace, cat.Spec.CAPApplicationInstance) + if err != nil { + return nil, err + } + + // fetch CAPApplicationVersion as per current tenant spec + cav, err := c.getRelevantCAPApplicationVersion(ctx, ca, cat.Spec.Version) + if err != nil { + return nil, err + } + + // compare with current version + if cat.Status.CurrentCAPApplicationVersionInstance != cav.Name { + // update status of the CAPTenant - ready for upgrade + return c.triggerTenantOperation(ctx, cat, v1alpha1.CAPTenantOperationTypeUpgrade) + } + + return nil, nil +} + +func (c *Controller) updateCAPTenantStatus(ctx context.Context, cat *v1alpha1.CAPTenant) error { + if isDeletionImminent(&cat.ObjectMeta) { + return nil + } + + if len(cat.Status.Conditions) == 0 { + // initialize conditions - with processing status + cat.SetStatusWithReadyCondition(cat.Status.State, metav1.ConditionFalse, CAPTenantEventProcessingStarted, "") + } + catUpdated, err := c.crdClient.SmeV1alpha1().CAPTenants(cat.Namespace).UpdateStatus(ctx, cat, metav1.UpdateOptions{}) + // update reference to the resource + if catUpdated != nil { + *cat = *catUpdated + } + return err +} + +func (*Controller) enforceTenantResourceOwnership(objMeta *metav1.ObjectMeta, typeMeta *metav1.TypeMeta, cat *v1alpha1.CAPTenant) (bool, error) { + var update bool + // verify owner references + if owner, ok := getOwnerByKind(objMeta.OwnerReferences, v1alpha1.CAPTenantKind); !ok { + // set owner reference + if objMeta.OwnerReferences == nil { + objMeta.OwnerReferences = []metav1.OwnerReference{} + } + objMeta.OwnerReferences = append(objMeta.OwnerReferences, *metav1.NewControllerRef(cat, v1alpha1.SchemeGroupVersion.WithKind(v1alpha1.CAPTenantKind))) + update = true + } else if owner.Name != cat.Name { + return false, fmt.Errorf("invalid owner reference found for %s %s.%s", typeMeta.Kind, objMeta.Namespace, objMeta.Name) + } + + // set labels, but do not set update to true (set based on owner reference or spec changes) + if objMeta.Labels == nil { + objMeta.Labels = map[string]string{} + } + objMeta.Labels[LabelOwnerIdentifierHash] = sha1Sum(cat.Namespace, cat.Name) + objMeta.Labels[LabelOwnerGeneration] = strconv.FormatInt(cat.Generation, 10) + if objMeta.Annotations == nil { + objMeta.Annotations = map[string]string{} + } + objMeta.Annotations[AnnotationOwnerIdentifier] = cat.Namespace + "." + cat.Name + + return update, nil +} diff --git a/internal/controller/reconcile-captenant_test.go b/internal/controller/reconcile-captenant_test.go new file mode 100644 index 0000000..dd72e21 --- /dev/null +++ b/internal/controller/reconcile-captenant_test.go @@ -0,0 +1,595 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +package controller + +import ( + "context" + "os" + "testing" + + "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" +) + +func TestInvalidCAPApplication(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenant, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider"}}, + TestData{ + description: "new captenant with invalid capapplication reference", + initialResources: []string{"testdata/captenant/cat-01.initial.yaml"}, + expectError: true, + }, + ) +} + +func TestLabelsOwnerRefs(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenant, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-consumer"}}, + TestData{ + description: "new captenant without labels, finalizers and owner references", + initialResources: []string{"testdata/common/capapplication.yaml", "testdata/captenant/cat-02.initial.yaml"}, + expectedResources: "testdata/captenant/cat-02.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ResourceCAPTenant: {{Name: "test-cap-01-consumer", Namespace: "default"}}}, + backlogItems: []string{ + "ERP4SMEPREPWORKAPPPLAT-3351", + }, + }, + ) +} + +func TestWithoutSpecVersion(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenant, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-consumer"}}, + TestData{ + description: "new captenant with labels, finalizers and owner references and no version in spec", + initialResources: []string{"testdata/common/capapplication.yaml", "testdata/captenant/cat-with-no-version.yaml"}, + expectedResources: "testdata/captenant/cat-with-no-version.yaml", + }, + ) +} + +func TestInvalidVersion(t *testing.T) { + err := reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenant, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-consumer"}}, + TestData{ + description: "new captenant with invalid version", + initialResources: []string{ + "testdata/common/istio-ingress.yaml", + "testdata/common/capapplication.yaml", + "testdata/captenant/cat-14.initial.yaml", + }, + expectError: true, + }, + ) + + if err.Error() != "could not find a CAPApplicationVersion with status Ready for CAPApplication default.test-cap-01 and version 5.6.7" { + t.Error("error message did not match expected") + } +} + +func TestCAPTenantStartProvisioning(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenant, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-consumer"}}, + TestData{ + description: "new captenant start provisioning (with secondary domains)", + initialResources: []string{ + "testdata/common/istio-ingress.yaml", + "testdata/common/capapplication.yaml", + "testdata/common/capapplicationversion-v1.yaml", + "testdata/captenant/cat-03.initial.yaml", + }, + expectedResources: "testdata/captenant/cat-03.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ResourceCAPTenant: {{Namespace: "default", Name: "test-cap-01-consumer"}}}, + }, + ) +} + +func TestCAPTenantProvisioningCompletedDNSEntriesNotReady(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenant, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider"}}, + TestData{ + description: "captenant provisioning operation completed dns entries not ready", + initialResources: []string{ + "testdata/common/istio-ingress.yaml", + "testdata/common/capapplication.yaml", + "testdata/common/capapplicationversion-v1.yaml", + "testdata/captenant/provider-tenant-dnsentry-not-ready.yaml", + "testdata/captenant/cat-04.initial.yaml", + }, + expectedResources: "testdata/captenant/cat-04.initial.yaml", // expect the same resource state + expectedRequeue: map[int][]NamespacedResourceKey{ResourceCAPTenant: {{Namespace: "default", Name: "test-cap-01-provider"}}}, + }, + ) +} + +func TestCAPTenantProvisioningCompleted(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenant, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider"}}, + TestData{ + description: "captenant provisioning operation completed (creates virtual service and destination rule)", + initialResources: []string{ + "testdata/common/istio-ingress.yaml", + "testdata/common/capapplication.yaml", + "testdata/common/operator-gateway.yaml", + "testdata/common/capapplicationversion-v1.yaml", + "testdata/captenant/provider-tenant-dnsentry.yaml", + "testdata/captenant/cat-04.initial.yaml", + }, + expectedResources: "testdata/captenant/cat-04.expected.yaml", + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2811"}, + }, + ) +} + +func TestCAPTenantProvisioningCompletedDestinationRuleModificationFailure(t *testing.T) { + err := reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenant, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider"}}, + TestData{ + description: "captenant provisioning operation completed (destination rule creation fails)", + initialResources: []string{ + "testdata/common/istio-ingress.yaml", + "testdata/common/capapplication.yaml", + "testdata/common/operator-gateway.yaml", + "testdata/common/capapplicationversion-v1.yaml", + "testdata/captenant/provider-tenant-dnsentry.yaml", + "testdata/captenant/cat-04.initial.yaml", + }, + expectError: true, + mockErrorForResources: []ResourceAction{{Verb: "create", Group: "networking.istio.io", Version: "v1beta1", Resource: "destinationrules", Namespace: "default", Name: "test-cap-01-provider"}}, + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2811"}, + }, + ) + if err.Error() != "mocked api error (destinationrules.networking.istio.io/v1beta1)" { + t.Error("error message is different from expected") + } +} + +func TestCAPTenantProvisioningFailed(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenant, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider"}}, + TestData{ + description: "captenant provisioning operation failed", + initialResources: []string{ + "testdata/common/istio-ingress.yaml", + "testdata/common/capapplication.yaml", + "testdata/common/capapplicationversion-v1.yaml", + "testdata/captenant/cat-05.initial.yaml", + }, + expectedResources: "testdata/captenant/cat-05.expected.yaml", + }, + ) +} + +func TestCAPTenantProvisioningRequestFailedWithInvalidEnv(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenant, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider"}}, + TestData{ + description: "captenant provisioning operation failed due to invalid env (status reason)", + initialResources: []string{ + "testdata/common/istio-ingress.yaml", + "testdata/common/capapplication.yaml", + "testdata/common/capapplicationversion-invalid-env.yaml", + "testdata/captenant/cat-26.initial.yaml", + }, + expectedResources: "testdata/captenant/cat-05.expected.yaml", + }, + ) +} + +func TestCAPTenantProvisioningRequestDeleted(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenant, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider"}}, + TestData{ + description: "captenant provisioning operation deleted while tenant is provisioning", + initialResources: []string{ + "testdata/common/istio-ingress.yaml", + "testdata/common/capapplication.yaml", + "testdata/common/capapplicationversion-v1.yaml", + "testdata/captenant/cat-20.initial.yaml", + }, + expectedResources: "testdata/captenant/cat-20.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ResourceCAPTenant: {{Namespace: "default", Name: "test-cap-01-provider"}}}, + }, + ) +} + +func TestCAPTenantWaitingForProvisioning(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenant, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider"}}, + TestData{ + description: "captenant waiting for provisioning operation", + initialResources: []string{ + "testdata/common/istio-ingress.yaml", + "testdata/common/capapplication.yaml", + "testdata/common/capapplicationversion-v1.yaml", + "testdata/captenant/cat-06.initial.yaml", + }, + expectedResources: "testdata/captenant/cat-06.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ResourceCAPTenant: {{Namespace: "default", Name: "test-cap-01-provider"}}}, + }, + ) +} + +func TestCAPTenantStartUpgradeWithStrategyAlways(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenant, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider"}}, + TestData{ + description: "captenant start upgrade from ready state with strategy always", + initialResources: []string{ + "testdata/common/istio-ingress.yaml", + "testdata/common/capapplication.yaml", + "testdata/common/capapplicationversion-v1.yaml", + "testdata/common/capapplicationversion-v2.yaml", + "testdata/captenant/cat-07.initial.yaml", + }, + expectedResources: "testdata/captenant/cat-07.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ResourceCAPTenant: {{Namespace: "default", Name: "test-cap-01-provider"}}}, + }, + ) +} + +func TestCAPTenantStartUpgradeWithStrategyNever(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenant, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider"}}, + TestData{ + description: "captenant start upgrade from ready state with strategy never ", + initialResources: []string{ + "testdata/common/istio-ingress.yaml", + "testdata/common/operator-gateway.yaml", + "testdata/common/capapplication.yaml", + "testdata/common/capapplicationversion-v1.yaml", + "testdata/common/capapplicationversion-v2.yaml", + "testdata/captenant/cat-08.initial.yaml", + "testdata/captenant/provider-tenant-vs-v1.yaml", + "testdata/captenant/provider-tenant-dr-v1.yaml", + }, + expectedResources: "testdata/captenant/cat-08.expected.yaml", + }, + ) +} + +func TestCAPTenantDeprovisioningFromReady(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenant, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider"}}, + TestData{ + description: "captenant deprovisioning from ready", + initialResources: []string{ + "testdata/common/istio-ingress.yaml", + "testdata/common/capapplication.yaml", + "testdata/common/capapplicationversion-v1.yaml", + "testdata/common/capapplicationversion-v2.yaml", + "testdata/captenant/cat-09.initial.yaml", + }, + expectedResources: "testdata/captenant/cat-09.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ResourceCAPTenant: {{Namespace: "default", Name: "test-cap-01-provider"}}}, + }, + ) +} + +func TestCAPTenantDeprovisioningCompleted(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenant, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider"}}, + TestData{ + description: "captenant deprovisioning completed", + initialResources: []string{ + "testdata/common/istio-ingress.yaml", + "testdata/common/capapplication.yaml", + "testdata/common/capapplicationversion-v1.yaml", + "testdata/captenant/cat-10.initial.yaml", + }, + expectedResources: "testdata/captenant/cat-10.expected.yaml", + }, + ) +} + +func TestCAPTenantDeprovisioningFromProvisioningError(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenant, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider"}}, + TestData{ + description: "captenant deprovisioning from provisioning error", + initialResources: []string{ + "testdata/common/istio-ingress.yaml", + "testdata/common/capapplication.yaml", + "testdata/common/capapplicationversion-v1.yaml", + "testdata/captenant/cat-11.initial.yaml", + }, + expectedResources: "testdata/captenant/cat-11.expected.yaml", + }, + ) +} + +func TestCAPTenantUpgradeOperationCompleted(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenant, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider"}}, + TestData{ + description: "captenant upgrade operation completed expecting virtual service, destination rule adjustments", + initialResources: []string{ + "testdata/common/istio-ingress.yaml", + "testdata/common/capapplication.yaml", + "testdata/common/operator-gateway.yaml", + "testdata/common/capapplicationversion-v1.yaml", + "testdata/common/capapplicationversion-v2.yaml", + "testdata/captenant/provider-tenant-dnsentry.yaml", + "testdata/captenant/provider-tenant-vs-v1.yaml", + "testdata/captenant/provider-tenant-dr-v1.yaml", + "testdata/captenant/cat-13.initial.yaml", + }, + expectedResources: "testdata/captenant/cat-13.expected.yaml", + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2811", "ERP4SMEPREPWORKAPPPLAT-3206"}, + }, + ) +} + +func TestCAPTenantUpgradeOperationCompletedPreviousVersionsLimited(t *testing.T) { + os.Setenv(v1alpha1.EnvMaxTenantVersionHistory, "3") + defer os.Unsetenv(v1alpha1.EnvMaxTenantVersionHistory) + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenant, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider"}}, + TestData{ + description: "captenant upgrade operation completed expecting limited previous versions in status to be adjusted", + initialResources: []string{ + "testdata/common/istio-ingress.yaml", + "testdata/common/capapplication.yaml", + "testdata/common/operator-gateway.yaml", + "testdata/common/capapplicationversion-v1.yaml", + "testdata/common/capapplicationversion-v2.yaml", + "testdata/captenant/provider-tenant-dnsentry.yaml", + "testdata/captenant/provider-tenant-vs-v1.yaml", + "testdata/captenant/provider-tenant-dr-v1.yaml", + "testdata/captenant/cat-29.initial.yaml", + }, + expectedResources: "testdata/captenant/cat-29.expected.yaml", + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-3206"}, + }, + ) +} + +func TestCAPTenantUpgradeRequestCompletedIncorrectVirtualServiceOwner(t *testing.T) { + err := reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenant, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider"}}, + TestData{ + description: "captenant upgrade operation completed, existing virtual service owner wrong", + initialResources: []string{ + "testdata/common/istio-ingress.yaml", + "testdata/common/capapplication.yaml", + "testdata/common/capapplicationversion-v1.yaml", + "testdata/common/capapplicationversion-v2.yaml", + "testdata/captenant/provider-tenant-dnsentry.yaml", + "testdata/captenant/cat-22.initial.yaml", + }, + expectError: true, + }, + ) + if err.Error() != "invalid owner reference found for VirtualService default.test-cap-01-provider" { + t.Error("wrong error message") + } +} + +func TestCAPTenantUpgradeRequestCompletedWithDeletionTriggered(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenant, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider"}}, + TestData{ + description: "captenant upgrade operation completed and deletion timestamp set", + initialResources: []string{ + "testdata/common/istio-ingress.yaml", + "testdata/common/capapplication.yaml", + "testdata/common/operator-gateway.yaml", + "testdata/common/capapplicationversion-v1.yaml", + "testdata/common/capapplicationversion-v2.yaml", + "testdata/captenant/provider-tenant-dnsentry.yaml", + "testdata/captenant/cat-21.initial.yaml", + }, + expectedResources: "testdata/captenant/cat-21.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ResourceCAPTenant: {{Namespace: "default", Name: "test-cap-01-provider"}}}, + }, + ) +} + +func TestCAPTenantSubdomainChange(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenant, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider"}}, + TestData{ + description: "update captenant subdomain (no existing destination rule)", + initialResources: []string{ + "testdata/common/istio-ingress.yaml", + "testdata/common/capapplication.yaml", + "testdata/common/operator-gateway.yaml", + "testdata/common/capapplicationversion-v1.yaml", + "testdata/common/capapplicationversion-v2.yaml", + "testdata/captenant/changed-provider-tenant-dnsentry.yaml", + "testdata/captenant/cat-15.initial.yaml", + }, + expectedResources: "testdata/captenant/cat-15.expected.yaml", + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2811"}, + }, + ) +} + +func TestAdjustVirtualServiceWithoutOperatorGateway(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenant, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider"}}, + TestData{ + description: "reconcile virtual service (subdomain change) when operator gateway is not ready", + initialResources: []string{ + "testdata/common/istio-ingress.yaml", + "testdata/common/capapplication.yaml", + "testdata/common/capapplicationversion-v1.yaml", + "testdata/common/capapplicationversion-v2.yaml", + "testdata/captenant/changed-provider-tenant-dnsentry.yaml", + "testdata/captenant/cat-15.initial.yaml", + }, + expectedResources: "testdata/captenant/cat-16.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ResourceCAPTenant: {{Namespace: "default", Name: "test-cap-01-provider"}}}, + }, + ) +} + +func TestCAPTenantDNSEntryModified(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenant, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider"}}, + TestData{ + description: "reconcile DNS Entry (subdomain change) update from existing DNSEntry", + initialResources: []string{ + "testdata/common/istio-ingress.yaml", + "testdata/common/operator-gateway.yaml", + "testdata/common/capapplication.yaml", + "testdata/common/capapplicationversion-v1.yaml", + "testdata/common/capapplicationversion-v2.yaml", + "testdata/captenant/to-be-updated-provider-tenant-dnsentry.yaml", + "testdata/captenant/cat-17.initial.yaml", + }, + expectedResources: "testdata/captenant/cat-17.expected.yaml", + // DeleteCollection does not work for fake test client - the common_test framework currently mocks it by matching (only) labels + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-1817", "ERP4SMEPREPWORKAPPPLAT-2707", "ERP4SMEPREPWORKAPPPLAT-2811"}, + }, + ) +} + +func TestCAPTenantDNSEntryDeletedInCluster(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenant, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider"}}, + TestData{ + description: "reconcile DNS Entry (subdomain change) when existing DNSEntry was deleted in cluster", + initialResources: []string{ + "testdata/common/istio-ingress.yaml", + "testdata/common/operator-gateway.yaml", + "testdata/common/capapplication.yaml", + "testdata/common/capapplicationversion-v1.yaml", + "testdata/common/capapplicationversion-v2.yaml", + "testdata/captenant/cat-15.initial.yaml", + }, + expectedResources: "testdata/captenant/cat-17.expected.yaml", + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-1817", "ERP4SMEPREPWORKAPPPLAT-2707"}, + }, + ) +} + +func TestCAPTenantWithUpgradeErrorSameVersion(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenant, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider"}}, + TestData{ + description: "tenant in UpgradeError state, no spec update, failed mtxrequest exists - expect no new mtxrequest", + initialResources: []string{ + "testdata/common/istio-ingress.yaml", + "testdata/common/operator-gateway.yaml", + "testdata/common/capapplication.yaml", + "testdata/common/capapplicationversion-v1.yaml", + "testdata/common/capapplicationversion-v2.yaml", + "testdata/common/capapplicationversion-v3.yaml", + "testdata/captenant/cat-23.initial.yaml", + "testdata/captenant/provider-tenant-vs-v1.yaml", + "testdata/captenant/provider-tenant-dr-v1.yaml", + }, + expectedResources: "testdata/captenant/cat-23.expected.yaml", + }, + ) +} + +func TestCAPTenantWithUpgradeErrorUpdatedVersion(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenant, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider"}}, + TestData{ + description: "tenant in UpgradeError state, spec version incremented, failed mtxrequest exists - expect new mtxrequest to be created", + initialResources: []string{ + "testdata/common/istio-ingress.yaml", + "testdata/common/capapplication.yaml", + "testdata/common/capapplicationversion-v1.yaml", + "testdata/common/capapplicationversion-v2.yaml", + "testdata/common/capapplicationversion-v3.yaml", + "testdata/captenant/cat-24.initial.yaml", + }, + expectedResources: "testdata/captenant/cat-24.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ResourceCAPTenant: {{Namespace: "default", Name: "test-cap-01-provider"}}}, + backlogItems: []string{ + "ERP4SMEPREPWORKAPPPLAT-2977", // `Ready` condition stays `True` through upgrades + }, + }, + ) +} + +func TestCAPTenantWithUpgradeErrorSameVersionMTXRequestRemoved(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenant, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider"}}, + TestData{ + description: "tenant in UpgradeError state, no spec update, failed mtxrequest removed - expect new mtxrequest", + initialResources: []string{ + "testdata/common/istio-ingress.yaml", + "testdata/common/capapplication.yaml", + "testdata/common/capapplicationversion-v1.yaml", + "testdata/common/capapplicationversion-v2.yaml", + "testdata/common/capapplicationversion-v3.yaml", + "testdata/captenant/cat-25.initial.yaml", + }, + expectedResources: "testdata/captenant/cat-25.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ResourceCAPTenant: {{Namespace: "default", Name: "test-cap-01-provider"}}}, + }, + ) +} + +func TestCAPTenantUpgradeWithoutTenantOperationWorkloadInVersion(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenant, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider"}}, + TestData{ + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2136"}, + description: "captenant start upgrade deriving TenantOperation workload from CAP workload", + initialResources: []string{ + "testdata/common/istio-ingress.yaml", + "testdata/common/capapplication.yaml", + "testdata/common/capapplicationversion-v1.yaml", + "testdata/common/capapplicationversion-v2-no-mtx-workload.yaml", + "testdata/captenant/cat-07.initial.yaml", + }, + expectedResources: "testdata/captenant/cat-27.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ResourceCAPTenant: {{Namespace: "default", Name: "test-cap-01-provider"}}}, + }, + ) +} + +func TestCAPTenantStartProvisioningWithMultipleOperationSteps(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenant, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-consumer"}}, + TestData{ + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2136"}, + description: "captenant start provisioning with version containing multiple operation steps", + initialResources: []string{ + "testdata/common/istio-ingress.yaml", + "testdata/common/capapplication.yaml", + "testdata/common/capapplicationversion-v2-multiple-tenant-ops.yaml", + "testdata/captenant/cat-28.initial.yaml", + }, + expectedResources: "testdata/captenant/cat-28.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ResourceCAPTenant: {{Namespace: "default", Name: "test-cap-01-consumer"}}}, + }, + ) +} diff --git a/internal/controller/reconcile-captenantoperation.go b/internal/controller/reconcile-captenantoperation.go new file mode 100644 index 0000000..42dce67 --- /dev/null +++ b/internal/controller/reconcile-captenantoperation.go @@ -0,0 +1,682 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +package controller + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os" + "regexp" + "strconv" + "strings" + "time" + + "github.com/sap/cap-operator/internal/util" + "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + "golang.org/x/exp/slices" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/kubernetes" + "k8s.io/klog/v2" +) + +type ProvisioningPayload struct { + SubscribedSubdomain string `json:"subscribedSubdomain"` + EventType string `json:"eventType"` +} + +type UpgradePayload struct { + Tenants []string `json:"tenants"` + AutoUnDeploy bool `json:"autoUndeploy"` +} + +type tentantOperationWorkload struct { + image string + imagePullPolicy corev1.PullPolicy + command []string + env []corev1.EnvVar + resources corev1.ResourceRequirements + securityContext *corev1.SecurityContext + podSecurityContext *corev1.PodSecurityContext + backoffLimit *int32 + ttlSecondsAfterFinished *int32 +} + +const ( + CAPTenantOperationEventInvalidReference = "InvalidReference" +) + +const ( + EnvMTXJobImage = "MTX_JOB_IMAGE" + MTXJobImageAllowedPattern = "^ghcr.io/sap/" + MTXJobImageDefault = "ghcr.io/sap/cap-operator/mtx-job" + EnvIsMTXSEnabled = "IS_MTXS_ENABLED" +) + +type cros struct { + CAPTenant *v1alpha1.CAPTenant + CAPApplication *v1alpha1.CAPApplication + CAPApplicationVersion *v1alpha1.CAPApplicationVersion +} + +const ( + CAPTenantOperationConditionReasonStepProcessing string = "StepProcessing" + CAPTenantOperationConditionReasonStepCompleted string = "StepCompleted" + CAPTenantOperationConditionReasonStepFailed string = "StepFailed" + CAPTenantOperationConditionReasonStepInitiated string = "StepInitiated" + CAPTenantOperationConditionReasonStepProcessingError string = "StepProcessingError" +) + +const ( + EventActionCreateJob = "CreateJob" + EventActionTrackJob = "TrackJob" +) + +func (c *Controller) reconcileCAPTenantOperation(ctx context.Context, item QueueItem, attempts int) (result *ReconcileResult, err error) { + // cached, err := c.crdInformerFactory.Sme().V1alpha1().CAPTenantOperations().Lister().CAPTenantOperations(item.ResourceKey.Namespace).Get(item.ResourceKey.Name) + cached, err := c.crdClient.SmeV1alpha1().CAPTenantOperations(item.ResourceKey.Namespace).Get(ctx, item.ResourceKey.Name, metav1.GetOptions{}) + + if err != nil { + return nil, handleOperatorResourceErrors(err) + } + ctop := cached.DeepCopy() + + defer func() { + if statusErr := c.updateCAPTenantOperationStatus(ctx, ctop); err == nil { + err = statusErr + } + }() + + // prepare owner refs, labels, finalizers + if update, err := c.prepareCAPTenantOperation(ctop); err != nil { + return nil, err + } else if update { + return c.updateCAPTenantOperation(ctx, ctop, true) + } + + if !isCROConditionReady(ctop.Status.GenericStatus) { + return c.reconcileTenantOperationSteps(ctx, ctop) + } else if ctop.DeletionTimestamp != nil { + return c.handleCAPTenantOperationDeletion(ctx, ctop) + } + + return result, err +} + +func (c *Controller) updateCAPTenantOperationStatus(ctx context.Context, ctop *v1alpha1.CAPTenantOperation) error { + if isDeletionImminent(&ctop.ObjectMeta) { + return nil + } + + if ctop.DeletionTimestamp != nil { + // set appropriate state for deletion + ctop.Status.State = v1alpha1.CAPTenantOperationStateDeleting + } else if ctop.Status.State == "" { + // start processing + ctop.Status.State = v1alpha1.CAPTenantOperationStateProcessing + } + + ctopUpdated, err := c.crdClient.SmeV1alpha1().CAPTenantOperations(ctop.Namespace).UpdateStatus(ctx, ctop, metav1.UpdateOptions{}) + // update reference to the resource + if ctopUpdated != nil { + *ctop = *ctopUpdated + } + return err +} + +func (c *Controller) prepareCAPTenantOperation(ctop *v1alpha1.CAPTenantOperation) (update bool, err error) { + // Do nothing when object is deleted + if ctop.DeletionTimestamp != nil { + return false, nil + } + cat, err := c.getCachedCAPTenant(ctop.Namespace, ctop.Spec.TenantId, true) + if err != nil { + msg := fmt.Sprintf("invalid %s reference", v1alpha1.CAPTenantKind) + c.Event(ctop, nil, corev1.EventTypeWarning, CAPTenantOperationEventInvalidReference, EventActionPrepare, msg) + return false, err + } + + // create owner reference - CAPTenant + if _, ok := getOwnerByKind(ctop.OwnerReferences, v1alpha1.CAPTenantKind); !ok { + ctop.OwnerReferences = append(ctop.OwnerReferences, *metav1.NewControllerRef(cat, v1alpha1.SchemeGroupVersion.WithKind(v1alpha1.CAPTenantKind))) + update = true + } + + if addCAPTenantOperationLabels(ctop, cat) { + update = true + } + + if ctop.DeletionTimestamp == nil { + // set finalizers if not added + if ctop.Finalizers == nil { + ctop.Finalizers = []string{} + } + if addFinalizer(&ctop.Finalizers, FinalizerCAPTenantOperation) { + update = true + } + } + + return +} + +func (c *Controller) getCachedCAPTenantFromOwnerReferences(refs []metav1.OwnerReference, namespace string) (*v1alpha1.CAPTenant, error) { + // get owning CAPTenant + owner, ok := getOwnerByKind(refs, v1alpha1.CAPTenantKind) + if !ok { + return nil, fmt.Errorf("could not find %s as owner reference", v1alpha1.CAPTenantKind) + } + return c.getCachedCAPTenant(namespace, owner.Name, false) +} + +func (c *Controller) updateCAPTenantOperation(ctx context.Context, ctop *v1alpha1.CAPTenantOperation, requeue bool) (result *ReconcileResult, err error) { + var ctopUpdated *v1alpha1.CAPTenantOperation + ctopUpdated, err = c.crdClient.SmeV1alpha1().CAPTenantOperations(ctop.Namespace).Update(ctx, ctop, metav1.UpdateOptions{}) + // Update reference to the resource + if ctopUpdated != nil { + *ctop = *ctopUpdated + } + if requeue { + result = NewReconcileResultWithResource(ResourceCAPTenantOperation, ctop.Name, ctop.Namespace, 1*time.Second) + } + return +} + +func (c *Controller) handleCAPTenantOperationDeletion(ctx context.Context, ctop *v1alpha1.CAPTenantOperation) (*ReconcileResult, error) { + // remove finalizer + update := removeFinalizer(&ctop.Finalizers, FinalizerCAPTenantOperation) + if update { + return c.updateCAPTenantOperation(ctx, ctop, false) + } + return nil, nil +} + +func (c *Controller) reconcileTenantOperationSteps(ctx context.Context, ctop *v1alpha1.CAPTenantOperation) (result *ReconcileResult, err error) { + /* NOTE REGARDING STEPS: + * - Initially the the first step (1) is identified and updated in the status + * - the job for the current step is created only in the next pass, which ensures that the current step in the status is always consistent + * - when the job for the current step gets completed the current step is incremented in the status (a job created in the subsequent pass) + * - the steps are sequentially executed till the end + */ + + defer func() { + if err != nil { + c.Event(ctop, nil, corev1.EventTypeWarning, CAPTenantOperationConditionReasonStepProcessingError, EventActionTrackJob, err.Error()) + } + }() + + if ctop.Status.CurrentStep == nil { // set initial step + if len(ctop.Spec.Steps) == 0 { + err = fmt.Errorf("operation steps missing in %s %s.%s", v1alpha1.CAPTenantOperationKind, ctop.Namespace, ctop.Name) + ctop.SetStatusWithReadyCondition(v1alpha1.CAPTenantOperationStateFailed, metav1.ConditionTrue, CAPTenantOperationConditionReasonStepProcessingError, err.Error()) + return + } + var initStep uint32 = 1 + ctop.SetStatusCurrentStep(&initStep, nil) + return NewReconcileResultWithResource(ResourceCAPTenantOperation, ctop.Name, ctop.Namespace, 0), nil + } + + defer func() { + if err != nil { // set step processing error in status + ctop.SetStatusWithReadyCondition(ctop.Status.State, metav1.ConditionFalse, CAPTenantOperationConditionReasonStepProcessingError, err.Error()) + } + }() + + // try to fetch active job + job, err := c.getActiveCAPTenantOperationJob(ctx, ctop) + if err != nil { + return + } + + if job == nil { // create job for step + result, err = c.initiateJobForCAPTenantOperationStep(ctx, ctop) + } else { // track the job + if ctop.Status.ActiveJob == nil || job.Name != *ctop.Status.ActiveJob { // check whether status is in sync. + ctop.SetStatusCurrentStep(ctop.Status.CurrentStep, &job.Name) + } + result = c.setCAPTenantOperationStatusFromJob(ctop, job) + } + + return +} + +func (c *Controller) getActiveCAPTenantOperationJob(ctx context.Context, ctop *v1alpha1.CAPTenantOperation) (*batchv1.Job, error) { + // NOTE: read using label selector from the api server (not the cache) + currentStep := ctop.Spec.Steps[*ctop.Status.CurrentStep-1] + labelsMap := map[string]string{ + LabelOwnerIdentifierHash: sha1Sum(ctop.Namespace, ctop.Name), + LabelTenantOperationType: string(ctop.Spec.Operation), + LabelTenantOperationStep: strconv.FormatInt(int64(*ctop.Status.CurrentStep), 10), // NOTE: step is required to read the job + LabelWorkloadType: string(currentStep.Type), // NOTE: use step type and not workload type as TenantOperation could be derived from CAP + LabelWorkloadName: currentStep.Name, + } + selector := labels.SelectorFromSet(labelsMap) + jobs, err := c.kubeClient.BatchV1().Jobs(ctop.Namespace).List(ctx, metav1.ListOptions{LabelSelector: selector.String()}) + if err != nil { + return nil, err + } + + switch len(jobs.Items) { + case 0: + return nil, nil + case 1: + return &jobs.Items[0], nil + default: + return nil, fmt.Errorf("multiple jobs exist for step %v of %s %s.%s", *ctop.Status.CurrentStep, v1alpha1.CAPTenantOperationKind, ctop.Namespace, ctop.Name) + } +} + +func (c *Controller) setCAPTenantOperationStatusFromJob(ctop *v1alpha1.CAPTenantOperation, job *batchv1.Job) (result *ReconcileResult) { + var requeueAfter time.Duration = 1 * time.Second + status := struct { + state v1alpha1.CAPTenantOperationState + conditionReason string + conditionStatus metav1.ConditionStatus + conditionMessage string + eventType string + }{} + + isFinalStep := false + if *ctop.Status.CurrentStep == uint32(len(ctop.Spec.Steps)) { + isFinalStep = true + } + + processStepCompletion := func() { + status.conditionReason = CAPTenantOperationConditionReasonStepCompleted + if isFinalStep { + status.state = v1alpha1.CAPTenantOperationStateCompleted + status.conditionStatus = metav1.ConditionTrue + ctop.SetStatusCurrentStep(nil, nil) + } else { + status.state = v1alpha1.CAPTenantOperationStateProcessing + status.conditionStatus = metav1.ConditionFalse + nxtStep := *ctop.Status.CurrentStep + 1 + ctop.SetStatusCurrentStep(&nxtStep, nil) + } + } + + jobState := getJobState(job) + switch jobState { + case JobStateComplete: + status.conditionMessage = fmt.Sprintf("step %v/%v : job %s.%s completed", *ctop.Status.CurrentStep, len(ctop.Spec.Steps), job.Namespace, job.Name) + status.eventType = corev1.EventTypeNormal + processStepCompletion() + case JobStateFailed: + status.conditionMessage = fmt.Sprintf("step %v/%v : job %s.%s failed", *ctop.Status.CurrentStep, len(ctop.Spec.Steps), job.Namespace, job.Name) + status.eventType = corev1.EventTypeWarning + if step := ctop.Spec.Steps[*ctop.Status.CurrentStep-1]; step.ContinueOnFailure { + status.conditionMessage = status.conditionMessage + "; continuing operation" + processStepCompletion() // NOTE: condition.reason needs to be set to StepCompleted in this case, as this is looked up by the CAPTenant + } else { + status.conditionReason = CAPTenantOperationConditionReasonStepFailed + status.state = v1alpha1.CAPTenantOperationStateFailed + status.conditionStatus = metav1.ConditionTrue + ctop.SetStatusCurrentStep(nil, nil) + } + case JobStateProcessing: + status.conditionReason = CAPTenantOperationConditionReasonStepProcessing + status.conditionMessage = fmt.Sprintf("step %v/%v : waiting for job %s.%s", *ctop.Status.CurrentStep, len(ctop.Spec.Steps), job.Namespace, job.Name) + status.state = v1alpha1.CAPTenantOperationStateProcessing + status.conditionStatus = metav1.ConditionFalse + requeueAfter = 15 * time.Second + } + + ctop.SetStatusWithReadyCondition(status.state, status.conditionStatus, status.conditionReason, status.conditionMessage) + if status.eventType != "" { + c.Event(ctop, job, status.eventType, status.conditionReason, EventActionTrackJob, status.conditionMessage) + } + + return NewReconcileResultWithResource(ResourceCAPTenantOperation, ctop.Name, ctop.Namespace, requeueAfter) +} + +func (c *Controller) getCAPResourcesFromCAPTenantOperation(ctx context.Context, ctop *v1alpha1.CAPTenantOperation) (*cros, error) { + // get owning CAPTenant + cat, err := c.getCachedCAPTenantFromOwnerReferences(ctop.OwnerReferences, ctop.Namespace) + if err != nil { + return nil, err + } + + // get owning CAPApplication + owner, ok := getOwnerByKind(cat.OwnerReferences, v1alpha1.CAPApplicationKind) + if !ok { + return nil, fmt.Errorf("%s could not be identified for %s %s.%s", v1alpha1.CAPApplicationKind, v1alpha1.CAPTenantOperationKind, ctop.Namespace, ctop.Name) + } + ca, err := c.getCachedCAPApplication(cat.Namespace, owner.Name) + if err != nil { + return nil, err + } + + // get specified CAPApplicationVersion + cav, err := c.crdClient.SmeV1alpha1().CAPApplicationVersions(ca.Namespace).Get(ctx, ctop.Spec.CAPApplicationVersionInstance, metav1.GetOptions{}) + if err != nil { + return nil, err + } + // verify status of CAPApplicationVersion + if !isCROConditionReady(cav.Status.GenericStatus) { + return nil, fmt.Errorf("%s %s is not %s to be used in %s %s.%s", v1alpha1.CAPApplicationVersionKind, cav.Name, v1alpha1.CAPApplicationVersionStateReady, v1alpha1.CAPTenantOperationKind, ctop.Namespace, ctop.Name) + } + + return &cros{ + CAPApplication: ca, + CAPTenant: cat, + CAPApplicationVersion: cav, + }, nil +} + +func (c *Controller) initiateJobForCAPTenantOperationStep(ctx context.Context, ctop *v1alpha1.CAPTenantOperation) (result *ReconcileResult, err error) { + relatedResources, err := c.getCAPResourcesFromCAPTenantOperation(ctx, ctop) + if err != nil { + return nil, err + } + + // get workload + step := ctop.Spec.Steps[*ctop.Status.CurrentStep-1] + workload := getWorkloadByName(step.Name, relatedResources.CAPApplicationVersion) + if workload == nil { + return nil, fmt.Errorf("could not find workload %s in %s %s.%s", step.Name, v1alpha1.CAPApplicationVersionKind, relatedResources.CAPApplicationVersion.Namespace, relatedResources.CAPApplicationVersion.Name) + } + + // create VCAP secret from consumed BTP services + consumedServiceInfos := getConsumedServiceInfos(getConsumedServiceMap(workload.ConsumedBTPServices), relatedResources.CAPApplication.Spec.BTP.Services) + vcapSecretName, err := createVCAPSecret(ctop.Name+"-"+strings.ToLower(workload.Name), ctop.Namespace, *metav1.NewControllerRef(ctop, v1alpha1.SchemeGroupVersion.WithKind(v1alpha1.CAPTenantOperationKind)), consumedServiceInfos, c.kubeClient) + if err != nil { + return nil, err + } + + annotations := copyMaps(workload.Annotations, map[string]string{ + AnnotationIstioSidecarInject: "false", + AnnotationBTPApplicationIdentifier: relatedResources.CAPApplication.Spec.GlobalAccountId + "." + relatedResources.CAPApplication.Spec.BTPAppName, + AnnotationOwnerIdentifier: ctop.Namespace + "." + ctop.Name, + }) + + labels := copyMaps(workload.Labels, map[string]string{ + App: relatedResources.CAPApplication.Spec.BTPAppName, + LabelBTPApplicationIdentifierHash: sha1Sum(relatedResources.CAPApplication.Spec.GlobalAccountId, relatedResources.CAPApplication.Spec.BTPAppName), + LabelOwnerIdentifierHash: sha1Sum(ctop.Namespace, ctop.Name), + LabelOwnerGeneration: strconv.FormatInt(ctop.Generation, 10), + LabelTenantOperationType: string(ctop.Spec.Operation), + LabelTenantOperationStep: strconv.FormatInt(int64(*ctop.Status.CurrentStep), 10), // NOTE: step is required to read the job + LabelWorkloadName: step.Name, + LabelWorkloadType: string(step.Type), // NOTE: use step type and not workload type as TenantOperation could be derived from CAP + LabelResourceCategory: CategoryWorkload, + }) + + params := &jobCreateParams{ + namePrefix: relatedResources.CAPTenant.Name + "-" + workload.Name + "-", + labels: labels, + annotations: annotations, + envFromVCAPSecret: getEnvFrom(vcapSecretName), + imagePullSecrets: convertToLocalObjectReferences(relatedResources.CAPApplicationVersion.Spec.RegistrySecrets), + version: relatedResources.CAPApplicationVersion.Spec.Version, + } + + var job *batchv1.Job + if ctop.Spec.Steps[*ctop.Status.CurrentStep-1].Type == v1alpha1.JobTenantOperation { + if params.xsuaaInstanceName, err = getXSUAAInstanceName(consumedServiceInfos, relatedResources, c.kubeClient); err != nil { + return + } + job, err = c.createTenantOperationJob(ctx, ctop, workload, params) + } else { // custom tenant operation + job, err = c.createCustomTenantOperationJob(ctx, ctop, workload, params) + } + if err != nil { + return + } + + msg := fmt.Sprintf("step %v/%v : job %s.%s created", *ctop.Status.CurrentStep, len(ctop.Spec.Steps), job.Namespace, job.Name) + ctop.SetStatusWithReadyCondition(v1alpha1.CAPTenantOperationStateProcessing, metav1.ConditionFalse, CAPTenantOperationConditionReasonStepInitiated, msg) + ctop.SetStatusCurrentStep(ctop.Status.CurrentStep, &job.Name) + c.Event(ctop, job, corev1.EventTypeNormal, CAPTenantOperationConditionReasonStepInitiated, EventActionCreateJob, msg) + + return NewReconcileResultWithResource(ResourceCAPTenantOperation, ctop.Name, ctop.Namespace, 15*time.Second), nil +} + +type jobCreateParams struct { + namePrefix string + labels map[string]string + annotations map[string]string + envFromVCAPSecret []corev1.EnvFromSource + imagePullSecrets []corev1.LocalObjectReference + version string + xsuaaInstanceName string +} + +func (c *Controller) createTenantOperationJob(ctx context.Context, ctop *v1alpha1.CAPTenantOperation, workload *v1alpha1.WorkloadDetails, params *jobCreateParams) (*batchv1.Job, error) { + // prepare payload request + var ( + payload []byte + err error + ) + if ctop.Spec.Operation == v1alpha1.CAPTenantOperationTypeProvisioning { + payload, err = json.Marshal(ProvisioningPayload{SubscribedSubdomain: ctop.Spec.SubDomain, EventType: "CREATE"}) + } else if ctop.Spec.Operation == v1alpha1.CAPTenantOperationTypeUpgrade { + payload, err = json.Marshal(UpgradePayload{Tenants: []string{ctop.Spec.TenantId}, AutoUnDeploy: true}) + } else { // deprovisioning + payload, err = json.Marshal(struct{}{}) + } + if err != nil { + return nil, err + } + + derivedWorkload := deriveWorkloadForTenantOperation(workload) + + // create job for tenant operation (provisioning / upgrade / deprovisioning) + job := &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: params.namePrefix, // generate name for each step + Namespace: ctop.Namespace, + Labels: params.labels, + Annotations: params.annotations, + OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(ctop, v1alpha1.SchemeGroupVersion.WithKind(v1alpha1.CAPTenantOperationKind))}, + }, + Spec: batchv1.JobSpec{ + BackoffLimit: derivedWorkload.backoffLimit, + TTLSecondsAfterFinished: derivedWorkload.ttlSecondsAfterFinished, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: params.labels, + Annotations: params.annotations, + }, + Spec: corev1.PodSpec{ + RestartPolicy: corev1.RestartPolicyNever, + ImagePullSecrets: params.imagePullSecrets, + Containers: getContainers(payload, ctop, derivedWorkload, workload, params), + SecurityContext: derivedWorkload.podSecurityContext, + }, + }, + }, + } + + return c.kubeClient.BatchV1().Jobs(ctop.Namespace).Create(ctx, job, metav1.CreateOptions{}) +} + +func isMTXSDisabled(envVars []corev1.EnvVar) bool { + return slices.ContainsFunc(envVars, func(env corev1.EnvVar) bool { return env.Name == EnvIsMTXSEnabled && env.Value == "false" }) +} + +func getContainers(payload []byte, ctop *v1alpha1.CAPTenantOperation, derivedWorkload tentantOperationWorkload, workload *v1alpha1.WorkloadDetails, params *jobCreateParams) []corev1.Container { + if !isMTXSDisabled(derivedWorkload.env) { + var operation string + container := &corev1.Container{ + Name: workload.Name, + Image: derivedWorkload.image, + ImagePullPolicy: derivedWorkload.imagePullPolicy, + Env: append([]corev1.EnvVar{ + {Name: EnvCAPOpAppVersion, Value: params.version}, {Name: EnvCAPOpTenantID, Value: ctop.Spec.TenantId}, {Name: EnvCAPOpTenantOperation, Value: string(ctop.Spec.Operation)}, {Name: EnvCAPOpTenantSubDomain, Value: string(ctop.Spec.SubDomain)}, + }, derivedWorkload.env...), + EnvFrom: params.envFromVCAPSecret, + Resources: derivedWorkload.resources, + SecurityContext: derivedWorkload.securityContext, + } + + if ctop.Spec.Operation == v1alpha1.CAPTenantOperationTypeProvisioning { + operation = "subscribe" + } else if ctop.Spec.Operation == v1alpha1.CAPTenantOperationTypeUpgrade { + operation = "upgrade" + } else { // deprovisioning + operation = "unsubscribe" + } + + if derivedWorkload.command != nil { + container.Command = derivedWorkload.command + } else { + container.Command = []string{"node", "./node_modules/@sap/cds-mtxs/bin/cds-mtx", operation, ctop.Spec.TenantId} + } + + return append([]corev1.Container{}, *container) + } + + return []corev1.Container{ + { + Name: "trigger", // TODO: get rid of this --> hopefully with mtxs cli where we start a single container image + Image: getMTXJobImage(), + Env: []corev1.EnvVar{ + {Name: "WAIT_FOR_SIDECAR", Value: "false"}, + {Name: "XSUAA_INSTANCE_NAME", Value: params.xsuaaInstanceName}, + {Name: "MTX_SERVICE_URL", Value: "http://localhost:" + strconv.Itoa(defaultServerPort)}, + {Name: "MTX_REQUEST_TYPE", Value: string(ctop.Spec.Operation)}, + {Name: "MTX_TENANT_ID", Value: ctop.Spec.TenantId}, + {Name: "MTX_REQUEST_PAYLOAD", Value: string(payload)}, + }, + EnvFrom: params.envFromVCAPSecret, + }, + { + Name: workload.Name, + Image: derivedWorkload.image, + ImagePullPolicy: derivedWorkload.imagePullPolicy, + Env: append([]corev1.EnvVar{ + {Name: EnvCAPOpAppVersion, Value: params.version}, {Name: EnvCAPOpTenantID, Value: ctop.Spec.TenantId}, {Name: EnvCAPOpTenantOperation, Value: string(ctop.Spec.Operation)}, {Name: EnvCAPOpTenantSubDomain, Value: string(ctop.Spec.SubDomain)}, + }, derivedWorkload.env...), + EnvFrom: params.envFromVCAPSecret, + Resources: derivedWorkload.resources, + SecurityContext: derivedWorkload.securityContext, + Command: []string{"/bin/sh", "-c"}, + Args: []string{"node ./node_modules/@sap/cds/bin/cds run & nc -lv -s localhost -p 8080"}, + }, + } +} + +func deriveWorkloadForTenantOperation(workload *v1alpha1.WorkloadDetails) tentantOperationWorkload { + result := tentantOperationWorkload{} + if workload.JobDefinition == nil { + // this must be a reference to CAP workload + result.image = workload.DeploymentDefinition.Image + result.imagePullPolicy = workload.DeploymentDefinition.ImagePullPolicy + result.env = workload.DeploymentDefinition.Env + result.resources = workload.DeploymentDefinition.Resources + result.backoffLimit = &backoffLimitValue + result.ttlSecondsAfterFinished = &tTLSecondsAfterFinishedValue + result.securityContext = workload.DeploymentDefinition.SecurityContext + result.podSecurityContext = workload.DeploymentDefinition.PodSecurityContext + } else { + // use job definition + result.image = workload.JobDefinition.Image + result.imagePullPolicy = workload.JobDefinition.ImagePullPolicy + result.command = workload.JobDefinition.Command + result.env = workload.JobDefinition.Env + result.resources = workload.JobDefinition.Resources + result.securityContext = workload.JobDefinition.SecurityContext + result.podSecurityContext = workload.JobDefinition.PodSecurityContext + if workload.JobDefinition.BackoffLimit != nil { + result.backoffLimit = workload.JobDefinition.BackoffLimit + } + if workload.JobDefinition.TTLSecondsAfterFinished != nil { + result.ttlSecondsAfterFinished = workload.JobDefinition.TTLSecondsAfterFinished + } + } + return result +} + +func (c *Controller) createCustomTenantOperationJob(ctx context.Context, ctop *v1alpha1.CAPTenantOperation, workload *v1alpha1.WorkloadDetails, params *jobCreateParams) (*batchv1.Job, error) { + // create job for custom tenant operation + job := &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: params.namePrefix, // generate name for each step + Namespace: ctop.Namespace, + Labels: params.labels, + Annotations: params.annotations, + OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(ctop, v1alpha1.SchemeGroupVersion.WithKind(v1alpha1.CAPTenantOperationKind))}, + }, + Spec: batchv1.JobSpec{ + BackoffLimit: workload.JobDefinition.BackoffLimit, + TTLSecondsAfterFinished: workload.JobDefinition.TTLSecondsAfterFinished, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: params.labels, + Annotations: params.annotations, + }, + Spec: corev1.PodSpec{ + RestartPolicy: corev1.RestartPolicyNever, + SecurityContext: workload.JobDefinition.PodSecurityContext, + ImagePullSecrets: params.imagePullSecrets, + Containers: []corev1.Container{ + { + Name: workload.Name, + Image: workload.JobDefinition.Image, + ImagePullPolicy: workload.JobDefinition.ImagePullPolicy, + Env: append([]corev1.EnvVar{ + {Name: EnvCAPOpAppVersion, Value: params.version}, {Name: EnvCAPOpTenantID, Value: ctop.Spec.TenantId}, {Name: EnvCAPOpTenantOperation, Value: string(ctop.Spec.Operation)}, {Name: EnvCAPOpTenantSubDomain, Value: string(ctop.Spec.SubDomain)}, + }, workload.JobDefinition.Env...), + EnvFrom: params.envFromVCAPSecret, + Command: workload.JobDefinition.Command, + Resources: workload.JobDefinition.Resources, + SecurityContext: workload.JobDefinition.SecurityContext, + }, + }, + }, + }, + }, + } + + return c.kubeClient.BatchV1().Jobs(ctop.Namespace).Create(ctx, job, metav1.CreateOptions{}) +} + +func getMTXJobImage() string { + mtxJobImageUri := os.Getenv(EnvMTXJobImage) + allowedUri, _ := regexp.MatchString(MTXJobImageAllowedPattern, mtxJobImageUri) + if !allowedUri { + klog.Warning("MTX Job Image URI '", mtxJobImageUri, "' not given in environment, or not allowed. Falling back to default.") + mtxJobImageUri = MTXJobImageDefault + } + + return mtxJobImageUri +} + +func getXSUAAInstanceName(consumedServiceInfos []v1alpha1.ServiceInfo, relatedResources *cros, kubeClient kubernetes.Interface) (string, error) { + info := util.GetXSUAAInfo(consumedServiceInfos, relatedResources.CAPApplication) + if info.Secret == "" { + return "", errors.New("missing XSUAA service information") + } + entry, err := util.CreateVCAPEntryFromSecret(info, relatedResources.CAPApplication.Namespace, kubeClient) + if err != nil { + return "", err + } + return entry["name"].(string), nil +} + +func addCAPTenantOperationLabels(ctop *v1alpha1.CAPTenantOperation, cat *v1alpha1.CAPTenant) (updated bool) { + appMetadata := appMetadataIdentifiers{ + ownerInfo: &ownerInfo{ + ownerNamespace: cat.Namespace, + ownerName: cat.Name, + ownerGeneration: cat.Generation, + }, + } + if updateLabelAnnotationMetadata(&ctop.ObjectMeta, &appMetadata) { + updated = true + } + + if _, ok := ctop.Labels[LabelTenantOperationType]; !ok { + ctop.Labels[LabelTenantOperationType] = string(ctop.Spec.Operation) + if ctop.Spec.Operation == v1alpha1.CAPTenantOperationTypeUpgrade { + ctop.Labels[LabelCAVVersion] = cat.Spec.Version + } + updated = true + } + return updated +} diff --git a/internal/controller/reconcile-captenantoperation_test.go b/internal/controller/reconcile-captenantoperation_test.go new file mode 100644 index 0000000..165fffe --- /dev/null +++ b/internal/controller/reconcile-captenantoperation_test.go @@ -0,0 +1,655 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +package controller + +import ( + "context" + "os" + "testing" +) + +func TestPrepareCAPTenantOperationProvisioning(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenantOperation, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider-abcd"}}, + TestData{ + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2136", "ERP4SMEPREPWORKAPPPLAT-3351"}, + description: "new captenantoperation type provisioning - prepare", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/captenant-provider-ready.yaml", + "testdata/captenantoperation/ctop-01.initial.yaml", + }, + expectedResources: "testdata/captenantoperation/ctop-01.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ + ResourceCAPTenantOperation: {{Namespace: "default", Name: "test-cap-01-provider-abcd"}}, + }, + }, + ) +} + +func TestTenantOperationInitializeStep(t *testing.T) { + _ = reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenantOperation, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider-abcd"}}, + TestData{ + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2136"}, + description: "prepared captenantoperation - initialize current step", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/captenant-provider-ready.yaml", + "testdata/captenantoperation/ctop-02.initial.yaml", + }, + expectedResources: "testdata/captenantoperation/ctop-02.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ + ResourceCAPTenantOperation: {{Namespace: "default", Name: "test-cap-01-provider-abcd"}}, + }, + }, + ) +} + +func TestTenantOperationWithNoSteps(t *testing.T) { + err := reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenantOperation, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider-abcd"}}, + TestData{ + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2136"}, + description: "prepared captenantoperation w/o valid capapplicationversion", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/captenant-provider-ready.yaml", + "testdata/captenantoperation/ctop-03.initial.yaml", + }, + expectError: true, + expectedResources: "testdata/captenantoperation/ctop-03.expected.yaml", + }, + ) + if err.Error() != "operation steps missing in CAPTenantOperation default.test-cap-01-provider-abcd" { + t.Error("unexpected error") + } +} + +func TestTenantOperationStepProcessingWithoutVersion(t *testing.T) { + err := reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenantOperation, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider-abcd"}}, + TestData{ + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2136"}, + description: "prepared captenantoperation - initialize current step", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/captenant-provider-ready.yaml", + "testdata/captenantoperation/ctop-04.initial.yaml", + }, + expectError: true, + }, + ) + if err.Error() != "capapplicationversions.sme.sap.com \"test-cap-01-cav-v1\" not found" { + t.Error("unexpected error") + } +} + +func TestProvisioningOperationTriggerStep(t *testing.T) { + _ = reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenantOperation, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider-abcd"}}, + TestData{ + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2136"}, + description: "prepared captenantoperation - initialize current step", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/captenant-provider-ready.yaml", + "testdata/common/capapplicationversion-v1.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/captenantoperation/ctop-04.initial.yaml", + }, + expectedResources: "testdata/captenantoperation/ctop-04.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ + ResourceCAPTenantOperation: {{Namespace: "default", Name: "test-cap-01-provider-abcd"}}, + }, + }, + ) +} + +func TestPrepareCAPTenantOperationUpgrade(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenantOperation, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider-abcd"}}, + TestData{ + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2136"}, + description: "new captenantoperation type upgrade - prepare", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/captenant-provider-ready.yaml", + "testdata/captenantoperation/ctop-05.initial.yaml", + }, + expectedResources: "testdata/captenantoperation/ctop-05.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ + ResourceCAPTenantOperation: {{Namespace: "default", Name: "test-cap-01-provider-abcd"}}, + }, + }, + ) +} + +func TestUpgradeOperationDeriveFromCAPWorkload(t *testing.T) { + _ = reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenantOperation, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider-abcd"}}, + TestData{ + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2136"}, + description: "prepared captenantoperation - tenant operation step deriving from cap workload", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/captenant-provider-ready.yaml", + "testdata/common/capapplicationversion-v2-no-mtx-workload.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/captenantoperation/ctop-06.initial.yaml", + }, + expectedResources: "testdata/captenantoperation/ctop-06.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ + ResourceCAPTenantOperation: {{Namespace: "default", Name: "test-cap-01-provider-abcd"}}, + }, + }, + ) +} + +func TestUpgradeOperationMultipleStepsInitiateFirstStep(t *testing.T) { + _ = reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenantOperation, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider-abcd"}}, + TestData{ + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2136", "ERP4SMEPREPWORKAPPPLAT-3226"}, + description: "prepared captenantoperation - multiple steps - start custom operation", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/captenant-provider-ready.yaml", + "testdata/common/capapplicationversion-v2-multiple-tenant-ops.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/captenantoperation/ctop-07.initial.yaml", + }, + expectedResources: "testdata/captenantoperation/ctop-07.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ + ResourceCAPTenantOperation: {{Namespace: "default", Name: "test-cap-01-provider-abcd"}}, + }, + }, + ) +} + +func TestUpgradeOperationMultipleStepsStepCompleted(t *testing.T) { + _ = reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenantOperation, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider-abcd"}}, + TestData{ + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2136", "ERP4SMEPREPWORKAPPPLAT-3226"}, + description: "prepared captenantoperation - multiple steps - custom step completed", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/captenant-provider-ready.yaml", + "testdata/common/capapplicationversion-v2-multiple-tenant-ops.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/captenantoperation/ctop-08.initial.yaml", + }, + expectedResources: "testdata/captenantoperation/ctop-08.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ + ResourceCAPTenantOperation: {{Namespace: "default", Name: "test-cap-01-provider-abcd"}}, + }, + }, + ) +} + +func TestUpgradeOperationMultipleStepsInitiateSubsequentStep(t *testing.T) { + _ = reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenantOperation, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider-abcd"}}, + TestData{ + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2136"}, + description: "prepared captenantoperation - multiple steps - initiate subsequent step", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/captenant-provider-ready.yaml", + "testdata/common/capapplicationversion-v2-multiple-tenant-ops.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/captenantoperation/ctop-09.initial.yaml", + }, + expectedResources: "testdata/captenantoperation/ctop-09.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ + ResourceCAPTenantOperation: {{Namespace: "default", Name: "test-cap-01-provider-abcd"}}, + }, + }, + ) +} + +func TestUpgradeOperationMultipleStepsStepFailure(t *testing.T) { + _ = reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenantOperation, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider-abcd"}}, + TestData{ + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2136"}, + description: "prepared captenantoperation - multiple steps - step failure", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/captenant-provider-ready.yaml", + "testdata/common/capapplicationversion-v2-multiple-tenant-ops.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/captenantoperation/ctop-10.initial.yaml", + }, + expectedResources: "testdata/captenantoperation/ctop-10.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ + ResourceCAPTenantOperation: {{Namespace: "default", Name: "test-cap-01-provider-abcd"}}, + }, + }, + ) +} + +func TestUpgradeOperationMultipleStepsFinalStepCompleted(t *testing.T) { + _ = reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenantOperation, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider-abcd"}}, + TestData{ + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2136"}, + description: "prepared captenantoperation - multiple steps - final step completed", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/captenant-provider-ready.yaml", + "testdata/common/capapplicationversion-v2-multiple-tenant-ops.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/captenantoperation/ctop-11.initial.yaml", + }, + expectedResources: "testdata/captenantoperation/ctop-11.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ + ResourceCAPTenantOperation: {{Namespace: "default", Name: "test-cap-01-provider-abcd"}}, + }, + }, + ) +} + +func TestUpgradeOperationMultipleStepsStepCompletedWithDeletion(t *testing.T) { + _ = reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenantOperation, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider-abcd"}}, + TestData{ + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2136"}, + description: "prepared captenantoperation - multiple steps - custom step completed with deletion timestamp set", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/captenant-provider-ready.yaml", + "testdata/common/capapplicationversion-v2-multiple-tenant-ops.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/captenantoperation/ctop-12.initial.yaml", + }, + expectedResources: "testdata/captenantoperation/ctop-12.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ + ResourceCAPTenantOperation: {{Namespace: "default", Name: "test-cap-01-provider-abcd"}}, + }, + }, + ) +} + +func TestUpgradeOperationMultipleStepsStepFailureWithDeletion(t *testing.T) { + _ = reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenantOperation, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider-abcd"}}, + TestData{ + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2136"}, + description: "prepared captenantoperation - multiple steps - step failure with deletion timestamp set", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/captenant-provider-ready.yaml", + "testdata/common/capapplicationversion-v2-multiple-tenant-ops.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/captenantoperation/ctop-13.initial.yaml", + }, + expectedResources: "testdata/captenantoperation/ctop-13.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ + ResourceCAPTenantOperation: {{Namespace: "default", Name: "test-cap-01-provider-abcd"}}, + }, + }, + ) +} + +func TestUpgradeOperationFailedWithDeletion(t *testing.T) { + _ = reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenantOperation, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider-abcd"}}, + TestData{ + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2136"}, + description: "prepared captenantoperation - failed condition - with deletion timestamp set", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/captenant-provider-ready.yaml", + "testdata/common/capapplicationversion-v2-multiple-tenant-ops.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/captenantoperation/ctop-14.initial.yaml", + }, + expectedResources: "testdata/captenantoperation/ctop-14.expected.yaml", + }, + ) +} + +func TestPrepareCAPTenantOperationDeprovisioningInvalidReference(t *testing.T) { + err := reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenantOperation, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-invalid-tenant-op"}}, + TestData{ + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2136"}, + description: "new captenantoperation type deprovisioning - prepare with invalid reference", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/captenant-provider-ready.yaml", + "testdata/captenantoperation/ctop-15.initial.yaml", + }, + expectedResources: "testdata/captenantoperation/ctop-15.expected.yaml", + expectError: true, + }, + ) + if err.Error() != "could not find CAPTenant with tenant id tenant-id-invalid" { + t.Errorf("unexpected error: %s", err.Error()) + } +} + +func TestTenantOperationDeprovisioningInitiateStep(t *testing.T) { + _ = reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenantOperation, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider-abcd"}}, + TestData{ + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2136"}, + description: "prepared captenantoperation type deprovisioning - initiate step processing", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/capapplicationversion-v1.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/common/captenant-provider-ready.yaml", + "testdata/captenantoperation/ctop-16.initial.yaml", + }, + expectedResources: "testdata/captenantoperation/ctop-16.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ + ResourceCAPTenantOperation: {{Namespace: "default", Name: "test-cap-01-provider-abcd"}}, + }, + }, + ) +} + +func TestTenantOperationDeprovisioningTrackStep(t *testing.T) { + _ = reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenantOperation, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider-abcd"}}, + TestData{ + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2136"}, + description: "captenantoperation type deprovisioning - track step progress", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/capapplicationversion-v1.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/common/captenant-provider-ready.yaml", + "testdata/captenantoperation/ctop-17.initial.yaml", + }, + expectedResources: "testdata/captenantoperation/ctop-17.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ + ResourceCAPTenantOperation: {{Namespace: "default", Name: "test-cap-01-provider-abcd"}}, + }, + }, + ) +} + +func TestProvisioningWithMTXSEnabled(t *testing.T) { + _ = reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenantOperation, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider-abcd"}}, + TestData{ + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2236", "ERP4SMEPREPWORKAPPPLAT-2379", "ERP4SMEPREPWORKAPPPLAT-3226", "ERP4SMEPREPWORKAPPPLAT-3807"}, + description: "Provisioning - With MTXS enabled", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/captenant-provider-ready.yaml", + "testdata/common/capapplicationversion-v1-mtxs.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/captenantoperation/ctop-18.initial.yaml", + }, + expectedResources: "testdata/captenantoperation/ctop-18.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ + ResourceCAPTenantOperation: {{Namespace: "default", Name: "test-cap-01-provider-abcd"}}, + }, + }, + ) +} + +func TestUpgradeWithMTXSEnabled(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenantOperation, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider-abcd"}}, + TestData{ + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2379", "ERP4SMEPREPWORKAPPPLAT-3226", "ERP4SMEPREPWORKAPPPLAT-3807"}, + description: "Upgrade - With MTXS enabled", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/captenant-provider-ready.yaml", + "testdata/common/capapplicationversion-v1-mtxs.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/captenantoperation/ctop-20.initial.yaml", + }, + expectedResources: "testdata/captenantoperation/ctop-20.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ + ResourceCAPTenantOperation: {{Namespace: "default", Name: "test-cap-01-provider-abcd"}}, + }, + }, + ) +} + +func TestDeprovisioningWithMTXSEnabled(t *testing.T) { + _ = reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenantOperation, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider-abcd"}}, + TestData{ + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2236", "ERP4SMEPREPWORKAPPPLAT-2379", "ERP4SMEPREPWORKAPPPLAT-3226", "ERP4SMEPREPWORKAPPPLAT-3807"}, + description: "Deprovisioning - With MTXS enabled", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/capapplicationversion-v1-mtxs.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/common/captenant-provider-ready.yaml", + "testdata/captenantoperation/ctop-19.initial.yaml", + }, + expectedResources: "testdata/captenantoperation/ctop-19.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ + ResourceCAPTenantOperation: {{Namespace: "default", Name: "test-cap-01-provider-abcd"}}, + }, + }, + ) +} + +func TestProvisioningWithMTXSEnabledAndCustomCommand(t *testing.T) { + _ = reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenantOperation, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider-abcd"}}, + TestData{ + backlogItems: []string{ + "ERP4SMEPREPWORKAPPPLAT-2747", // Custom command for Tenant Operation + "ERP4SMEPREPWORKAPPPLAT-2885", // Annotations for Tenant Operation + "ERP4SMEPREPWORKAPPPLAT-3807", // MTXS is made default + }, + description: "Provisioning - With MTXS enabled and custom command", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/captenant-provider-ready.yaml", + "testdata/common/capapplicationversion-v1-mtxs-custom-cmd.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/captenantoperation/ctop-24.initial.yaml", + }, + expectedResources: "testdata/captenantoperation/ctop-24.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ + ResourceCAPTenantOperation: {{Namespace: "default", Name: "test-cap-01-provider-abcd"}}, + }, + }, + ) +} + +func TestGetMTXJobImage(t *testing.T) { + const ( + jiraBLI = "(ERP4SMEPREPWORKAPPPLAT-1647)" + ) + + const ( + imageAllowed = "ghcr.io/sap/some-repo/another-mtx-job" + imageNotAllowed1 = "unwanted.external.repositories.cloud/cap-operator/mtx-job" + imageNotAllowed2 = "ghcr.io/sapfake/cap-operator/mtx-job" + ) + + tests := []struct { + id string + setEnvironment string + expectedImage string + }{ + { + id: "not given environment leads to default image", // which is a special case of 'not matching image pattern' + setEnvironment: "", + expectedImage: MTXJobImageDefault, + }, + { + id: "not matching image pattern leads to default image, subdomain", + setEnvironment: imageNotAllowed1, + expectedImage: MTXJobImageDefault, + }, + { + id: "not matching image pattern leads to default image, ensures .sap/", + setEnvironment: imageNotAllowed2, + expectedImage: MTXJobImageDefault, + }, + { + id: "matching image pattern is passing", + setEnvironment: imageAllowed, + expectedImage: imageAllowed, + }, + } + + activeEnvironment := os.Getenv(EnvMTXJobImage) + defer func() { + os.Setenv(EnvMTXJobImage, activeEnvironment) + }() + + for _, tt := range tests { + t.Run(tt.id+" "+jiraBLI, func(t *testing.T) { + if tt.setEnvironment == "" { + os.Unsetenv(EnvMTXJobImage) + } else { + os.Setenv(EnvMTXJobImage, tt.setEnvironment) + } + + actualImage := getMTXJobImage() + if actualImage != tt.expectedImage { + t.Errorf("expected image '%q', got '%q", tt.expectedImage, actualImage) + } + }) + } +} + +func TestProvisioningWithResources(t *testing.T) { + _ = reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenantOperation, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider-abcd"}}, + TestData{ + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2237"}, + description: "Provisioning - With Resources", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/captenant-provider-ready.yaml", + "testdata/common/capapplicationversion-v1-resources.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/captenantoperation/ctop-21.initial.yaml", + }, + expectedResources: "testdata/captenantoperation/ctop-21.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ + ResourceCAPTenantOperation: {{Namespace: "default", Name: "test-cap-01-provider-abcd"}}, + }, + }, + ) +} + +func TestProvisioningWithSecurityContext(t *testing.T) { + _ = reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenantOperation, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider-abcd"}}, + TestData{ + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2573"}, + description: "Provisioning - With securityContext for Container", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/captenant-provider-ready.yaml", + "testdata/common/capapplicationversion-v1-security-context.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/captenantoperation/ctop-22.initial.yaml", + }, + expectedResources: "testdata/captenantoperation/ctop-22.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ + ResourceCAPTenantOperation: {{Namespace: "default", Name: "test-cap-01-provider-abcd"}}, + }, + }, + ) +} + +func TestProvisioningWithPodSecurityContext(t *testing.T) { + _ = reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenantOperation, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider-abcd"}}, + TestData{ + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2573"}, + description: "Provisioning - With securityContext for Pod", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/captenant-provider-ready.yaml", + "testdata/common/capapplicationversion-v2-pod-security-context.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/captenantoperation/ctop-23.initial.yaml", + }, + expectedResources: "testdata/captenantoperation/ctop-23.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ + ResourceCAPTenantOperation: {{Namespace: "default", Name: "test-cap-01-provider-abcd"}}, + }, + }, + ) +} + +func TestProvisioningWithAnnotations(t *testing.T) { + _ = reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenantOperation, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider-abcd"}}, + TestData{ + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2885"}, + description: "Provisioning - With annotations", + initialResources: []string{ + "testdata/common/capapplication.yaml", + "testdata/common/captenant-provider-ready.yaml", + "testdata/common/capapplicationversion-v2-annotations.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/captenantoperation/ctop-25.initial.yaml", + }, + expectedResources: "testdata/captenantoperation/ctop-25.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ + ResourceCAPTenantOperation: {{Namespace: "default", Name: "test-cap-01-provider-abcd"}}, + }, + }, + ) +} + +func TestMultiXSUAAWithAnnotation(t *testing.T) { + _ = reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPTenantOperation, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-provider-abcd"}}, + TestData{ + backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-3773"}, + description: "prepared captenantoperation - tenant operation with multiple xsuaa usage", + initialResources: []string{ + "testdata/common/capapplication-multi-xsuaa.yaml", + "testdata/common/captenant-provider-ready.yaml", + "testdata/common/capapplicationversion-v2-multi-xsuaa.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/captenantoperation/ctop-26.initial.yaml", + }, + expectedResources: "testdata/captenantoperation/ctop-26.expected.yaml", + expectedRequeue: map[int][]NamespacedResourceKey{ + ResourceCAPTenantOperation: {{Namespace: "default", Name: "test-cap-01-provider-abcd"}}, + }, + }, + ) +} diff --git a/internal/controller/reconcile-domains.go b/internal/controller/reconcile-domains.go new file mode 100644 index 0000000..b90d847 --- /dev/null +++ b/internal/controller/reconcile-domains.go @@ -0,0 +1,1274 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +package controller + +import ( + "context" + "encoding/json" + "fmt" + "strconv" + "strings" + "time" + + certManagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" + certManagermetav1 "github.com/cert-manager/cert-manager/pkg/apis/meta/v1" + certv1alpha1 "github.com/gardener/cert-management/pkg/apis/cert/v1alpha1" + dnsv1alpha1 "github.com/gardener/external-dns-management/pkg/apis/dns/v1alpha1" + "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + "google.golang.org/protobuf/types/known/durationpb" + networkingv1beta1 "istio.io/api/networking/v1beta1" + istionwv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/klog/v2" +) + +// TODO: ignore duplicates reconciliation calls for same dnsTarget, Finalizers... and a whole lot more! + +const PrimaryDnsSuffix = "primary-dns" + +const ( + CAPOperator = "CAPOperator" + OperatorDomainLabel = CAPOperator + "." + OperatorDomains + OperatorDomainNamePrefix = "cap-operator-domains-" +) + +var ( + cNameLookup = int64(30) + ttl = int64(600) +) + +const ( + formatResourceState = "%s in state %s for %s %s.%s" + formatResourceStateErr = formatResourceState + ": %s" +) + +func (c *Controller) handleDomains(ctx context.Context, ca *v1alpha1.CAPApplication) (*ReconcileResult, error) { + domains, err := json.Marshal(ca.Spec.Domains) + if err != nil { + return nil, fmt.Errorf("error occurred while encoding domains to json: %w", err) + } + domainsHash := sha256Sum(string(domains)) + + requeue := NewReconcileResult() + + commonName := strings.Join([]string{"*", ca.Spec.Domains.Primary}, ".") + secretName := strings.Join([]string{strings.Join([]string{ca.Namespace, ca.Name}, "--"), SecretSuffix}, "-") + c.handlePrimaryDomainGateway(ctx, ca, secretName, ca.Namespace) + + istioIngressGatewayInfo, err := c.getIngressGatewayInfo(ctx, ca) + if err != nil { + return nil, err + } + + c.handlePrimaryDomainCertificate(ctx, ca, commonName, secretName, istioIngressGatewayInfo.Namespace) + c.handlePrimaryDomainDNSEntry(ctx, ca, commonName, ca.Namespace, istioIngressGatewayInfo.DNSTarget) + + if domainsHash != ca.Status.DomainSpecHash { + + // Reconcile Secondary domains via a dummy resource (separate reconciliation) + requeue.AddResource(ResourceOperatorDomains, "", metav1.NamespaceAll, 0) + requeue.AddResource(ResourceCAPApplication, ca.Name, ca.Namespace, 3*time.Second) // requeue CAPApplication for further processing + + // notify tenants of domain specification change (dns entries, virtual services) + cats, err := c.getRelevantTenantsForCA(ca) + if err != nil { + return nil, err + } + for _, cat := range cats { + requeue.AddResource(ResourceCAPTenant, cat.Name, cat.Namespace, 2*time.Second) + } + + ca.SetStatusWithReadyCondition(v1alpha1.CAPApplicationStateProcessing, metav1.ConditionFalse, EventActionProcessingDomainResources, "") + ca.SetStatusDomainSpecHash(domainsHash) + return requeue, nil + } + + return nil, nil +} + +func (c *Controller) handlePrimaryDomainGateway(ctx context.Context, ca *v1alpha1.CAPApplication, secretName string, namespace string) error { + gwName := getResourceName(ca.Spec.BTPAppName, GatewaySuffix) + ingressGWLabels := getIngressGatewayLabels(ca) + gwSpec := networkingv1beta1.Gateway{ + Selector: ingressGWLabels, + Servers: []*networkingv1beta1.Server{ + getGatewayServerSpec(ca.Spec.Domains.Primary, secretName), + }, + } + // Calculate sha256 sum for GW spec + hash := sha256Sum(ca.Spec.Domains.Primary, secretName, fmt.Sprintf("%v", ingressGWLabels)) + + // check for existing gateway + gw, err := c.istioInformerFactory.Networking().V1beta1().Gateways().Lister().Gateways(namespace).Get(gwName) + + // create gateway + if errors.IsNotFound(err) { + _, err = c.istioClient.NetworkingV1beta1().Gateways(namespace).Create( + ctx, &istionwv1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: gwName, + Namespace: namespace, + Annotations: map[string]string{ + AnnotationResourceHash: hash, + AnnotationBTPApplicationIdentifier: ca.Spec.GlobalAccountId + "." + ca.Spec.BTPAppName, + }, + Labels: map[string]string{ + LabelBTPApplicationIdentifierHash: sha1Sum(ca.Spec.GlobalAccountId, ca.Spec.BTPAppName), + }, + OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(ca, v1alpha1.SchemeGroupVersion.WithKind(v1alpha1.CAPApplicationKind))}, + }, + Spec: gwSpec, + }, metav1.CreateOptions{}, + ) + } else if err == nil && gw != nil && gw.Annotations[AnnotationResourceHash] != hash { + // Update the relevant gw parts, if there are changes (detected via sha256 sum) + gw.Spec = gwSpec + + // Update hash value on annotation + updateResourceAnnotation(&gw.ObjectMeta, hash) + + // Trigger the actual update on the resource + _, err = c.istioClient.NetworkingV1beta1().Gateways(namespace).Update(ctx, gw, metav1.UpdateOptions{}) + } + + if err == nil { + c.Event(ca, nil, corev1.EventTypeNormal, CAPApplicationEventPrimaryGatewayModified, EventActionProcessingDomainResources, fmt.Sprintf("primary gateway %s has been modified", gwName)) + } + return err +} + +func (c *Controller) handlePrimaryDomainCertificate(ctx context.Context, ca *v1alpha1.CAPApplication, commonName string, secretName string, istioNamespace string) error { + var err error + certName := getResourceName(ca.Spec.BTPAppName, CertificateSuffix) + // Calculate sha256 sum for Cert spec + hash := sha256Sum(commonName, secretName) + switch certificateManager() { + case certManagerGardener: + // check for existing certificate + gardenerCert, err := c.gardenerCertificateClient.CertV1alpha1().Certificates(istioNamespace).Get(context.TODO(), certName, metav1.GetOptions{}) + gardenerCertSpec := getGardenerCertificateSpec(commonName, secretName) + if errors.IsNotFound(err) { + // create certificate + _, err = c.gardenerCertificateClient.CertV1alpha1().Certificates(istioNamespace).Create( + ctx, &certv1alpha1.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + Name: certName, + Namespace: istioNamespace, + Annotations: map[string]string{ + AnnotationResourceHash: hash, + AnnotationOwnerIdentifier: KindMap[ResourceCAPApplication] + "." + ca.Namespace + "." + ca.Name, + }, + Labels: map[string]string{ + LabelOwnerIdentifierHash: sha1Sum(KindMap[ResourceCAPApplication], ca.Namespace, ca.Name), + LabelOwnerGeneration: strconv.FormatInt(ca.Generation, 10), + }, + Finalizers: []string{FinalizerCAPApplication}, + }, + Spec: gardenerCertSpec, + }, metav1.CreateOptions{}, + ) + } else if err == nil && gardenerCert != nil && gardenerCert.Annotations[AnnotationResourceHash] != hash { + // Update the certificate spec + gardenerCert.Spec = gardenerCertSpec + + // Update hash value on annotation + updateResourceAnnotation(&gardenerCert.ObjectMeta, hash) + + // Trigger the actual update on the resource + _, err = c.gardenerCertificateClient.CertV1alpha1().Certificates(istioNamespace).Update(ctx, gardenerCert, metav1.UpdateOptions{}) + } + + case certManagerCertManagerIO: + // check for existing certificate + certManagerCert, err := c.certManagerCertificateClient.CertmanagerV1().Certificates(istioNamespace).Get(context.TODO(), certName, metav1.GetOptions{}) + certManagerCertSpec := getCertManagerCertificateSpec(commonName, secretName) + + if errors.IsNotFound(err) { + // create certificate + _, err = c.certManagerCertificateClient.CertmanagerV1().Certificates(istioNamespace).Create( + ctx, &certManagerv1.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + Name: certName, + Namespace: istioNamespace, + Annotations: map[string]string{ + AnnotationResourceHash: hash, + AnnotationOwnerIdentifier: KindMap[ResourceCAPApplication] + "." + ca.Namespace + "." + ca.Name, + }, + Labels: map[string]string{ + LabelOwnerIdentifierHash: sha1Sum(KindMap[ResourceCAPApplication], ca.Namespace, ca.Name), + LabelOwnerGeneration: strconv.FormatInt(ca.Generation, 10), + }, + Finalizers: []string{FinalizerCAPApplication}, + }, + Spec: certManagerCertSpec, + }, metav1.CreateOptions{}, + ) + } else if err == nil && certManagerCert != nil && certManagerCert.Annotations[AnnotationResourceHash] != hash { + // Update the certificate spec + certManagerCert.Spec = certManagerCertSpec + + // Update hash value on annotation + updateResourceAnnotation(&certManagerCert.ObjectMeta, hash) + + // Trigger the actual update on the resource + _, err = c.certManagerCertificateClient.CertmanagerV1().Certificates(istioNamespace).Update(ctx, certManagerCert, metav1.UpdateOptions{}) + } + } + return err +} + +func (c *Controller) handlePrimaryDomainDNSEntry(ctx context.Context, ca *v1alpha1.CAPApplication, commonName string, namespace string, dnsTarget string) error { + // nothing to do here for non-gardener scenario because external-dns handles istio gateways automatically + if dnsManager() == dnsManagerGardener { + dnsEntryName := getResourceName(ca.Spec.BTPAppName, PrimaryDnsSuffix) + dnsEntrySpec := dnsv1alpha1.DNSEntrySpec{ + CNameLookupInterval: &cNameLookup, + DNSName: commonName, + Targets: []string{ + dnsTarget, + }, + TTL: &ttl, + } + // Calculate sha256 sum for DNSEntry spec + hash := sha256Sum(commonName, dnsTarget) + // check for existing DNSEntry + dnsEntry, err := c.gardenerDNSClient.DnsV1alpha1().DNSEntries(namespace).Get(context.TODO(), dnsEntryName, metav1.GetOptions{}) + + if errors.IsNotFound(err) { + // create DNSEntry + _, err = c.gardenerDNSClient.DnsV1alpha1().DNSEntries(namespace).Create( + ctx, &dnsv1alpha1.DNSEntry{ + ObjectMeta: metav1.ObjectMeta{ + Name: dnsEntryName, + Namespace: namespace, + Annotations: map[string]string{ + AnnotationResourceHash: hash, + GardenerDNSClassIdentifier: GardenerDNSClassValue, + AnnotationOwnerIdentifier: KindMap[ResourceCAPApplication] + "." + ca.Namespace + "." + ca.Name, + }, + Labels: map[string]string{ + LabelOwnerIdentifierHash: sha1Sum(KindMap[ResourceCAPApplication], ca.Namespace, ca.Name), + }, + OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(ca, v1alpha1.SchemeGroupVersion.WithKind(v1alpha1.CAPApplicationKind))}, + }, + Spec: dnsEntrySpec, + }, metav1.CreateOptions{}, + ) + } else if err == nil && dnsEntry != nil && dnsEntry.Annotations[AnnotationResourceHash] != hash { + // Update the DnsEntry spec + dnsEntry.Spec = dnsEntrySpec + + // Update hash value on annotation + updateResourceAnnotation(&dnsEntry.ObjectMeta, hash) + + // Trigger the actual update on the resource + _, err = c.gardenerDNSClient.DnsV1alpha1().DNSEntries(namespace).Update(ctx, dnsEntry, metav1.UpdateOptions{}) + } + return err + } + return nil +} + +func (c *Controller) checkPrimaryDomainResources(ctx context.Context, ca *v1alpha1.CAPApplication) (processing bool, err error) { + defer func() { + if err != nil { + // set CAPApplication status - with error + ca.SetStatusWithReadyCondition(v1alpha1.CAPApplicationStateError, metav1.ConditionFalse, "DomainResourcesError", err.Error()) + } + }() + + // check for existing gateway + _, err = c.istioClient.NetworkingV1beta1().Gateways(ca.Namespace).Get(ctx, getResourceName(ca.Spec.BTPAppName, GatewaySuffix), metav1.GetOptions{}) + if err != nil { + return false, err + } + + var istioIngressGatewayInfo *ingressGatewayInfo + istioIngressGatewayInfo, err = c.getIngressGatewayInfo(ctx, ca) + if err != nil { + return false, err + } + + certName := getResourceName(ca.Spec.BTPAppName, CertificateSuffix) + // check for certificate status + if processing, err := c.checkCertificateStatus(ctx, ca, istioIngressGatewayInfo.Namespace, certName); err != nil || processing { + return processing, err + } + + dnsEntryName := strings.Join([]string{ca.Spec.BTPAppName, PrimaryDnsSuffix}, "-") + if dnsManager() == dnsManagerGardener { + // check for existing DNSEntry + dnsEntry, err := c.gardenerDNSClient.DnsV1alpha1().DNSEntries(ca.Namespace).Get(context.TODO(), dnsEntryName, metav1.GetOptions{}) + if err != nil { + return false, err + } + + // check for ready state + if dnsEntry.Status.State == dnsv1alpha1.STATE_ERROR { + return false, fmt.Errorf(formatResourceStateErr, dnsv1alpha1.DNSEntryKind, dnsv1alpha1.STATE_ERROR, v1alpha1.CAPApplicationKind, ca.Namespace, ca.Name, *dnsEntry.Status.Message) + } else if dnsEntry.Status.State != dnsv1alpha1.STATE_READY { + klog.Infof(formatResourceState, dnsv1alpha1.DNSEntryKind, dnsEntry.Status.State, v1alpha1.CAPApplicationKind, ca.Namespace, ca.Name) + ca.SetStatusWithReadyCondition(v1alpha1.CAPApplicationStateProcessing, metav1.ConditionFalse, "DomainResourcesProcessing", "") + return true, nil + } + } + + return +} + +func (c *Controller) deletePrimaryDomainCertificate(ctx context.Context, ca *v1alpha1.CAPApplication) error { + var err error + ingressGatewayInfo, err := c.getIngressGatewayInfo(ctx, ca) + if err != nil { + return err + } + certName := getResourceName(ca.Spec.BTPAppName, CertificateSuffix) + // delete Certificate + switch certificateManager() { + case certManagerGardener: + err = c.deleteGardenerCertificate(ingressGatewayInfo, certName, ctx) + case certManagerCertManagerIO: + err = c.deleteCertManagerCertificate(ingressGatewayInfo, certName, ctx) + } + return err +} + +func (c *Controller) deleteGardenerCertificate(ingressGatewayInfo *ingressGatewayInfo, certName string, ctx context.Context) error { + certificate, err := c.gardenerCertInformerFactory.Cert().V1alpha1().Certificates().Lister().Certificates(ingressGatewayInfo.Namespace).Get(certName) + if err != nil { + return err + } + // remove Finalizer from Certificate + if removeFinalizer(&certificate.Finalizers, FinalizerCAPApplication) { + if _, err = c.gardenerCertificateClient.CertV1alpha1().Certificates(ingressGatewayInfo.Namespace).Update(ctx, certificate, metav1.UpdateOptions{}); err != nil { + return err + } + } + // delete Certificate + if err = c.gardenerCertificateClient.CertV1alpha1().Certificates(ingressGatewayInfo.Namespace).Delete(ctx, certName, metav1.DeleteOptions{}); err != nil { + return err + } + + return nil +} + +func (c *Controller) deleteCertManagerCertificate(ingressGatewayInfo *ingressGatewayInfo, certName string, ctx context.Context) error { + certificate, err := c.certManagerInformerFactory.Certmanager().V1().Certificates().Lister().Certificates(ingressGatewayInfo.Namespace).Get(certName) + if err != nil { + return err + } + // remove Finalizer from Certificate + if removeFinalizer(&certificate.Finalizers, FinalizerCAPApplication) { + if _, err = c.certManagerCertificateClient.CertmanagerV1().Certificates(ingressGatewayInfo.Namespace).Update(ctx, certificate, metav1.UpdateOptions{}); err != nil { + return err + } + } + // delete Certificate + if err = c.certManagerCertificateClient.CertmanagerV1().Certificates(ingressGatewayInfo.Namespace).Delete(ctx, certName, metav1.DeleteOptions{}); err != nil { + return err + } + + return nil +} + +func getResourceName(btpAppName string, resourceSuffix string) string { + return strings.Join([]string{btpAppName, resourceSuffix}, "-") +} + +func (c *Controller) checkCertificateStatus(ctx context.Context, ca *v1alpha1.CAPApplication, certNamespace string, certName string) (bool, error) { + switch certificateManager() { + case certManagerGardener: + // check for existing certificate + certificate, err := c.gardenerCertificateClient.CertV1alpha1().Certificates(certNamespace).Get(context.TODO(), certName, metav1.GetOptions{}) + if err != nil { + return false, err + } + + // check for ready state + if certificate.Status.State == certv1alpha1.StateError { + return false, fmt.Errorf(formatResourceStateErr, certv1alpha1.CertificateKind, certv1alpha1.StateError, v1alpha1.CAPApplicationKind, ca.Namespace, ca.Name, *certificate.Status.Message) + } else if certificate.Status.State != certv1alpha1.StateReady { + klog.Infof(formatResourceState, certv1alpha1.CertificateKind, certificate.Status.State, v1alpha1.CAPApplicationKind, ca.Namespace, ca.Name) + return true, nil + } + case certManagerCertManagerIO: + // check for existing certificate + certificate, err := c.certManagerCertificateClient.CertmanagerV1().Certificates(certNamespace).Get(context.TODO(), certName, metav1.GetOptions{}) + if err != nil { + return false, err + } + + // get ready condition + readyCond := getCertManagerReadyCondition(certificate) + // check for ready state + if readyCond == nil || readyCond.Status == certManagermetav1.ConditionUnknown { + klog.Infof(formatResourceState, certManagerv1.CertificateKind, "unknown", v1alpha1.CAPApplicationKind, ca.Namespace, ca.Name) + return true, nil + } else if readyCond.Status == certManagermetav1.ConditionFalse { + return false, fmt.Errorf(formatResourceStateErr, certManagerv1.CertificateKind, "not ready", v1alpha1.CAPApplicationKind, ca.Namespace, ca.Name, readyCond.Message) + } + } + // Cert is Ready + return false, nil +} + +func getCertManagerReadyCondition(certificate *certManagerv1.Certificate) *certManagerv1.CertificateCondition { + var readyCond *certManagerv1.CertificateCondition + for _, cond := range certificate.Status.Conditions { + if cond.Type == certManagerv1.CertificateConditionReady { + readyCond = &cond + break + } + } + return readyCond +} + +func getGatewayServerSpec(domain string, credentialName string) *networkingv1beta1.Server { + return &networkingv1beta1.Server{ + Hosts: []string{"*." + domain}, + Port: &networkingv1beta1.Port{ + Number: 443, + Protocol: "HTTPS", + Name: domain, + }, + Tls: &networkingv1beta1.ServerTLSSettings{ + CredentialName: credentialName, + Mode: networkingv1beta1.ServerTLSSettings_SIMPLE, + }, + } +} + +func getGardenerCertificateSpec(commonName string, secretName string) certv1alpha1.CertificateSpec { + return certv1alpha1.CertificateSpec{ + CommonName: &commonName, + SecretName: &secretName, + } +} + +func getCertManagerCertificateSpec(commonName string, secretName string) certManagerv1.CertificateSpec { + return certManagerv1.CertificateSpec{ + CommonName: commonName, + DNSNames: []string{commonName}, + SecretName: secretName, + IssuerRef: certManagermetav1.ObjectReference{ + // TODO: make this configurable + Kind: certManagerv1.ClusterIssuerKind, + Name: "cluster-ca", + }, + } +} + +func (c *Controller) detectTenantDNSEntryChanges(ctx context.Context, cat *v1alpha1.CAPTenant, ca *v1alpha1.CAPApplication, hash string) (bool, error) { + labelOwner := map[string]string{ + LabelOwnerIdentifierHash: sha1Sum(v1alpha1.CAPTenantKind, cat.Namespace, cat.Name), + } + dnsEntries, err := c.gardenerDNSClient.DnsV1alpha1().DNSEntries(ca.Namespace).List(ctx, metav1.ListOptions{LabelSelector: labels.SelectorFromSet(labelOwner).String()}) + if err != nil { + return false, err + } + // When no DNSEntry exists --> assume we might have to create some + if len(dnsEntries.Items) == 0 { + return true, nil + } + + // Detect changes on DNSEntry based on known mismatches (hash / length) + changeDetected := false + // length check + if len(dnsEntries.Items) != len(ca.Spec.Domains.Secondary) { + changeDetected = true + } + // hash check + if !changeDetected { + for _, dnsEntry := range dnsEntries.Items { + if dnsEntry.Annotations[AnnotationResourceHash] != hash { + changeDetected = true + break + } + } + } + // Delete all existing DNSEntries + if changeDetected { + // Delete all existing DNSEntries + err = c.gardenerDNSClient.DnsV1alpha1().DNSEntries(ca.Namespace).DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{LabelSelector: labels.SelectorFromSet(labelOwner).String()}) + if err != nil { + return false, err + } + } + + return changeDetected, nil +} + +func (c *Controller) reconcileTenantDNSEntries(ctx context.Context, cat *v1alpha1.CAPTenant) error { + if dnsManager() != dnsManagerGardener { + // Not a gardener managed cluster -> return + return nil + } + // get owning CAPApplication + ca, _ := c.getCachedCAPApplication(cat.Namespace, cat.Spec.CAPApplicationInstance) + ingressGatewayInfo, err := c.getIngressGatewayInfo(ctx, ca) + if err != nil { + return err + } + hash := sha256Sum(ingressGatewayInfo.DNSTarget, cat.Spec.SubDomain, strings.Join(ca.Spec.Domains.Secondary, "")) + changeDetected, err := c.detectTenantDNSEntryChanges(ctx, cat, ca, hash) + if err != nil || !changeDetected { + return err + } + + // Create DNS Entries + for index, domain := range ca.Spec.Domains.Secondary { + dnsEntryName := cat.Name + strconv.Itoa(index) + _, err = c.gardenerDNSClient.DnsV1alpha1().DNSEntries(ca.Namespace).Create( + ctx, &dnsv1alpha1.DNSEntry{ + ObjectMeta: metav1.ObjectMeta{ + Name: dnsEntryName, + Annotations: map[string]string{ + GardenerDNSClassIdentifier: GardenerDNSClassValue, + AnnotationResourceHash: hash, + AnnotationOwnerIdentifier: v1alpha1.CAPTenantKind + "." + cat.Namespace + "." + cat.Name, + }, + Labels: map[string]string{ + LabelOwnerIdentifierHash: sha1Sum(v1alpha1.CAPTenantKind, cat.Namespace, cat.Name), + }, + OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(cat, v1alpha1.SchemeGroupVersion.WithKind(v1alpha1.CAPTenantKind))}, + }, + Spec: dnsv1alpha1.DNSEntrySpec{ + DNSName: cat.Spec.SubDomain + "." + domain, + Targets: []string{ + ingressGatewayInfo.DNSTarget, + }, + }, + }, metav1.CreateOptions{}, + ) + // Unknown error --> break loop + if err != nil { + break + } + } + return err +} + +func (c *Controller) checkTenantDNSEntries(ctx context.Context, cat *v1alpha1.CAPTenant) (bool, error) { + // TODO: ensure that the CAPTenant is set to Ready only once all these DNSEntries are actually ready + if dnsManager() == dnsManagerGardener { + // get relevant DNSEntries + dnsEntries, err := c.gardenerDNSClient.DnsV1alpha1().DNSEntries(cat.Namespace).List(ctx, metav1.ListOptions{LabelSelector: labels.SelectorFromValidatedSet(map[string]string{LabelOwnerIdentifierHash: sha1Sum(KindMap[ResourceCAPTenant], cat.Namespace, cat.Name)}).String()}) + if err != nil { + return false, err + } + + if len(dnsEntries.Items) == 0 { + return false, fmt.Errorf("could not find dnsentries for %s %s.%s", v1alpha1.CAPTenantKind, cat.Namespace, cat.Name) + } + + for _, dnsEntry := range dnsEntries.Items { + // check for ready state + if dnsEntry.Status.State == dnsv1alpha1.STATE_ERROR { + return false, fmt.Errorf(formatResourceStateErr, dnsv1alpha1.DNSEntryKind, dnsv1alpha1.STATE_ERROR, v1alpha1.CAPTenantKind, cat.Namespace, cat.Name, *dnsEntry.Status.Message) + } else if dnsEntry.Status.State != dnsv1alpha1.STATE_READY { + klog.Infof(formatResourceState, dnsv1alpha1.DNSEntryKind, dnsEntry.Status.State, v1alpha1.CAPTenantKind, cat.Namespace, cat.Name) + return true, nil + } + } + } + // Not a gardener managed cluster -or- DNSEntries Ready -> return + return false, nil +} + +func (c *Controller) reconcileTenantNetworking(ctx context.Context, cat *v1alpha1.CAPTenant, cavName string, ca *v1alpha1.CAPApplication) (requeue *ReconcileResult, err error) { + var ( + reason, message string + drModified, vsModified bool + eventType string = corev1.EventTypeNormal + ) + + defer func() { + if err != nil { + eventType = corev1.EventTypeWarning + message = err.Error() + if _, ok := err.(*OperatorGatewayMissingError); ok { + err = nil + requeue = NewReconcileResultWithResource(ResourceCAPTenant, cat.Name, cat.Namespace, 10*time.Second) + } + } + if reason != "" { // raise event only when there is a modification or problem + c.Event(cat, nil, eventType, reason, EventActionReconcileTenantNetworking, message) + } + }() + + if drModified, err = c.reconcileTenantDestinationRule(ctx, cat, cavName, ca); err != nil { + reason = CAPTenantEventDestinationRuleModificationFailed + return + } + + if vsModified, err = c.reconcileTenantVirtualService(ctx, cat, cavName, ca); err != nil { + reason = CAPTenantEventVirtualServiceModificationFailed + return + } + + // update tenant status + if drModified || vsModified { + message = fmt.Sprintf("VirtualService (and DestinationRule) %s.%s was reconciled", cat.Namespace, cat.Name) + reason = CAPTenantEventTenantNetworkingModified + conditionStatus := metav1.ConditionFalse + if isCROConditionReady(cat.Status.GenericStatus) { + conditionStatus = metav1.ConditionTrue + } + cat.SetStatusWithReadyCondition(cat.Status.State, conditionStatus, CAPTenantEventTenantNetworkingModified, message) + } + + return +} + +func (c *Controller) reconcileTenantDestinationRule(ctx context.Context, cat *v1alpha1.CAPTenant, cavName string, ca *v1alpha1.CAPApplication) (modified bool, err error) { + var ( + create, update bool + dr *istionwv1beta1.DestinationRule + ) + dr, err = c.istioClient.NetworkingV1beta1().DestinationRules(cat.Namespace).Get(ctx, cat.Name, metav1.GetOptions{}) + if errors.IsNotFound(err) { + dr = &istionwv1beta1.DestinationRule{ + ObjectMeta: metav1.ObjectMeta{ + Name: cat.Name, // keep the same name as CAPTenant to avoid duplicates + Namespace: cat.Namespace, + Labels: map[string]string{}, + OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(cat, v1alpha1.SchemeGroupVersion.WithKind(v1alpha1.CAPTenantKind))}, + }, + } + create = true + } else if err != nil { + return + } + + if update, err = c.getUpdatedTenantDestinationRuleObject(ctx, cat, dr, cavName); err != nil { + return + } + + if create { + _, err = c.istioClient.NetworkingV1beta1().DestinationRules(cat.Namespace).Create(ctx, dr, metav1.CreateOptions{}) + } else if update { + _, err = c.istioClient.NetworkingV1beta1().DestinationRules(cat.Namespace).Update(ctx, dr, metav1.UpdateOptions{}) + } + + return create || update, err +} + +func (c *Controller) getUpdatedTenantDestinationRuleObject(ctx context.Context, cat *v1alpha1.CAPTenant, dr *istionwv1beta1.DestinationRule, cavName string) (modified bool, err error) { + // verify owner reference + modified, err = c.enforceTenantResourceOwnership(&dr.ObjectMeta, &dr.TypeMeta, cat) + if err != nil { + return modified, err + } + + routerPortInfo, err := c.getRouterServicePortInfo(cavName, cat.Namespace) + if err != nil { + return modified, err + } + + spec := &networkingv1beta1.DestinationRule{ + Host: routerPortInfo.WorkloadName + ServiceSuffix + "." + cat.Namespace + ".svc.cluster.local", + TrafficPolicy: &networkingv1beta1.TrafficPolicy{ + LoadBalancer: &networkingv1beta1.LoadBalancerSettings{ + LbPolicy: &networkingv1beta1.LoadBalancerSettings_ConsistentHash{ + ConsistentHash: &networkingv1beta1.LoadBalancerSettings_ConsistentHashLB{ + HashKey: &networkingv1beta1.LoadBalancerSettings_ConsistentHashLB_HttpCookie{ + HttpCookie: &networkingv1beta1.LoadBalancerSettings_ConsistentHashLB_HTTPCookie{ + Name: HttpCookieName, + Ttl: durationpb.New(0 * time.Second), + Path: "/", + }, + }, + }, + }, + }, + }, + } + + // check whether changes have to be applied using hash comparison + serializedSpec, err := json.Marshal(spec) + if err != nil { + return modified, fmt.Errorf("error serializing destination rule spec: %s", err.Error()) + } + hash := sha256Sum(string(serializedSpec)) + if dr.Annotations[AnnotationResourceHash] != hash { + dr.Spec = *spec + updateResourceAnnotation(&dr.ObjectMeta, hash) + modified = true + } + + return modified, err +} + +func (c *Controller) reconcileTenantVirtualService(ctx context.Context, cat *v1alpha1.CAPTenant, cavName string, ca *v1alpha1.CAPApplication) (modified bool, err error) { + var ( + create, update bool + vs *istionwv1beta1.VirtualService + ) + + vs, err = c.istioClient.NetworkingV1beta1().VirtualServices(cat.Namespace).Get(ctx, cat.Name, metav1.GetOptions{}) + if errors.IsNotFound(err) { + vs = &istionwv1beta1.VirtualService{ + ObjectMeta: metav1.ObjectMeta{ + Name: cat.Name, // keep the same name as CAPTenant to avoid duplicates + Namespace: cat.Namespace, + Labels: map[string]string{}, + OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(cat, v1alpha1.SchemeGroupVersion.WithKind(v1alpha1.CAPTenantKind))}, + }, + } + create = true + } else if err != nil { + return + } + + if update, err = c.getUpdatedTenantVirtualServiceObject(ctx, cat, vs, cavName, ca); err != nil { + return + } + + if create { + _, err = c.istioClient.NetworkingV1beta1().VirtualServices(cat.Namespace).Create(ctx, vs, metav1.CreateOptions{}) + } else if update { + _, err = c.istioClient.NetworkingV1beta1().VirtualServices(cat.Namespace).Update(ctx, vs, metav1.UpdateOptions{}) + } + + return create || update, err +} + +func (c *Controller) getUpdatedTenantVirtualServiceObject(ctx context.Context, cat *v1alpha1.CAPTenant, vs *istionwv1beta1.VirtualService, cavName string, ca *v1alpha1.CAPApplication) (modified bool, err error) { + if ca == nil { + ca, err = c.getCachedCAPApplication(cat.Namespace, cat.Spec.CAPApplicationInstance) + if err != nil { + return modified, err + } + } + + // verify owner reference + modified, err = c.enforceTenantResourceOwnership(&vs.ObjectMeta, &vs.TypeMeta, cat) + if err != nil { + return modified, err + } + + routerPortInfo, err := c.getRouterServicePortInfo(cavName, ca.Namespace) + if err != nil { + return modified, err + } + + spec := &networkingv1beta1.VirtualService{ + Gateways: []string{ca.Spec.BTPAppName + "-gw"}, + Hosts: []string{cat.Spec.SubDomain + "." + ca.Spec.Domains.Primary}, + Http: []*networkingv1beta1.HTTPRoute{{ + Match: []*networkingv1beta1.HTTPMatchRequest{ + {Uri: &networkingv1beta1.StringMatch{MatchType: &networkingv1beta1.StringMatch_Prefix{Prefix: "/"}}}, + }, + Route: []*networkingv1beta1.HTTPRouteDestination{{ + Destination: &networkingv1beta1.Destination{ + Host: routerPortInfo.WorkloadName + ServiceSuffix + "." + cat.Namespace + ".svc.cluster.local", + Port: &networkingv1beta1.PortSelector{Number: uint32(routerPortInfo.Ports[0].Port)}, + }, + Weight: 100, + }}, + }}, + } + err = c.updateTenantVirtualServiceSpecWithSecondaryDomains(ctx, spec, cat, ca) + if err != nil { + return modified, err + } + + // check whether changes have to be applied using hash comparison + serializedSpec, err := json.Marshal(spec) + if err != nil { + return modified, fmt.Errorf("error serializing virtual service spec: %s", err.Error()) + } + hash := sha256Sum(string(serializedSpec)) + if vs.Annotations[AnnotationResourceHash] != hash { + vs.Spec = *spec + updateResourceAnnotation(&vs.ObjectMeta, hash) + modified = true + } + + return modified, err +} + +type OperatorGatewayMissingError struct{} + +func (err *OperatorGatewayMissingError) Error() string { + return "operator gateway for secondary domains missing" +} + +func (c *Controller) updateTenantVirtualServiceSpecWithSecondaryDomains(ctx context.Context, spec *networkingv1beta1.VirtualService, cat *v1alpha1.CAPTenant, ca *v1alpha1.CAPApplication) error { + secondaryDomainsExist := ca.Spec.Domains.Secondary != nil && len(ca.Spec.Domains.Secondary) > 0 + if !secondaryDomainsExist { + return nil + } + + // add customer specific domains + for _, domain := range ca.Spec.Domains.Secondary { + spec.Hosts = append(spec.Hosts, cat.Spec.SubDomain+"."+domain) + } + + // Determine Ingress GW service for this app + gwInfo, err := c.getIngressGatewayInfo(ctx, ca) + if err != nil { + return err + } + + // Get the relevant central operator GW for this ingress GW + operatorGW, _ := c.getOperatorGateway(ctx, gwInfo.Namespace, sha1Sum(gwInfo.DNSTarget)) + if operatorGW == nil { + // requeue for later reconciliation + return &OperatorGatewayMissingError{} + } + spec.Gateways = append(spec.Gateways, operatorGW.Namespace+"/"+operatorGW.Name) + + return nil +} + +func getIngressGatewayLabels(ca *v1alpha1.CAPApplication) map[string]string { + ingressLabels := map[string]string{} + for _, label := range ca.Spec.Domains.IstioIngressGatewayLabels { + ingressLabels[label.Name] = label.Value + } + return ingressLabels +} + +func (c *Controller) getIngressGatewayInfo(ctx context.Context, ca *v1alpha1.CAPApplication) (ingGwInfo *ingressGatewayInfo, err error) { + // create ingress gateway selector from labels + ingressLabelSelector, err := labels.ValidatedSelectorFromSet(getIngressGatewayLabels(ca)) + if err != nil { + return nil, err + } + + defer func() { + if err != nil { + c.Event(ca, nil, corev1.EventTypeWarning, CAPApplicationEventMissingIngressGatewayInfo, EventActionProcessingDomainResources, err.Error()) + } + }() + + // Get relevant Ingress Gateway pods + ingressPods, err := c.kubeClient.CoreV1().Pods(metav1.NamespaceAll).List(ctx, metav1.ListOptions{LabelSelector: ingressLabelSelector.String()}) + if err != nil { + return nil, err + } + + // Determine relevant istio-ingressgateway namespace + namespace := "" + name := "" + // Create a dummy lookup map for determining relevant pods + relevantPodsNames := map[string]struct{}{} + for _, pod := range ingressPods.Items { + // We only support 1 ingress gateway pod namespace as of now! (Multiple pods e.g. replicas can exist in the same namespace) + if namespace == "" { + namespace = pod.Namespace + name = pod.Name + } else if namespace != pod.Namespace { + return nil, fmt.Errorf("more than one matching ingress gateway pod namespaces found for %s %s.%s", v1alpha1.CAPApplicationKind, ca.Namespace, ca.Name) + } + relevantPodsNames[pod.Name] = struct{}{} + } + if namespace == "" { + return nil, fmt.Errorf("no matching ingress gateway pod found for %s %s.%s", v1alpha1.CAPApplicationKind, ca.Namespace, ca.Name) + } + + // Get dnsTarget + // First try to use dnsTarget --> if it is set + dnsTarget := ca.Spec.Domains.DnsTarget + // Attempt to get dnsTarget from Service via annotation(s) + if dnsTarget == "" { + ingressGWSvc, err := c.getIngressGatewayService(ctx, namespace, relevantPodsNames, ca) + if err != nil { + return nil, err + } + if ingressGWSvc != nil { + dnsTarget = getDNSTarget(ingressGWSvc) + } + } + // No DNS Target --> Error + if dnsTarget == "" { + return nil, fmt.Errorf("ingress gateway service not annotated with dns target name for %s %s.%s", v1alpha1.CAPApplicationKind, ca.Namespace, ca.Name) + } + + // Return ingress Gateway info (Namespace and DNS target) + return &ingressGatewayInfo{Namespace: namespace, Name: name, DNSTarget: dnsTarget}, nil +} + +func getDNSTarget(ingressGWSvc *corev1.Service) string { + var dnsTarget string + switch dnsManager() { + case dnsManagerGardener: + dnsTarget = ingressGWSvc.Annotations[AnnotationGardenerDNSTarget] + case dnsManagerKubernetes: + dnsTarget = ingressGWSvc.Annotations[AnnotationKubernetesDNSTarget] + } + + // Use the 1st value from Comma separated values (if any) + return strings.Split(dnsTarget, ",")[0] +} + +func (c *Controller) getLoadBalancerSvcs(ctx context.Context, istioIngressGWNamespace string) ([]corev1.Service, error) { + // List all services in the same namespace as the istio-ingressgateway pod namespace + allServices, err := c.kubeClient.CoreV1().Services(istioIngressGWNamespace).List(ctx, metav1.ListOptions{}) + if err != nil { + return nil, err + } + // Filter out LoadBalancer services + loadBalancerSvcs := []corev1.Service{} + for _, svc := range allServices.Items { + if svc.Spec.Type == corev1.ServiceTypeLoadBalancer { + loadBalancerSvcs = append(loadBalancerSvcs, svc) + } + } + return loadBalancerSvcs, nil +} + +func (c *Controller) getIngressGatewayService(ctx context.Context, istioIngressGWNamespace string, relevantPodNames map[string]struct{}, ca *v1alpha1.CAPApplication) (*corev1.Service, error) { + loadBalancerSvcs, err := c.getLoadBalancerSvcs(ctx, istioIngressGWNamespace) + if err != nil { + return nil, err + } + // Get Relevant services that match the ingress gw pod via selectors + var ingressGwSvc corev1.Service + for _, svc := range loadBalancerSvcs { + // Get all matching ingress GW pods in the ingress gw namespace via ingress gw service selectors + matchedPods, err := c.kubeClient.CoreV1().Pods(istioIngressGWNamespace).List(ctx, metav1.ListOptions{LabelSelector: labels.SelectorFromValidatedSet(svc.Spec.Selector).String()}) + if err != nil { + return nil, err + } + for _, pod := range matchedPods.Items { + if _, ok := relevantPodNames[pod.Name]; ok { + if ingressGwSvc.Name == "" { + // we only expect 1 ingress gateway service in the cluster + ingressGwSvc = svc + break + } else if ingressGwSvc.Name != svc.Name { + return nil, fmt.Errorf("more than one matching ingress gateway service found for %s %s.%s", v1alpha1.CAPApplicationKind, ca.Namespace, ca.Name) + } + } + } + } + + if ingressGwSvc.Name == "" { + return nil, fmt.Errorf("unable to find a matching ingress gateway service for %s %s.%s", v1alpha1.CAPApplicationKind, ca.Namespace, ca.Name) + } + return &ingressGwSvc, nil +} + +type operatorDomainInfo struct { + Namespace string + Name string + ingressGWSelector map[string]string + dnsTarget string + Domains []string +} + +// Operator Domains is a dummy resource that is referenced by a DNSTarget (in QueuedItem) to handle "secondary" domains across all relevant CAPApplications +// TODO: ignore duplicate reconciliation calls for same dnsTarget, Finalizers... and a whole lot more! +func (c *Controller) reconcileOperatorDomains(ctx context.Context, item QueueItem, attempts int) error { + // Get Relevant Domain Infos + relevantDomainInfos, err := c.getRelevantOperatorDomainInfo(ctx) + if err != nil { + return err + } + + for dnsTargetSum, relevantDomainInfo := range relevantDomainInfos { + // When no secondary domains exists --> Cleanup and return + if len(relevantDomainInfo.Domains) == 0 { + return c.cleanUpOperatorDomains(ctx, relevantDomainInfo, dnsTargetSum) + } + + // Handle Operator Gateway + gw, err := c.handleOperatorGateway(ctx, relevantDomainInfo, dnsTargetSum) + if err != nil { + return err + } + // Handle Operator Certificate + return c.handleOperatorCertificate(ctx, gw.Name, relevantDomainInfo, dnsTargetSum) + } + return nil +} + +func (c *Controller) getRelevantOperatorDomainInfo(ctx context.Context) (map[string]*operatorDomainInfo, error) { + relevantDomainInfos := map[string]*operatorDomainInfo{} + operatorDomainGWs, err := c.istioInformerFactory.Networking().V1beta1().Gateways().Lister().Gateways(metav1.NamespaceAll).List(labels.SelectorFromValidatedSet(map[string]string{LabelOwnerIdentifierHash: sha1Sum(CAPOperator, OperatorDomains)})) + if err != nil { + return nil, err + } + // Collect existing operator gateways (without Domains) + for _, operatorDomainGW := range operatorDomainGWs { + dnsTargetSum := operatorDomainGW.Labels[LabelRelevantDNSTarget] + relevantDomainInfos[dnsTargetSum] = &operatorDomainInfo{ + Namespace: operatorDomainGW.Namespace, + Name: operatorDomainGW.Name, + ingressGWSelector: operatorDomainGW.Spec.Selector, + Domains: []string{}, + } + } + + allCAs, err := c.crdInformerFactory.Sme().V1alpha1().CAPApplications().Lister().CAPApplications(metav1.NamespaceAll).List(labels.Everything()) + if err != nil { + return nil, err + } + // Create & Update relevant operator gateways with domains + for _, ca := range allCAs { + // If no secondary domain exists for a CAPApplication --> skip + if len(ca.Spec.Domains.Secondary) == 0 { + continue + } + // Create / Update relevant operator domain info + if gwInfo, err := c.getIngressGatewayInfo(ctx, ca); err == nil { + dnsTarget := trimDNSTarget(gwInfo.DNSTarget) + + dnsTargetSum := sha1Sum(gwInfo.DNSTarget) + + if relevantDomainInfo, ok := relevantDomainInfos[dnsTargetSum]; ok { + relevantDomainInfo.Domains = append(relevantDomainInfo.Domains, ca.Spec.Domains.Secondary...) + // Fill dnsTarget + relevantDomainInfo.dnsTarget = dnsTarget + } else { + relevantDomainInfos[dnsTargetSum] = &operatorDomainInfo{ + Namespace: gwInfo.Namespace, + Name: OperatorDomainNamePrefix, + ingressGWSelector: getIngressGatewayLabels(ca), + dnsTarget: dnsTarget, + Domains: ca.Spec.Domains.Secondary, + } + } + } else { + return nil, err + } + } + + return relevantDomainInfos, nil +} + +func (c *Controller) getOperatorGateway(ctx context.Context, gwNamespace string, dnsTargetSum string) (*istionwv1beta1.Gateway, error) { + gwSelector, err := labels.ValidatedSelectorFromSet(map[string]string{ + LabelRelevantDNSTarget: dnsTargetSum, + LabelOwnerIdentifierHash: sha1Sum(CAPOperator, OperatorDomains), + }) + if err != nil { + return nil, err + } + gwList, err := c.istioInformerFactory.Networking().V1beta1().Gateways().Lister().Gateways(gwNamespace).List(gwSelector) + if err != nil { + return nil, err + } + if len(gwList) == 0 { + return nil, nil + } + return gwList[0], nil +} + +func (c *Controller) handleOperatorGateway(ctx context.Context, relevantDomainInfo *operatorDomainInfo, dnsTargetSum string) (*istionwv1beta1.Gateway, error) { + gw, err := c.getOperatorGateway(ctx, relevantDomainInfo.Namespace, dnsTargetSum) + if err != nil { + return nil, err + } + hash := sha256Sum(fmt.Sprintf("%v", relevantDomainInfo)) + // If no Gateway exists yet --> create one + if gw == nil { + gw = &istionwv1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: relevantDomainInfo.Name, + Annotations: map[string]string{ + AnnotationResourceHash: hash, + AnnotationOwnerIdentifier: OperatorDomainLabel, + }, + Labels: map[string]string{ + LabelRelevantDNSTarget: dnsTargetSum, + LabelOwnerIdentifierHash: sha1Sum(CAPOperator, OperatorDomains), + }, + }, + Spec: networkingv1beta1.Gateway{ + Selector: relevantDomainInfo.ingressGWSelector, + }, + } + c.updateServerInfo(gw, relevantDomainInfo, relevantDomainInfo.dnsTarget) + gw, err = c.istioClient.NetworkingV1beta1().Gateways(relevantDomainInfo.Namespace).Create(ctx, gw, metav1.CreateOptions{}) + } else if gw.Annotations[AnnotationResourceHash] != hash { // Check if update is needed + // Update the relevant gw parts, if there are changes (detected via sha256 sum) + gw = gw.DeepCopy() + c.updateServerInfo(gw, relevantDomainInfo, relevantDomainInfo.dnsTarget) + // Update hash value on annotation + updateResourceAnnotation(&gw.ObjectMeta, hash) + // Trigger the actual update on the resource + gw, err = c.istioClient.NetworkingV1beta1().Gateways(relevantDomainInfo.Namespace).Update(ctx, gw, metav1.UpdateOptions{}) + } + return gw, err +} + +func (c *Controller) updateServerInfo(gw *istionwv1beta1.Gateway, relevantDomainInfo *operatorDomainInfo, dnsTarget string) { + gw.Spec.Servers = []*networkingv1beta1.Server{} + for _, domain := range relevantDomainInfo.Domains { + gw.Spec.Servers = append(gw.Spec.Servers, getGatewayServerSpec(domain, dnsTarget)) + } +} + +func (c *Controller) handleOperatorCertificate(ctx context.Context, certName string, relevantDomainInfo *operatorDomainInfo, dnsTargetSum string) error { + hash := sha256Sum(fmt.Sprintf("%v", relevantDomainInfo)) + dnsTarget := trimDNSTarget(relevantDomainInfo.dnsTarget) + switch certificateManager() { + case certManagerGardener: + cert, err := c.getGardenerOperatorCertificate(ctx, relevantDomainInfo.Namespace, dnsTargetSum) + if err != nil { + return err + } + // If no certiicate exists yet --> create one + if cert == nil { + cert := &certv1alpha1.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + Name: certName, + Annotations: map[string]string{ + AnnotationResourceHash: hash, + AnnotationOwnerIdentifier: OperatorDomainLabel, + }, + Labels: map[string]string{ + LabelRelevantDNSTarget: dnsTargetSum, + LabelOwnerIdentifierHash: sha1Sum(CAPOperator, OperatorDomains), + }, + }, + Spec: getGardenerCertificateSpec(dnsTarget, dnsTarget), + } + cert.Spec.DNSNames = getCertificateDNSNames(relevantDomainInfo) + _, err = c.gardenerCertificateClient.CertV1alpha1().Certificates(relevantDomainInfo.Namespace).Create(ctx, cert, metav1.CreateOptions{}) + } else if cert.Annotations[AnnotationResourceHash] != hash { + // Update the relevant certificate parts, if there are changes (detected via sha256 sum) + cert = cert.DeepCopy() + cert.Spec.DNSNames = getCertificateDNSNames(relevantDomainInfo) + // Update hash value on annotation + updateResourceAnnotation(&cert.ObjectMeta, hash) + // Trigger the actual update on the resource + _, err = c.gardenerCertificateClient.CertV1alpha1().Certificates(relevantDomainInfo.Namespace).Update(ctx, cert, metav1.UpdateOptions{}) + } + return err + case certManagerCertManagerIO: + cert, err := c.getCertManagerOperatorCertificate(ctx, relevantDomainInfo.Namespace, dnsTargetSum) + if err != nil { + return err + } + // If no certiicate exists yet --> create one + if cert == nil { + cert := &certManagerv1.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + Name: certName, + Annotations: map[string]string{ + AnnotationResourceHash: hash, + AnnotationOwnerIdentifier: OperatorDomainLabel, + }, + Labels: map[string]string{ + LabelRelevantDNSTarget: dnsTargetSum, + LabelOwnerIdentifierHash: sha1Sum(CAPOperator, OperatorDomains), + }, + }, + Spec: getCertManagerCertificateSpec("*."+dnsTarget, dnsTarget), + } + cert.Spec.DNSNames = getCertificateDNSNames(relevantDomainInfo) + _, err = c.certManagerCertificateClient.CertmanagerV1().Certificates(relevantDomainInfo.Namespace).Create(ctx, cert, metav1.CreateOptions{}) + } else if cert.Annotations[AnnotationResourceHash] != hash { + // Update the relevant certificate parts, if there are changes (detected via sha256 sum) + cert = cert.DeepCopy() + cert.Spec.DNSNames = getCertificateDNSNames(relevantDomainInfo) + // Update hash value on annotation + updateResourceAnnotation(&cert.ObjectMeta, hash) + // Trigger the actual update on the resource + _, err = c.certManagerCertificateClient.CertmanagerV1().Certificates(relevantDomainInfo.Namespace).Update(ctx, cert, metav1.UpdateOptions{}) + } + return err + } + return nil +} + +func (c *Controller) getGardenerOperatorCertificate(ctx context.Context, gwNamespace string, dnsTargetSum string) (*certv1alpha1.Certificate, error) { + certSelector, err := labels.ValidatedSelectorFromSet(map[string]string{ + LabelRelevantDNSTarget: dnsTargetSum, + LabelOwnerIdentifierHash: sha1Sum(CAPOperator, OperatorDomains), + }) + if err != nil { + return nil, err + } + + certList, err := c.gardenerCertInformerFactory.Cert().V1alpha1().Certificates().Lister().Certificates(gwNamespace).List(certSelector) + if err != nil { + return nil, err + } + + if len(certList) == 0 { + return nil, nil + } + return certList[0], nil +} + +func (c *Controller) getCertManagerOperatorCertificate(ctx context.Context, gwNamespace string, dnsTargetSum string) (*certManagerv1.Certificate, error) { + certSelector, err := labels.ValidatedSelectorFromSet(map[string]string{ + LabelRelevantDNSTarget: dnsTargetSum, + LabelOwnerIdentifierHash: sha1Sum(CAPOperator, OperatorDomains), + }) + if err != nil { + return nil, err + } + + certList, err := c.certManagerInformerFactory.Certmanager().V1().Certificates().Lister().Certificates(gwNamespace).List(certSelector) + if err != nil { + return nil, err + } + + if len(certList) == 0 { + return nil, nil + } + return certList[0], nil +} + +func (c *Controller) cleanUpOperatorDomains(ctx context.Context, relevantDomainInfo *operatorDomainInfo, dnsTargetSum string) error { + // Delete Operator Gateway (if any) + gw, err := c.getOperatorGateway(ctx, relevantDomainInfo.Namespace, dnsTargetSum) + if err != nil { + return err + } + if gw != nil { + err := c.istioClient.NetworkingV1beta1().Gateways(relevantDomainInfo.Namespace).Delete(ctx, gw.Name, metav1.DeleteOptions{}) + if err != nil { + return err + } + } + + // Delete Operator certificate (if any) + switch certificateManager() { + case certManagerGardener: + cert, err := c.getGardenerOperatorCertificate(ctx, relevantDomainInfo.Namespace, dnsTargetSum) + if err != nil { + return err + } + if cert != nil { + return c.gardenerCertificateClient.CertV1alpha1().Certificates(relevantDomainInfo.Namespace).Delete(ctx, cert.Name, metav1.DeleteOptions{}) + } + case certManagerCertManagerIO: + cert, err := c.getCertManagerOperatorCertificate(ctx, relevantDomainInfo.Namespace, dnsTargetSum) + if err != nil { + return err + } + if cert != nil { + return c.certManagerCertificateClient.CertmanagerV1().Certificates(relevantDomainInfo.Namespace).Delete(ctx, cert.Name, metav1.DeleteOptions{}) + } + } + return nil +} + +func getCertificateDNSNames(relevantDomainInfo *operatorDomainInfo) []string { + dnsNames := []string{} + for _, domain := range relevantDomainInfo.Domains { + dnsNames = append(dnsNames, "*."+domain) + } + return dnsNames +} + +func trimDNSTarget(dnsTarget string) string { + // Trim dnsTarget to under 64 chars --> TODO: Also handle this in webhook/crd spec + for len(dnsTarget) > 64 { + dnsTarget = dnsTarget[strings.Index(dnsTarget, ".")+1:] + } + // Fix for domain gw/creds secret name (Replace *.domain with x.domain for secret name) + return strings.ReplaceAll(dnsTarget, "*", "x") +} diff --git a/internal/controller/reconcile-domains_test.go b/internal/controller/reconcile-domains_test.go new file mode 100644 index 0000000..aaa295c --- /dev/null +++ b/internal/controller/reconcile-domains_test.go @@ -0,0 +1,284 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +package controller + +import ( + "context" + "os" + "testing" + + certManagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" + certv1alpha1 "github.com/gardener/cert-management/pkg/apis/cert/v1alpha1" + "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + istionwv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" +) + +func TestController_reconcileOperatorDomains(t *testing.T) { + tests := []struct { + name string + createCA bool + createCA2 bool + updateCA bool + createIngress bool + cleanUpDomains bool + wantErr bool + expectDomainResources bool + enableCertManagerEnv bool + }{ + { + name: "Test without CAPApplication", + wantErr: false, + expectDomainResources: false, + }, + { + name: "Test with CAPApplication but without Ingress GW", + createCA: true, + wantErr: true, + expectDomainResources: false, + }, + { + name: "Test with CAPApplication and Ingress GW", + createCA: true, + createIngress: true, + wantErr: false, + expectDomainResources: true, + }, + { + name: "Test cleanup after creation", + createCA: true, + createIngress: true, + wantErr: false, + cleanUpDomains: true, + expectDomainResources: false, + }, + { + name: "Test with multiple CAPApplications and Ingress GW", + createCA: true, + createCA2: true, + createIngress: true, + wantErr: false, + expectDomainResources: true, + }, + // { + // name: "Test cleanup with multiple CAPApplications and Ingress GW", + // createCA: true, + // createCA2: true, + // createIngress: true, + // cleanUpDomains: true, + // wantErr: false, + // expectDomainResources: true, + // }, + { + name: "Test update with CAPApplication and Ingress GW", + createCA: true, + updateCA: true, + createIngress: true, + wantErr: false, + expectDomainResources: true, + }, + { + name: "Test with CAPApplication and Ingress GW (enableCertManagerEnv)", + createCA: true, + createIngress: true, + enableCertManagerEnv: true, + wantErr: false, + expectDomainResources: true, + }, + { + name: "Test cleanup after creation (enableCertManagerEnv)", + createCA: true, + createIngress: true, + enableCertManagerEnv: true, + wantErr: false, + cleanUpDomains: true, + expectDomainResources: false, + }, + { + name: "Test with multiple CAPApplications and Ingress GW (enableCertManagerEnv)", + createCA: true, + createCA2: true, + createIngress: true, + enableCertManagerEnv: true, + wantErr: false, + expectDomainResources: true, + }, + // { + // name: "Test cleanup with multiple CAPApplications and Ingress GW (enableCertManagerEnv)", + // createCA: true, + // createCA2: true, + // createIngress: true, + // cleanUpDomains: true, + // enableCertManagerEnv: true, + // wantErr: false, + // expectDomainResources: true, + // }, + { + name: "Test update with CAPApplication and Ingress GW (enableCertManagerEnv)", + createCA: true, + updateCA: true, + createIngress: true, + enableCertManagerEnv: true, + wantErr: false, + expectDomainResources: true, + }, + } + defer os.Setenv(certManagerEnv, "") + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.enableCertManagerEnv { + os.Setenv(certManagerEnv, certManagerCertManagerIO) + } else { + os.Setenv(certManagerEnv, certManagerGardener) + } + var c *Controller + var ca *v1alpha1.CAPApplication + var ca2 *v1alpha1.CAPApplication + var ingressRes *ingressResources + if tt.createCA { + ca = createCaCRO(caCroName, false) + if tt.createCA2 { + ca2 = ca.DeepCopy() + ca2.Name += "2" + ca2.Spec.Domains.Secondary = []string{"2" + secondaryDomain, "3" + secondaryDomain} + } + } + + if tt.createIngress { + ingressRes = createIngressResource(ingressGWName, ca, dnsTarget) + } + + c = getTestController(testResources{ + cas: []*v1alpha1.CAPApplication{ca, ca2}, + ingressGW: []*ingressResources{ingressRes}, + }) + + q := QueueItem{ + Key: ResourceOperatorDomains, + ResourceKey: NamespacedResourceKey{ + Namespace: metav1.NamespaceAll, + Name: "", + }, + } + err := c.reconcileOperatorDomains(context.TODO(), q, 0) + if (err != nil) != tt.wantErr { + t.Errorf("Controller.reconcileOperatorDomains() error = %v, wantErr %v", err, tt.wantErr) + } + + if tt.updateCA { + var gw *istionwv1beta1.Gateway + listGWs, _ := c.istioClient.NetworkingV1beta1().Gateways(metav1.NamespaceAll).List(context.TODO(), metav1.ListOptions{LabelSelector: labels.SelectorFromValidatedSet(map[string]string{LabelOwnerIdentifierHash: sha1Sum(CAPOperator, OperatorDomains)}).String()}) + if len(listGWs.Items) > 0 { + gw = listGWs.Items[0] + generateMetaObjName(gw) + } + var gardenerCert *certv1alpha1.Certificate + var certManagerCert *certManagerv1.Certificate + if tt.enableCertManagerEnv { + certManagerCertList, _ := c.certManagerCertificateClient.CertmanagerV1().Certificates(metav1.NamespaceAll).List(context.TODO(), metav1.ListOptions{LabelSelector: labels.SelectorFromValidatedSet(map[string]string{LabelOwnerIdentifierHash: sha1Sum(CAPOperator, OperatorDomains)}).String()}) + if len(certManagerCertList.Items) > 0 { + certManagerCert = &certManagerCertList.Items[0] + certManagerCert.Name = gw.Name + } + } else { + gardenerCertList, _ := c.gardenerCertificateClient.CertV1alpha1().Certificates(metav1.NamespaceAll).List(context.TODO(), metav1.ListOptions{LabelSelector: labels.SelectorFromValidatedSet(map[string]string{LabelOwnerIdentifierHash: sha1Sum(CAPOperator, OperatorDomains)}).String()}) + if len(gardenerCertList.Items) > 0 { + gardenerCert = &gardenerCertList.Items[0] + gardenerCert.Name = gw.Name + } + } + ca.Spec.Domains.Secondary = []string{"2" + secondaryDomain, "3" + secondaryDomain} + c = getTestController(testResources{ + cas: []*v1alpha1.CAPApplication{ca, ca2}, + gateway: gw, + gardenerCert: gardenerCert, + certManagerCert: certManagerCert, + ingressGW: []*ingressResources{ingressRes}, + }) + err = c.reconcileOperatorDomains(context.TODO(), q, 0) + if (err != nil) != tt.wantErr { + t.Errorf("Controller.reconcileOperatorDomains() error = %v, wantErr %v", err, tt.wantErr) + } + } + + if tt.cleanUpDomains { + var gw *istionwv1beta1.Gateway + var ingressGW2 *ingressResources + listGWs, _ := c.istioClient.NetworkingV1beta1().Gateways(metav1.NamespaceAll).List(context.TODO(), metav1.ListOptions{LabelSelector: labels.SelectorFromValidatedSet(map[string]string{LabelOwnerIdentifierHash: sha1Sum(CAPOperator, OperatorDomains)}).String()}) + if len(listGWs.Items) > 0 { + gw = listGWs.Items[0] + generateMetaObjName(gw) + } + var gardenerCert *certv1alpha1.Certificate + var certManagerCert *certManagerv1.Certificate + if tt.enableCertManagerEnv { + certManagerCertList, _ := c.certManagerCertificateClient.CertmanagerV1().Certificates(metav1.NamespaceAll).List(context.TODO(), metav1.ListOptions{LabelSelector: labels.SelectorFromValidatedSet(map[string]string{LabelOwnerIdentifierHash: sha1Sum(CAPOperator, OperatorDomains)}).String()}) + if len(certManagerCertList.Items) > 0 { + certManagerCert = &certManagerCertList.Items[0] + certManagerCert.Name = gw.Name + } + } else { + gardenerCertList, _ := c.gardenerCertificateClient.CertV1alpha1().Certificates(metav1.NamespaceAll).List(context.TODO(), metav1.ListOptions{LabelSelector: labels.SelectorFromValidatedSet(map[string]string{LabelOwnerIdentifierHash: sha1Sum(CAPOperator, OperatorDomains)}).String()}) + if len(gardenerCertList.Items) > 0 { + gardenerCert = &gardenerCertList.Items[0] + gardenerCert.Name = gw.Name + } + } + ca.Spec.Domains.Secondary = []string{} + if tt.createCA2 { + ca2.Spec.Domains.IstioIngressGatewayLabels[0].Value += "2" + ca2.Spec.Domains.IstioIngressGatewayLabels[1].Value += "2" + ingressGW2 = createIngressResource(ingressGWName+"2", ca2, "Something.that.surely.exceeds.the.64char.limit."+dnsTarget) + } + c = getTestController(testResources{ + cas: []*v1alpha1.CAPApplication{ca, ca2}, + gateway: gw, + gardenerCert: gardenerCert, + certManagerCert: certManagerCert, + ingressGW: []*ingressResources{ingressRes, ingressGW2}, + }) + err = c.reconcileOperatorDomains(context.TODO(), q, 0) + if (err != nil) != tt.wantErr { + t.Errorf("Controller.reconcileOperatorDomains() error = %v, wantErr %v", err, tt.wantErr) + } + } + + var gw *istionwv1beta1.Gateway + listGWs, _ := c.istioClient.NetworkingV1beta1().Gateways(metav1.NamespaceAll).List(context.TODO(), metav1.ListOptions{LabelSelector: labels.SelectorFromValidatedSet(map[string]string{LabelOwnerIdentifierHash: sha1Sum(CAPOperator, OperatorDomains)}).String()}) + if len(listGWs.Items) > 0 { + gw = listGWs.Items[0] + } + var cert interface{} + if tt.enableCertManagerEnv { + certManagerCertList, _ := c.certManagerCertificateClient.CertmanagerV1().Certificates(metav1.NamespaceAll).List(context.TODO(), metav1.ListOptions{LabelSelector: labels.SelectorFromValidatedSet(map[string]string{LabelOwnerIdentifierHash: sha1Sum(CAPOperator, OperatorDomains)}).String()}) + if len(certManagerCertList.Items) > 0 { + cert = &certManagerCertList.Items[0] + } + } else { + gardenerCertList, _ := c.gardenerCertificateClient.CertV1alpha1().Certificates(metav1.NamespaceAll).List(context.TODO(), metav1.ListOptions{LabelSelector: labels.SelectorFromValidatedSet(map[string]string{LabelOwnerIdentifierHash: sha1Sum(CAPOperator, OperatorDomains)}).String()}) + if len(gardenerCertList.Items) > 0 { + cert = &gardenerCertList.Items[0] + } + } + + if tt.expectDomainResources { + if gw == nil { + t.Errorf("Controller.reconcileOperatorDomains() error = Expected OperatorDomain Gateway missing") + } + if cert == nil { + t.Errorf("Controller.reconcileOperatorDomains() error = Expected OperatorDomain Certificate missing") + } + } else { + if gw != nil { + t.Errorf("Controller.reconcileOperatorDomains() error = Unexpected OperatorDomain Gateway: %v", gw) + } + if cert != nil { + t.Errorf("Controller.reconcileOperatorDomains() error = Unexpected OperatorDomain Certificate: %v", cert) + } + } + }) + } +} diff --git a/internal/controller/reconcile.go b/internal/controller/reconcile.go new file mode 100644 index 0000000..d7999f6 --- /dev/null +++ b/internal/controller/reconcile.go @@ -0,0 +1,523 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +package controller + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/sap/cap-operator/internal/util" + "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + "golang.org/x/mod/semver" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes" + "k8s.io/klog/v2" +) + +const ( + LabelOwnerIdentifierHash = "sme.sap.com/owner-identifier-hash" + LabelOwnerGeneration = "sme.sap.com/owner-generation" + LabelWorkloadName = "sme.sap.com/workload-name" + LabelWorkloadType = "sme.sap.com/workload-type" + LabelResourceCategory = "sme.sap.com/category" + LabelBTPApplicationIdentifierHash = "sme.sap.com/btp-app-identifier-hash" + LabelTenantType = "sme.sap.com/tenant-type" + LabelTenantId = "sme.sap.com/btp-tenant-id" + LabelTenantOperationType = "sme.sap.com/tenant-operation-type" + LabelTenantOperationStep = "sme.sap.com/tenant-operation-step" + LabelCAVVersion = "sme.sap.com/cav-version" + LabelRelevantDNSTarget = "sme.sap.com/relevant-dns-target-hash" + LabelDisableKarydia = "x4.sap.com/disable-karydia" + AnnotationOwnerIdentifier = "sme.sap.com/owner-identifier" + AnnotationBTPApplicationIdentifier = "sme.sap.com/btp-app-identifier" + AnnotationResourceHash = "sme.sap.com/resource-hash" + AnnotationControllerClass = "sme.sap.com/controller-class" + AnnotationIstioSidecarInject = "sidecar.istio.io/inject" + AnnotationGardenerDNSTarget = "dns.gardener.cloud/dnsnames" + AnnotationKubernetesDNSTarget = "external-dns.alpha.kubernetes.io/hostname" + FinalizerCAPApplication = "sme.sap.com/capapplication" + FinalizerCAPApplicationVersion = "sme.sap.com/capapplicationversion" + FinalizerCAPTenant = "sme.sap.com/captenant" + FinalizerCAPTenantOperation = "sme.sap.com/captenantoperation" + GardenerDNSClassIdentifier = "dns.gardener.cloud/class" +) + +const ( + CertificateSuffix = "certificate" + GardenerDNSClassValue = "garden" + GatewaySuffix = "gw" + IstioSystemNamespace = "istio-system" + SecretSuffix = "secret" +) + +var ( + backoffLimitValue int32 = 2 + tTLSecondsAfterFinishedValue int32 = 300 +) + +const ( + ProviderTenantType = "provider" + ConsumerTenantType = "consumer" +) + +// Use same name as default cookie from approuter used for session stickiness +const HttpCookieName = "JSESSIONID" + +const ( + EnvCAPOpAppVersion = "CAPOP_APP_VERSION" + EnvCAPOpTenantID = "CAPOP_TENANT_ID" + EnvCAPOpTenantSubDomain = "CAPOP_TENANT_SUBDOMAIN" + EnvCAPOpTenantOperation = "CAPOP_TENANT_OPERATION" + EnvVCAPServices = "VCAP_SERVICES" +) + +type JobState string + +const ( + JobStateComplete JobState = "Complete" + JobStateFailed JobState = "Failed" + JobStateProcessing JobState = "Processing" +) + +type ingressGatewayInfo struct { + Namespace string + Name string + DNSTarget string +} + +type servicePortInfo struct { + WorkloadName string + DeploymentType string + Ports []corev1.ServicePort + ClusterPorts []int32 + Destinations []destinationInfo +} + +type destinationInfo struct { + DestinationName string + Port int32 +} + +const ( + ServiceSuffix = "-svc" +) + +var restrictedEnvNames = map[string]struct{}{ + EnvCAPOpAppVersion: {}, + EnvVCAPServices: {}, +} + +// See https://www.npmjs.com//* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +package/@sap/approuter#destinations +type RouterDestination struct { + Name string `json:"name"` + URL string `json:"url"` + ProxyHost string `json:"proxyHost,omitempty"` + ProxyPort string `json:"proxyPort,omitempty"` + ForwardAuthToken bool `json:"forwardAuthToken,omitempty"` + StrictSSL bool `json:"strictSSL,omitempty"` + Timeout *int64 `json:"timeout,omitempty"` + SetXForwardedHeaders bool `json:"setXForwardedHeaders,omitempty"` + ProxyType string `json:"proxyType,omitempty"` +} + +func (c *Controller) Event(main runtime.Object, related runtime.Object, eventType, reason, action, message string) { + defer func() { + // do not let the routine dump due to event recording errors + if r := recover(); r != nil { + klog.Errorf("error when recording event: ", r) + } + }() + c.eventRecorder.Eventf(main, related, eventType, reason, action, message) +} + +func (c *Controller) getCachedCAPApplication(namespace string, name string) (*v1alpha1.CAPApplication, error) { + lister := c.crdInformerFactory.Sme().V1alpha1().CAPApplications().Lister() + return lister.CAPApplications(namespace).Get(name) +} + +func (c *Controller) getCachedCAPTenant(namespace string, value string, valueIsTenantId bool) (*v1alpha1.CAPTenant, error) { + lister := c.crdInformerFactory.Sme().V1alpha1().CAPTenants().Lister() + if !valueIsTenantId { + // fetch with name + return lister.CAPTenants(namespace).Get(value) + } + + // fetch with label selector + set := map[string]string{LabelTenantId: value} + selector, err := labels.ValidatedSelectorFromSet(set) + if err != nil { + return nil, err + } + + cats, err := lister.CAPTenants(namespace).List(selector) + if err != nil { + return nil, err + } + if len(cats) == 0 { + return nil, fmt.Errorf("could not find CAPTenant with tenant id %s", value) + } + return cats[0], nil // expect only one matching tenant +} + +/* +fetch the latest CAPApplicationVersion in Ready state, for a specified CAPApplication +*/ +func (c *Controller) getLatestReadyCAPApplicationVersion(ctx context.Context, ca *v1alpha1.CAPApplication, avoidNotFound bool) (*v1alpha1.CAPApplicationVersion, error) { + cavs, err := c.getCachedCAPApplicationVersions(ctx, ca) + if err != nil { + return nil, err + } + + var latestCav *v1alpha1.CAPApplicationVersion + for _, cav := range cavs { + // determine the latest semantic version + if isCROConditionReady(cav.Status.GenericStatus) && + (latestCav == nil || semver.Compare("v"+cav.Spec.Version, "v"+latestCav.Spec.Version) == 1) { + latestCav = cav + } + } + + if latestCav == nil && !avoidNotFound { + err = fmt.Errorf("could not find a %s with status %s for %s %s.%s", v1alpha1.CAPApplicationVersionKind, v1alpha1.CAPApplicationVersionStateReady, v1alpha1.CAPApplicationKind, ca.Namespace, ca.Name) + } + + return latestCav, err +} + +/* +fetch the latest CAPApplicationVersion, for a specified CAPApplication +*/ +func (c *Controller) getLatestCAPApplicationVersion(ctx context.Context, ca *v1alpha1.CAPApplication) (*v1alpha1.CAPApplicationVersion, error) { + cavs, err := c.getCachedCAPApplicationVersions(ctx, ca) + if err != nil { + return nil, err + } + + var latestCav *v1alpha1.CAPApplicationVersion + for _, cav := range cavs { + // determine the latest semantic version + if latestCav == nil || semver.Compare("v"+cav.Spec.Version, "v"+latestCav.Spec.Version) == 1 { + latestCav = cav + } + } + + if latestCav == nil { + err = fmt.Errorf("could not find a %s for %s %s.%s", v1alpha1.CAPApplicationVersionKind, v1alpha1.CAPApplicationKind, ca.Namespace, ca.Name) + } + + return latestCav, err +} + +/* +* + + fetch the relevant CAPApplicationVersion in Ready state, for a specified CAPApplication and version string +*/ +func (c *Controller) getRelevantCAPApplicationVersion(ctx context.Context, ca *v1alpha1.CAPApplication, version string) (*v1alpha1.CAPApplicationVersion, error) { + cavs, err := c.getCachedCAPApplicationVersions(ctx, ca) + if err != nil { + return nil, err + } + + var latestCav *v1alpha1.CAPApplicationVersion + for _, cav := range cavs { + // determine the matching semantic version and return + if isCROConditionReady(cav.Status.GenericStatus) && cav.Spec.Version == version { + latestCav = cav + break + } + } + + if latestCav == nil { + err = fmt.Errorf("could not find a %s with status %s for %s %s.%s and version %s", v1alpha1.CAPApplicationVersionKind, v1alpha1.CAPApplicationVersionStateReady, v1alpha1.CAPApplicationKind, ca.Namespace, ca.Name, version) + } + + return latestCav, err +} + +func (c *Controller) getCachedCAPApplicationVersions(ctx context.Context, ca *v1alpha1.CAPApplication) ([]*v1alpha1.CAPApplicationVersion, error) { + selector, err := labels.ValidatedSelectorFromSet(map[string]string{ + LabelOwnerIdentifierHash: sha1Sum(ca.Namespace, ca.Name), + }) + + if err != nil { + return nil, err + } + + return c.crdInformerFactory.Sme().V1alpha1().CAPApplicationVersions().Lister().List(selector) +} + +func (c *Controller) checkSecretsExist(serviceInfos []v1alpha1.ServiceInfo, namespace string) error { + var err error + secretLister := c.kubeInformerFactory.Core().V1().Secrets().Lister() + + for _, service := range serviceInfos { + secretName := service.Secret + if _, err = secretLister.Secrets(namespace).Get(secretName); err != nil { + break + } + } + return err +} + +// This method is called to handle NotFound error at the beginning of reconciliation to skip requeue on errors due to deletion of resource +func handleOperatorResourceErrors(err error) error { + // Handle NotFound errors (object was most likely deleted) + if errors.IsNotFound(err) { + return nil // No error, to skips requeue of the resource + } + return err +} + +func getConsumedServiceMap(consumedServices []string) map[string]string { + // Create a Map of consumedServices + consumedServicesMap := make(map[string]string) + for _, consumedService := range consumedServices { + consumedServicesMap[consumedService] = consumedService + } + return consumedServicesMap +} + +func getConsumedServiceInfos(consumedServicesMap map[string]string, serviceInfos []v1alpha1.ServiceInfo) []v1alpha1.ServiceInfo { + consumedServiceInfo := []v1alpha1.ServiceInfo{} + + for _, serviceInfo := range serviceInfos { + if serviceInfo.Name == consumedServicesMap[serviceInfo.Name] { + consumedServiceInfo = append(consumedServiceInfo, serviceInfo) + } + } + return consumedServiceInfo +} + +func generateVCAPEnv(ns string, serviceInfos []v1alpha1.ServiceInfo, kubeClient kubernetes.Interface) ([]byte, error) { + envVCAPServices := map[string][]map[string]any{} + for _, serviceInfo := range serviceInfos { + entry, err := util.CreateVCAPEntryFromSecret(&serviceInfo, ns, kubeClient) + if err != nil { + return nil, err + } + + // Generate vcap_service info for the class + if envVCAPServices[serviceInfo.Class] == nil { + envVCAPServices[serviceInfo.Class] = []map[string]any{} + } + // Simulate attributes that describe a bound service (@TODO: consider adding tags, binding_name, plan etc..) + envVCAPServices[serviceInfo.Class] = append(envVCAPServices[serviceInfo.Class], entry) + } + + // Return stringified vcap env + return json.Marshal(envVCAPServices) +} + +func createVCAPSecret(namePrefix string, ns string, ownerRef metav1.OwnerReference, serviceInfos []v1alpha1.ServiceInfo, kubeClient kubernetes.Interface) (string, error) { + // Generate VCAP_SERVICES env. variable + vcapEnv, err := generateVCAPEnv(ns, serviceInfos, kubeClient) + if err != nil { + return "", err + } + + // Create a secret for VCAP_SERVICES for the given workload + secret, err := kubeClient.CoreV1().Secrets(ns).Create(context.TODO(), + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: namePrefix + "-", + OwnerReferences: []metav1.OwnerReference{ownerRef}, + }, + StringData: map[string]string{ + EnvVCAPServices: string(vcapEnv), + }, + }, + metav1.CreateOptions{}, + ) + if err != nil { + return "", err + } + // Successfully created deployment secret --> return name + // @TODO: Reconcile CAV once we expect secrets/credentials to be updated + return secret.Name, nil +} + +func validateEnv(envConfig []corev1.EnvVar, restrictedNames map[string]struct{}) string { + for _, envConfigEntry := range envConfig { + if _, ok := restrictedNames[envConfigEntry.Name]; ok { + return envConfigEntry.Name + } + } + // No restricted entries found + return "" +} + +func errorEnv(workloadType string, entry string) error { + return fmt.Errorf("invalid env configuration for workload: %s, remove entry: %s from configuration", workloadType, entry) +} + +func getRelevantJob(workloadType v1alpha1.JobType, cav *v1alpha1.CAPApplicationVersion) *v1alpha1.WorkloadDetails { + for _, workload := range cav.Spec.Workloads { + if workload.JobDefinition != nil && workload.JobDefinition.Type == workloadType { + return &workload + } + } + return nil +} + +func getRelevantDeployment(workloadType v1alpha1.DeploymentType, cav *v1alpha1.CAPApplicationVersion) *v1alpha1.WorkloadDetails { + workloads := getDeployments(workloadType, cav) + if len(workloads) == 0 { + return nil + } + return &workloads[0] +} + +func getDeployments(workloadType v1alpha1.DeploymentType, cav *v1alpha1.CAPApplicationVersion) []v1alpha1.WorkloadDetails { + deployments := []v1alpha1.WorkloadDetails{} + for _, workload := range cav.Spec.Workloads { + if workload.DeploymentDefinition != nil && workload.DeploymentDefinition.Type == workloadType { + deployments = append(deployments, workload) + } + } + return deployments +} + +func getWorkloadByName(name string, cav *v1alpha1.CAPApplicationVersion) *v1alpha1.WorkloadDetails { + for _, workload := range cav.Spec.Workloads { + if workload.Name == name { + return &workload + } + } + return nil +} + +func getJobState(job *batchv1.Job) JobState { + // check for completion + for _, condition := range job.Status.Conditions { + if condition.Status == corev1.ConditionTrue { + var state JobState + switch condition.Type { + case batchv1.JobComplete: + state = JobStateComplete + case batchv1.JobFailed: + state = JobStateFailed + default: + continue + } + return state + } + } + + // probably the job is still in process + return JobStateProcessing +} + +func isDeletionImminent(m *metav1.ObjectMeta) bool { + if m.DeletionTimestamp == nil { + return false + } + return len(m.Finalizers) == 0 +} + +func getRelevantServicePortInfo(cav *v1alpha1.CAPApplicationVersion) []servicePortInfo { + overallPortInfos := []servicePortInfo{} + for _, workload := range cav.Spec.Workloads { + var workloadPortInfo *servicePortInfo + if workload.DeploymentDefinition != nil { + workloadPortInfo = getWorkloadPortInfo(workload, cav.Name) + } + + if workloadPortInfo != nil { + overallPortInfos = append(overallPortInfos, *workloadPortInfo) + } + } + return overallPortInfos +} + +func getWorkloadPortInfo(workload v1alpha1.WorkloadDetails, cavName string) *servicePortInfo { + var servicePorts []corev1.ServicePort + var destinationDetails []destinationInfo + var clusterPorts []int32 + if len(workload.DeploymentDefinition.Ports) > 0 { + servicePorts = []corev1.ServicePort{} + destinationDetails = []destinationInfo{} + clusterPorts = []int32{} + for _, port := range workload.DeploymentDefinition.Ports { + servicePorts = append(servicePorts, corev1.ServicePort{Name: port.Name, Port: port.Port, AppProtocol: port.AppProtocol}) + if port.RouterDestinationName != "" { + destinationDetails = append(destinationDetails, destinationInfo{ + DestinationName: port.RouterDestinationName, + Port: port.Port, + }) + } + if port.NetworkPolicy == v1alpha1.PortNetworkPolicyTypeCluster { + clusterPorts = append(clusterPorts, port.Port) + } + } + } + workloadPortInfo := updateWorkloadPortInfo(cavName, workload.Name, workload.DeploymentDefinition.Type, servicePorts, destinationDetails, clusterPorts) + return workloadPortInfo +} + +func updateWorkloadPortInfo(cavName string, workloadName string, deploymentType v1alpha1.DeploymentType, servicePorts []corev1.ServicePort, destinationDetails []destinationInfo, clusterPorts []int32) *servicePortInfo { + var workloadPortInfo *servicePortInfo + if len(servicePorts) == 0 { + // Use fallback defaults + if deploymentType == v1alpha1.DeploymentRouter { + servicePorts = []corev1.ServicePort{ + {Name: "router-svc-port", Port: defaultRouterPort}, + } + } else if deploymentType == v1alpha1.DeploymentCAP { + servicePorts = []corev1.ServicePort{ + {Name: "server-svc-port", Port: defaultServerPort}, + } + // When there are no ports there can be no destinations, just create a default one for CAP backend + destinationDetails = append([]destinationInfo{}, destinationInfo{ + DestinationName: "srv-api", + Port: defaultServerPort, + }) + } + } + + if len(servicePorts) > 0 { + workloadPortInfo = &servicePortInfo{ + WorkloadName: cavName + "-" + workloadName, + DeploymentType: string(deploymentType), + Ports: servicePorts, + Destinations: destinationDetails, + ClusterPorts: clusterPorts, + } + } + + return workloadPortInfo +} + +func (c *Controller) getRouterServicePortInfo(cavName string, namespace string) (*servicePortInfo, error) { + cav, err := c.crdInformerFactory.Sme().V1alpha1().CAPApplicationVersions().Lister().CAPApplicationVersions(namespace).Get(cavName) + if err != nil { + return nil, err + } + + routerWorkload := getRelevantDeployment(v1alpha1.DeploymentRouter, cav) + + return getWorkloadPortInfo(*routerWorkload, cavName), nil +} + +func copyMaps(originalMap map[string]string, additionalMap map[string]string) map[string]string { + newMap := map[string]string{} + for key, value := range originalMap { + newMap[key] = value + } + for key, value := range additionalMap { + newMap[key] = value + } + return newMap +} diff --git a/internal/controller/reconcile_test.go b/internal/controller/reconcile_test.go new file mode 100644 index 0000000..63c362b --- /dev/null +++ b/internal/controller/reconcile_test.go @@ -0,0 +1,585 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +package controller + +import ( + "context" + "os" + "reflect" + "strconv" + "strings" + "testing" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/rand" + k8sfake "k8s.io/client-go/kubernetes/fake" + k8stesting "k8s.io/client-go/testing" + + certManagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" + certManagerFake "github.com/cert-manager/cert-manager/pkg/client/clientset/versioned/fake" + certv1alpha1 "github.com/gardener/cert-management/pkg/apis/cert/v1alpha1" + certfake "github.com/gardener/cert-management/pkg/client/cert/clientset/versioned/fake" + dnsv1alpha1 "github.com/gardener/external-dns-management/pkg/apis/dns/v1alpha1" + dnsfake "github.com/gardener/external-dns-management/pkg/client/dns/clientset/versioned/fake" + "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + "github.com/sap/cap-operator/pkg/client/clientset/versioned/fake" + istionwv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" + istiofake "istio.io/client-go/pkg/clientset/versioned/fake" +) + +const ( + caCroName = "ca-test-name" + providerSubDomain = "provider-subdomain" + consumerSubDomain = "consumer-subdomain" + providerTenantId = "provider-tenant-id" + consumerTenantId = "consumer-tenant-id" + cavCroName = "cav-test-name" + btpApplicationName = "some-app-name" + globalAccountId = "global-id-test" + primaryDomain = "app.sme.sap.com" + secondaryDomain = "sec.sme.sap.com" + dnsTarget = "public-ingress.some.cluster.sap" + defaultVersion = "0.0.1" +) + +const ( + ingressGWName = "ingressGw" + istioSystemNamespace = "istio-system" + gatewayName = btpApplicationName + "-" + GatewaySuffix + certificateName = btpApplicationName + "-" + CertificateSuffix + dnsEntryName = btpApplicationName + "-" + PrimaryDnsSuffix +) + +type ingressResources struct { + service *corev1.Service + pod *corev1.Pod +} + +type testResources struct { + cas []*v1alpha1.CAPApplication + cavs []*v1alpha1.CAPApplicationVersion + cats []*v1alpha1.CAPTenant + ingressGW []*ingressResources + gateway *istionwv1beta1.Gateway + gardenerCert *certv1alpha1.Certificate + certManagerCert *certManagerv1.Certificate + dnsEntry *dnsv1alpha1.DNSEntry + preventStart bool +} + +func createIngressResource(name string, ca *v1alpha1.CAPApplication, dnsTarget string) *ingressResources { + ingressLabelSelector := map[string]string{} + svcName := "istioingress-gateway" + namespace := istioSystemNamespace + if name != ingressGWName { + svcName = name + namespace = metav1.NamespaceDefault + } + for _, label := range ca.Spec.Domains.IstioIngressGatewayLabels { + ingressLabelSelector[label.Name] = label.Value + } + + return &ingressResources{ + pod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Labels: ingressLabelSelector, + }, + }, + service: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: svcName, + Namespace: namespace, + Annotations: map[string]string{ + AnnotationGardenerDNSTarget: dnsTarget, + }, + }, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeLoadBalancer, + Selector: ingressLabelSelector, + }, + }, + } +} + +func createCaCRO(name string, withFinalizer bool) *v1alpha1.CAPApplication { + ca := &v1alpha1.CAPApplication{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: metav1.NamespaceDefault, + }, + Spec: v1alpha1.CAPApplicationSpec{ + Domains: v1alpha1.ApplicationDomains{ + Primary: primaryDomain, + Secondary: []string{secondaryDomain}, + IstioIngressGatewayLabels: []v1alpha1.NameValue{ + { + Name: "istio", + Value: "ingressgateway", + }, + { + Name: "app", + Value: "istio-ingressgateway", + }, + }, + }, + GlobalAccountId: globalAccountId, + BTPAppName: btpApplicationName, + Provider: v1alpha1.BTPTenantIdentification{ + SubDomain: providerSubDomain, + TenantId: providerTenantId, + }, + BTP: v1alpha1.BTP{ + Services: []v1alpha1.ServiceInfo{ + { + Class: "xsuaa", + Name: "test-xsuaa", + Secret: "test-xsuaa-sec", + }, + { + Class: "saas-registry", + Name: "test-saas", + Secret: "test-saas-sec", + }, + { + Class: "service-manager", + Name: "test-sm", + Secret: "test-sm-sec", + }, + { + Class: "destination", + Name: "test-dest", + Secret: "test-dest-sec", + }, + { + Class: "html5-apps-repo", + Name: "test-html-host", + Secret: "test-html-host-sec", + }, + { + Class: "html5-apps-repo", + Name: "test-html-rt", + Secret: "test-html-rt-sec", + }, + }, + }, + }, + } + + if withFinalizer { + ca.Finalizers = []string{FinalizerCAPApplication} + } + + return ca +} + +func createCavCRO(name string, state v1alpha1.CAPApplicationVersionState, version string) *v1alpha1.CAPApplicationVersion { + status := metav1.ConditionFalse + if state == v1alpha1.CAPApplicationVersionStateReady || state == v1alpha1.CAPApplicationVersionStateDeleting { + status = metav1.ConditionTrue + } + return &v1alpha1.CAPApplicationVersion{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: metav1.NamespaceDefault, + Labels: map[string]string{ + LabelOwnerIdentifierHash: sha1Sum(metav1.NamespaceDefault, caCroName), + }, + }, + Spec: v1alpha1.CAPApplicationVersionSpec{ + CAPApplicationInstance: caCroName, + Version: version, + Workloads: []v1alpha1.WorkloadDetails{ + { + Name: "cap-backend-server", + ConsumedBTPServices: []string{ + "test-xsuaa", + "test-saas", + }, + DeploymentDefinition: &v1alpha1.DeploymentDetails{ + Type: v1alpha1.DeploymentCAP, + ContainerDetails: v1alpha1.ContainerDetails{ + Image: "test://image", + }, + }, + }, + { + Name: "app-router", + ConsumedBTPServices: []string{}, + DeploymentDefinition: &v1alpha1.DeploymentDetails{ + ContainerDetails: v1alpha1.ContainerDetails{ + Image: "test://image", + }, + }, + }, + }, + }, + Status: v1alpha1.CAPApplicationVersionStatus{ + GenericStatus: v1alpha1.GenericStatus{ + Conditions: []metav1.Condition{ + { + Status: status, + Type: string(v1alpha1.ConditionTypeReady), + }, + }, + }, + State: state, + }, + } +} + +// Replace GenerateName logic from k8s as fake clients do not generate a name +func generateName(namePrefix string) string { + return namePrefix + rand.String(5) +} + +func generateMetaObjName(obj interface{}) { + metaObj, _ := meta.Accessor(obj) + if metaObj.GetName() == "" && metaObj.GetGenerateName() != "" { + metaObj.SetName(generateName(metaObj.GetGenerateName())) + } +} + +func createCatCRO(caName string, tenantType string, withFinalizers bool) *v1alpha1.CAPTenant { + cat := &v1alpha1.CAPTenant{ + ObjectMeta: metav1.ObjectMeta{ + Name: strings.Join([]string{caName, tenantType}, "-"), + Namespace: metav1.NamespaceDefault, + }, + Spec: v1alpha1.CAPTenantSpec{ + CAPApplicationInstance: caCroName, + BTPTenantIdentification: v1alpha1.BTPTenantIdentification{}, + Version: defaultVersion, + }, + Status: v1alpha1.CAPTenantStatus{ + CurrentCAPApplicationVersionInstance: cavCroName, + }, + } + + if tenantType == ProviderTenantType { + cat.Spec.BTPTenantIdentification.TenantId = providerTenantId + cat.Spec.BTPTenantIdentification.SubDomain = providerSubDomain + } else { + cat.Spec.BTPTenantIdentification.TenantId = consumerTenantId + cat.Spec.BTPTenantIdentification.SubDomain = consumerSubDomain + } + + if withFinalizers { + cat.Finalizers = []string{FinalizerCAPTenant} + } + return cat +} + +func addRuntimeObjects(objects *[]runtime.Object, object runtime.Object) { + if reflect.ValueOf(object).Elem().IsValid() { + *objects = append(*objects, object) + } +} + +func getTestController(resources testResources) *Controller { + crdObjects := []runtime.Object{} + coreObjects := []runtime.Object{} + istioObjects := []runtime.Object{} + gardenerCertObjects := []runtime.Object{} + certManagerCertObjects := []runtime.Object{} + dnsObjects := []runtime.Object{} + + for _, ca := range resources.cas { + addRuntimeObjects(&crdObjects, ca) + } + + for _, cav := range resources.cavs { + addRuntimeObjects(&crdObjects, cav) + } + + for _, cat := range resources.cats { + addRuntimeObjects(&crdObjects, cat) + } + + for _, ingressGW := range resources.ingressGW { + if ingressGW != nil { + addRuntimeObjects(&coreObjects, ingressGW.service) + addRuntimeObjects(&coreObjects, ingressGW.pod) + } + } + + addRuntimeObjects(&istioObjects, resources.gateway) + addRuntimeObjects(&gardenerCertObjects, resources.gardenerCert) + addRuntimeObjects(&certManagerCertObjects, resources.certManagerCert) + addRuntimeObjects(&dnsObjects, resources.dnsEntry) + + coreClient := k8sfake.NewSimpleClientset(coreObjects...) + coreClient.PrependReactor("create", "*", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) { + act := action.(k8stesting.CreateAction) + obj := act.GetObject() + generateMetaObjName(obj) + return false, obj, nil + }) + + crdClient := fake.NewSimpleClientset(crdObjects...) + + istioClient := istiofake.NewSimpleClientset(istioObjects...) + + certClient := certfake.NewSimpleClientset(gardenerCertObjects...) + + certManagerCertClient := certManagerFake.NewSimpleClientset(certManagerCertObjects...) + + dnsClient := dnsfake.NewSimpleClientset(dnsObjects...) + + c := NewController(coreClient, crdClient, istioClient, certClient, certManagerCertClient, dnsClient) + + for _, ca := range resources.cas { + if ca != nil { + c.crdInformerFactory.Sme().V1alpha1().CAPApplications().Informer().GetIndexer().Add(ca) + } + } + + for _, cav := range resources.cavs { + if cav != nil { + c.crdInformerFactory.Sme().V1alpha1().CAPApplicationVersions().Informer().GetIndexer().Add(cav) + } + } + + for _, cat := range resources.cats { + if cat != nil { + c.crdInformerFactory.Sme().V1alpha1().CAPTenants().Informer().GetIndexer().Add(cat) + } + } + + if resources.gateway != nil { + c.istioClient.NetworkingV1beta1().Gateways(resources.gateway.Namespace).Create(context.TODO(), resources.gateway, metav1.CreateOptions{}) + c.istioInformerFactory.Networking().V1beta1().Gateways().Informer().GetIndexer().Add(resources.gateway) + } + + if resources.gardenerCert != nil { + c.gardenerCertInformerFactory.Cert().V1alpha1().Certificates().Informer().GetIndexer().Add(resources.gardenerCert) + } + + if resources.certManagerCert != nil { + c.certManagerInformerFactory.Certmanager().V1().Certificates().Informer().GetIndexer().Add(resources.certManagerCert) + } + + if resources.dnsEntry != nil { + c.gardenerDNSInformerFactory.Dns().V1alpha1().DNSEntries().Informer().GetIndexer().Add(resources.dnsEntry) + } + + for _, ingressGW := range resources.ingressGW { + if ingressGW != nil { + c.kubeInformerFactory.Core().V1().Services().Informer().GetIndexer().Add(ingressGW.service) + c.kubeInformerFactory.Core().V1().Pods().Informer().GetIndexer().Add(ingressGW.pod) + } + } + if !resources.preventStart { + stopCh := make(chan struct{}) + defer close(stopCh) + + c.crdInformerFactory.Start(stopCh) + c.kubeInformerFactory.Start(stopCh) + c.istioInformerFactory.Start(stopCh) + switch certificateManager() { + case certManagerGardener: + c.gardenerCertInformerFactory.Start(stopCh) + case certManagerCertManagerIO: + c.certManagerInformerFactory.Start(stopCh) + } + c.gardenerDNSInformerFactory.Start(stopCh) + } + + return c +} + +func TestMain(m *testing.M) { + os.Setenv(certManagerEnv, "gardener") + os.Setenv(dnsManagerEnv, "gardener") + defer os.Setenv(certManagerEnv, "") + defer os.Setenv(dnsManagerEnv, "") + m.Run() +} + +func TestGetLatestReadyCAPApplicationVersion(t *testing.T) { + tests := []struct { + testName string + status v1alpha1.CAPApplicationVersionState + number int + expectedVersion string + }{ + { + testName: "when getLatestReadyCAPApplicationVersion() is called with no CAVs", + status: "", + number: 0, + expectedVersion: "", + }, + { + testName: "when getLatestReadyCAPApplicationVersion() is called with one CAV in ready state", + status: v1alpha1.CAPApplicationVersionStateReady, + number: 1, + expectedVersion: "0.0.1", + }, + { + testName: "when getLatestReadyCAPApplicationVersion() is called with one CAV in processing (not ready) state", + status: v1alpha1.CAPApplicationVersionStateProcessing, + number: 9, + expectedVersion: "", + }, + { + testName: "when getLatestReadyCAPApplicationVersion() is called with multiple CAVs in ready states", + status: v1alpha1.CAPApplicationVersionStateReady, + number: 18, + expectedVersion: "0.9.0", + }, + { + testName: "when getLatestReadyCAPApplicationVersion() is called with multiple CAVs in mixed states", + status: "mixed", + number: 18, + expectedVersion: "0.0.9", + }, + } + for _, test := range tests { + t.Run(test.testName, func(t *testing.T) { + ca := createCaCRO(caCroName, true) + var cavs []*v1alpha1.CAPApplicationVersion + + for i := 1; i <= test.number; i++ { + var state v1alpha1.CAPApplicationVersionState + // for mixed states - mark the latest versions in processing (not-ready) state + if test.status == "mixed" { + if i > 9 { + state = v1alpha1.CAPApplicationVersionStateProcessing + } else { + state = v1alpha1.CAPApplicationVersionStateReady + } + } else { + state = test.status + } + + indexString := strconv.Itoa(i) + var version string + if i > 9 && i < 20 { + version = "0." + strconv.Itoa((i+1)-10) + ".0" // 0.1.0 - 0.9.0 + } else { + version = "0.0." + indexString // 0.0.1 - 0.0.9 + } + cav := createCavCRO(cavCroName+version, state, version) + + cavs = append(cavs, cav) + } + + c := getTestController(testResources{ + cas: []*v1alpha1.CAPApplication{ca}, + cavs: cavs, + }) + + latestCav, err := c.getLatestReadyCAPApplicationVersion(context.TODO(), ca, false) + + if test.status == v1alpha1.CAPApplicationVersionStateReady || test.status == "mixed" { + if err != nil { + t.Fatal("Error should not be thrown") + } + + if latestCav.Spec.Version != test.expectedVersion { + t.Fatal("Expected version not returned") + } + } else if err == nil { + t.Fatal("Error should be thrown") + } + }) + } +} + +func TestGetLatestCAPApplicationVersion(t *testing.T) { + tests := []struct { + testName string + expectError bool + status string + number int + expectedVersion string + }{ + { + testName: "when getLatestCAPApplicationVersion() is called with no CAVs", + expectError: true, + number: 0, + expectedVersion: "", + }, + { + testName: "when getLatestCAPApplicationVersion() is called with one CAV in ready state", + expectError: false, + number: 1, + expectedVersion: "0.0.1", + }, + { + testName: "when getLatestCAPApplicationVersion() is called with one CAV in processing (not ready) state", + expectError: false, + status: "mixed", + number: 10, + expectedVersion: "0.1.0", + }, + { + testName: "when getLatestCAPApplicationVersion() is called with multiple CAVs in ready states", + expectError: false, + number: 18, + expectedVersion: "0.9.0", + }, + { + testName: "when getLatestCAPApplicationVersion() is called with multiple CAVs in mixed states", + status: "mixed", + number: 18, + expectedVersion: "0.9.0", + }, + } + for _, test := range tests { + t.Run(test.testName, func(t *testing.T) { + ca := createCaCRO(caCroName, true) + var cavs []*v1alpha1.CAPApplicationVersion + + for i := 1; i <= test.number; i++ { + var state v1alpha1.CAPApplicationVersionState + // for mixed states - mark the latest versions in processing (not-ready) state + if test.status == "mixed" { + if i > 9 { + state = v1alpha1.CAPApplicationVersionStateProcessing + } else { + state = v1alpha1.CAPApplicationVersionStateReady + } + } else { + state = v1alpha1.CAPApplicationVersionStateReady + } + + indexString := strconv.Itoa(i) + var version string + if i > 9 && i < 20 { + version = "0." + strconv.Itoa((i+1)-10) + ".0" // 0.1.0 - 0.9.0 + } else { + version = "0.0." + indexString // 0.0.1 - 0.0.9 + } + cav := createCavCRO(cavCroName+version, state, version) + + cavs = append(cavs, cav) + } + + c := getTestController(testResources{ + cas: []*v1alpha1.CAPApplication{ca}, + cavs: cavs, + }) + + latestCav, err := c.getLatestCAPApplicationVersion(context.TODO(), ca) + + if test.expectError == false { + if err != nil { + t.Fatal("Error should not be thrown") + } + + if latestCav.Spec.Version != test.expectedVersion { + t.Fatal("Expected version not returned") + } + } else if err == nil { + t.Fatal("Error should be thrown") + } + }) + } +} diff --git a/internal/controller/reconciliation-result.go b/internal/controller/reconciliation-result.go new file mode 100644 index 0000000..8c2632b --- /dev/null +++ b/internal/controller/reconciliation-result.go @@ -0,0 +1,46 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +package controller + +import ( + "time" +) + +type ReconcileResult struct { + // the key in this map is a value which corresponds to a specific resource type + requeueResources map[int][]RequeueItem +} + +type RequeueItem struct { + resourceKey NamespacedResourceKey + // requeueAfter tells the Controller to re-queue the item after the specified duration. Defaults to 0s (immediate re-queue) + requeueAfter time.Duration +} + +func NewReconcileResult() *ReconcileResult { + return &ReconcileResult{} +} + +func NewReconcileResultWithResource(rid int, resourceName string, resourceNamespace string, requeueAfter time.Duration) *ReconcileResult { + reconResult := NewReconcileResult() + reconResult.AddResource(rid, resourceName, resourceNamespace, requeueAfter) + return reconResult +} + +func (r *ReconcileResult) AddResource(rid int, resourceName string, resourceNamespace string, after time.Duration) { + resource := NamespacedResourceKey{Namespace: resourceNamespace, Name: resourceName} + if r.requeueResources == nil { + r.requeueResources = map[int][]RequeueItem{ + rid: {{resourceKey: resource, requeueAfter: after}}, + } + return + } + items, ok := r.requeueResources[rid] + if !ok { + r.requeueResources[rid] = []RequeueItem{{resourceKey: resource, requeueAfter: after}} + } else { + r.requeueResources[rid] = append(items, RequeueItem{resourceKey: resource, requeueAfter: after}) + } +} diff --git a/internal/controller/testdata/capapplication/ca-01.expected.yaml b/internal/controller/testdata/capapplication/ca-01.expected.yaml new file mode 100644 index 0000000..4aeaab1 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-01.expected.yaml @@ -0,0 +1,42 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + state: "" diff --git a/internal/controller/testdata/capapplication/ca-01.initial.yaml b/internal/controller/testdata/capapplication/ca-01.initial.yaml new file mode 100644 index 0000000..e25a1f5 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-01.initial.yaml @@ -0,0 +1,38 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + state: "" \ No newline at end of file diff --git a/internal/controller/testdata/capapplication/ca-02.expected.yaml b/internal/controller/testdata/capapplication/ca-02.expected.yaml new file mode 100644 index 0000000..f025fd2 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-02.expected.yaml @@ -0,0 +1,49 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + conditions: + - reason: ProcessingSecrets + message: waiting for secrets to get ready for CAPApplication test-cap-01.default + observedGeneration: 2 + status: "False" + type: Ready + observedGeneration: 2 + state: Processing \ No newline at end of file diff --git a/internal/controller/testdata/capapplication/ca-02.initial.yaml b/internal/controller/testdata/capapplication/ca-02.initial.yaml new file mode 100644 index 0000000..74de667 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-02.initial.yaml @@ -0,0 +1,42 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + state: "Processing" \ No newline at end of file diff --git a/internal/controller/testdata/capapplication/ca-03.expected.yaml b/internal/controller/testdata/capapplication/ca-03.expected.yaml new file mode 100644 index 0000000..1bb1958 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-03.expected.yaml @@ -0,0 +1,52 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + conditions: + - reason: WaitingForReadyCAPApplicationVersion + observedGeneration: 2 + status: "False" + type: Ready + - type: LatestVersionReady + reason: WaitingForReadyCAPApplicationVersion + observedGeneration: 2 + status: "False" + observedGeneration: 2 + state: Processing \ No newline at end of file diff --git a/internal/controller/testdata/capapplication/ca-03.initial.yaml b/internal/controller/testdata/capapplication/ca-03.initial.yaml new file mode 100644 index 0000000..74de667 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-03.initial.yaml @@ -0,0 +1,42 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + state: "Processing" \ No newline at end of file diff --git a/internal/controller/testdata/capapplication/ca-04.expected.yaml b/internal/controller/testdata/capapplication/ca-04.expected.yaml new file mode 100644 index 0000000..0e739ce --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-04.expected.yaml @@ -0,0 +1,50 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + conditions: + - reason: ProviderTenantProcessing + message: "provider CAPTenant not ready for CAPApplication default.test-cap-01; waiting for it to be ready" + observedGeneration: 2 + status: "False" + type: Ready + observedGeneration: 2 + domainSpecHash: 84fdeebf558412817f54b17a0158c6b6d53dda98e8bc7a887aaa7170296841f8 + state: Processing diff --git a/internal/controller/testdata/capapplication/ca-04.initial.yaml b/internal/controller/testdata/capapplication/ca-04.initial.yaml new file mode 100644 index 0000000..bb22196 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-04.initial.yaml @@ -0,0 +1,43 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + state: "Processing" + domainSpecHash: 84fdeebf558412817f54b17a0158c6b6d53dda98e8bc7a887aaa7170296841f8 diff --git a/internal/controller/testdata/capapplication/ca-05.expected.yaml b/internal/controller/testdata/capapplication/ca-05.expected.yaml new file mode 100644 index 0000000..1bb1958 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-05.expected.yaml @@ -0,0 +1,52 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + conditions: + - reason: WaitingForReadyCAPApplicationVersion + observedGeneration: 2 + status: "False" + type: Ready + - type: LatestVersionReady + reason: WaitingForReadyCAPApplicationVersion + observedGeneration: 2 + status: "False" + observedGeneration: 2 + state: Processing \ No newline at end of file diff --git a/internal/controller/testdata/capapplication/ca-05.initial.yaml b/internal/controller/testdata/capapplication/ca-05.initial.yaml new file mode 100644 index 0000000..74de667 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-05.initial.yaml @@ -0,0 +1,42 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + state: "Processing" \ No newline at end of file diff --git a/internal/controller/testdata/capapplication/ca-06.expected.yaml b/internal/controller/testdata/capapplication/ca-06.expected.yaml new file mode 100644 index 0000000..7875d03 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-06.expected.yaml @@ -0,0 +1,57 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + conditions: + - reason: VersionExists + observedGeneration: 2 + status: "True" + type: Ready + - reason: LatestVersionReady + observedGeneration: 2 + status: "True" + type: LatestVersionReady + - reason: AllTenantsReady + observedGeneration: 2 + status: "True" + type: AllTenantsReady + observedGeneration: 2 + domainSpecHash: 84fdeebf558412817f54b17a0158c6b6d53dda98e8bc7a887aaa7170296841f8 + state: Consistent diff --git a/internal/controller/testdata/capapplication/ca-06.initial.yaml b/internal/controller/testdata/capapplication/ca-06.initial.yaml new file mode 100644 index 0000000..bb22196 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-06.initial.yaml @@ -0,0 +1,43 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + state: "Processing" + domainSpecHash: 84fdeebf558412817f54b17a0158c6b6d53dda98e8bc7a887aaa7170296841f8 diff --git a/internal/controller/testdata/capapplication/ca-07.expected.yaml b/internal/controller/testdata/capapplication/ca-07.expected.yaml new file mode 100644 index 0000000..ca1aba5 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-07.expected.yaml @@ -0,0 +1,49 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + conditions: + - reason: ProcessingDomainResources + observedGeneration: 2 + status: "False" + type: Ready + observedGeneration: 2 + domainSpecHash: 84fdeebf558412817f54b17a0158c6b6d53dda98e8bc7a887aaa7170296841f8 + state: Processing diff --git a/internal/controller/testdata/capapplication/ca-07.initial.yaml b/internal/controller/testdata/capapplication/ca-07.initial.yaml new file mode 100644 index 0000000..74de667 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-07.initial.yaml @@ -0,0 +1,42 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + state: "Processing" \ No newline at end of file diff --git a/internal/controller/testdata/capapplication/ca-08.expected.yaml b/internal/controller/testdata/capapplication/ca-08.expected.yaml new file mode 100644 index 0000000..ca1aba5 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-08.expected.yaml @@ -0,0 +1,49 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + conditions: + - reason: ProcessingDomainResources + observedGeneration: 2 + status: "False" + type: Ready + observedGeneration: 2 + domainSpecHash: 84fdeebf558412817f54b17a0158c6b6d53dda98e8bc7a887aaa7170296841f8 + state: Processing diff --git a/internal/controller/testdata/capapplication/ca-08.initial.yaml b/internal/controller/testdata/capapplication/ca-08.initial.yaml new file mode 100644 index 0000000..74de667 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-08.initial.yaml @@ -0,0 +1,42 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + state: "Processing" \ No newline at end of file diff --git a/internal/controller/testdata/capapplication/ca-09.expected.yaml b/internal/controller/testdata/capapplication/ca-09.expected.yaml new file mode 100644 index 0000000..6396dc8 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-09.expected.yaml @@ -0,0 +1,50 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + conditions: + - reason: ProviderTenantError + message: "provider CAPTenant in state ProvisioningError for CAPApplication default.test-cap-01" + observedGeneration: 2 + status: "False" + type: Ready + observedGeneration: 2 + domainSpecHash: 84fdeebf558412817f54b17a0158c6b6d53dda98e8bc7a887aaa7170296841f8 + state: Error diff --git a/internal/controller/testdata/capapplication/ca-09.initial.yaml b/internal/controller/testdata/capapplication/ca-09.initial.yaml new file mode 100644 index 0000000..bb22196 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-09.initial.yaml @@ -0,0 +1,43 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + state: "Processing" + domainSpecHash: 84fdeebf558412817f54b17a0158c6b6d53dda98e8bc7a887aaa7170296841f8 diff --git a/internal/controller/testdata/capapplication/ca-10.expected.yaml b/internal/controller/testdata/capapplication/ca-10.expected.yaml new file mode 100644 index 0000000..3fdd810 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-10.expected.yaml @@ -0,0 +1,50 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + conditions: + - reason: DomainResourcesError + message: "Certificate in state Error for CAPApplication default.test-cap-01: cert message" + observedGeneration: 2 + status: "False" + type: Ready + observedGeneration: 2 + domainSpecHash: 84fdeebf558412817f54b17a0158c6b6d53dda98e8bc7a887aaa7170296841f8 + state: Error diff --git a/internal/controller/testdata/capapplication/ca-10.initial.yaml b/internal/controller/testdata/capapplication/ca-10.initial.yaml new file mode 100644 index 0000000..bb22196 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-10.initial.yaml @@ -0,0 +1,43 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + state: "Processing" + domainSpecHash: 84fdeebf558412817f54b17a0158c6b6d53dda98e8bc7a887aaa7170296841f8 diff --git a/internal/controller/testdata/capapplication/ca-11.expected.yaml b/internal/controller/testdata/capapplication/ca-11.expected.yaml new file mode 100644 index 0000000..d52d485 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-11.expected.yaml @@ -0,0 +1,50 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + conditions: + - reason: DomainResourcesError + message: "DNSEntry in state Error for CAPApplication default.test-cap-01: dns message" + observedGeneration: 2 + status: "False" + type: Ready + observedGeneration: 2 + domainSpecHash: 84fdeebf558412817f54b17a0158c6b6d53dda98e8bc7a887aaa7170296841f8 + state: Error diff --git a/internal/controller/testdata/capapplication/ca-11.initial.yaml b/internal/controller/testdata/capapplication/ca-11.initial.yaml new file mode 100644 index 0000000..bb22196 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-11.initial.yaml @@ -0,0 +1,43 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + state: "Processing" + domainSpecHash: 84fdeebf558412817f54b17a0158c6b6d53dda98e8bc7a887aaa7170296841f8 diff --git a/internal/controller/testdata/capapplication/ca-12.expected.yaml b/internal/controller/testdata/capapplication/ca-12.expected.yaml new file mode 100644 index 0000000..25ed51a --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-12.expected.yaml @@ -0,0 +1,50 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + conditions: + - reason: ProviderTenantError + message: "provider CAPTenant in state UpgradeError for CAPApplication default.test-cap-01" + observedGeneration: 2 + status: "False" + type: Ready + observedGeneration: 2 + domainSpecHash: 84fdeebf558412817f54b17a0158c6b6d53dda98e8bc7a887aaa7170296841f8 + state: Error diff --git a/internal/controller/testdata/capapplication/ca-12.initial.yaml b/internal/controller/testdata/capapplication/ca-12.initial.yaml new file mode 100644 index 0000000..bb22196 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-12.initial.yaml @@ -0,0 +1,43 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + state: "Processing" + domainSpecHash: 84fdeebf558412817f54b17a0158c6b6d53dda98e8bc7a887aaa7170296841f8 diff --git a/internal/controller/testdata/capapplication/ca-13.expected.yaml b/internal/controller/testdata/capapplication/ca-13.expected.yaml new file mode 100644 index 0000000..04d2204 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-13.expected.yaml @@ -0,0 +1,42 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + state: Processing diff --git a/internal/controller/testdata/capapplication/ca-13.initial.yaml b/internal/controller/testdata/capapplication/ca-13.initial.yaml new file mode 100644 index 0000000..bfe51cf --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-13.initial.yaml @@ -0,0 +1,40 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + state: "Processing" \ No newline at end of file diff --git a/internal/controller/testdata/capapplication/ca-14.expected.yaml b/internal/controller/testdata/capapplication/ca-14.expected.yaml new file mode 100644 index 0000000..2fbdbe2 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-14.expected.yaml @@ -0,0 +1,41 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + deletionTimestamp: "2022-03-22T13:24:38Z" + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + state: Processing diff --git a/internal/controller/testdata/capapplication/ca-14.initial.yaml b/internal/controller/testdata/capapplication/ca-14.initial.yaml new file mode 100644 index 0000000..4234d73 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-14.initial.yaml @@ -0,0 +1,41 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + deletionTimestamp: "2022-03-22T13:24:38Z" + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + state: "Processing" \ No newline at end of file diff --git a/internal/controller/testdata/capapplication/ca-15.expected.yaml b/internal/controller/testdata/capapplication/ca-15.expected.yaml new file mode 100644 index 0000000..b102df5 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-15.expected.yaml @@ -0,0 +1,50 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + deletionTimestamp: "2022-03-22T13:24:38Z" + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + conditions: + - reason: DeleteTriggered + message: "" + observedGeneration: 2 + status: "False" + type: Ready + observedGeneration: 2 + state: Deleting \ No newline at end of file diff --git a/internal/controller/testdata/capapplication/ca-15.initial.yaml b/internal/controller/testdata/capapplication/ca-15.initial.yaml new file mode 100644 index 0000000..4fbc4b3 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-15.initial.yaml @@ -0,0 +1,43 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + deletionTimestamp: "2022-03-22T13:24:38Z" + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + state: "Processing" \ No newline at end of file diff --git a/internal/controller/testdata/capapplication/ca-16.expected.yaml b/internal/controller/testdata/capapplication/ca-16.expected.yaml new file mode 100644 index 0000000..d88e0d7 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-16.expected.yaml @@ -0,0 +1,46 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: [] + deletionTimestamp: "2022-03-22T13:24:38Z" + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + state: Deleting + conditions: + - type: Ready + reason: DeleteTriggered + status: "False" diff --git a/internal/controller/testdata/capapplication/ca-16.initial.yaml b/internal/controller/testdata/capapplication/ca-16.initial.yaml new file mode 100644 index 0000000..a9d588e --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-16.initial.yaml @@ -0,0 +1,47 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + deletionTimestamp: "2022-03-22T13:24:38Z" + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + state: "Deleting" + conditions: + - type: Ready + reason: DeleteTriggered + status: "False" diff --git a/internal/controller/testdata/capapplication/ca-17.expected.yaml b/internal/controller/testdata/capapplication/ca-17.expected.yaml new file mode 100644 index 0000000..ea6f53d --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-17.expected.yaml @@ -0,0 +1,47 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + deletionTimestamp: "2022-03-22T13:24:38Z" + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + state: Deleting + conditions: + - type: Ready + reason: DeletionTriggered + status: "False" diff --git a/internal/controller/testdata/capapplication/ca-17.initial.yaml b/internal/controller/testdata/capapplication/ca-17.initial.yaml new file mode 100644 index 0000000..aae4a74 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-17.initial.yaml @@ -0,0 +1,47 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + deletionTimestamp: "2022-03-22T13:24:38Z" + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + state: "Deleting" + conditions: + - type: Ready + reason: DeletionTriggered + status: "False" diff --git a/internal/controller/testdata/capapplication/ca-18.expected.yaml b/internal/controller/testdata/capapplication/ca-18.expected.yaml new file mode 100644 index 0000000..ea6f53d --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-18.expected.yaml @@ -0,0 +1,47 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + deletionTimestamp: "2022-03-22T13:24:38Z" + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + state: Deleting + conditions: + - type: Ready + reason: DeletionTriggered + status: "False" diff --git a/internal/controller/testdata/capapplication/ca-18.initial.yaml b/internal/controller/testdata/capapplication/ca-18.initial.yaml new file mode 100644 index 0000000..aae4a74 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-18.initial.yaml @@ -0,0 +1,47 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + deletionTimestamp: "2022-03-22T13:24:38Z" + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + state: "Deleting" + conditions: + - type: Ready + reason: DeletionTriggered + status: "False" diff --git a/internal/controller/testdata/capapplication/ca-19.expected.yaml b/internal/controller/testdata/capapplication/ca-19.expected.yaml new file mode 100644 index 0000000..5c263ac --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-19.expected.yaml @@ -0,0 +1,46 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: [] + deletionTimestamp: "2022-03-22T13:24:38Z" + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + state: Deleting + conditions: + - type: Ready + reason: DeletionTriggered + status: "False" diff --git a/internal/controller/testdata/capapplication/ca-19.initial.yaml b/internal/controller/testdata/capapplication/ca-19.initial.yaml new file mode 100644 index 0000000..aae4a74 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-19.initial.yaml @@ -0,0 +1,47 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + deletionTimestamp: "2022-03-22T13:24:38Z" + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + state: "Deleting" + conditions: + - type: Ready + reason: DeletionTriggered + status: "False" diff --git a/internal/controller/testdata/capapplication/ca-20.expected.yaml b/internal/controller/testdata/capapplication/ca-20.expected.yaml new file mode 100644 index 0000000..923c348 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-20.expected.yaml @@ -0,0 +1,47 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + deletionTimestamp: "2022-03-22T13:24:38Z" + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + state: Deleting + conditions: + - type: Ready + reason: DeleteTriggered + status: "False" diff --git a/internal/controller/testdata/capapplication/ca-20.initial.yaml b/internal/controller/testdata/capapplication/ca-20.initial.yaml new file mode 100644 index 0000000..a9d588e --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-20.initial.yaml @@ -0,0 +1,47 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + deletionTimestamp: "2022-03-22T13:24:38Z" + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + state: "Deleting" + conditions: + - type: Ready + reason: DeleteTriggered + status: "False" diff --git a/internal/controller/testdata/capapplication/ca-21.expected.yaml b/internal/controller/testdata/capapplication/ca-21.expected.yaml new file mode 100644 index 0000000..0e739ce --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-21.expected.yaml @@ -0,0 +1,50 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + conditions: + - reason: ProviderTenantProcessing + message: "provider CAPTenant not ready for CAPApplication default.test-cap-01; waiting for it to be ready" + observedGeneration: 2 + status: "False" + type: Ready + observedGeneration: 2 + domainSpecHash: 84fdeebf558412817f54b17a0158c6b6d53dda98e8bc7a887aaa7170296841f8 + state: Processing diff --git a/internal/controller/testdata/capapplication/ca-21.initial.yaml b/internal/controller/testdata/capapplication/ca-21.initial.yaml new file mode 100644 index 0000000..bb22196 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-21.initial.yaml @@ -0,0 +1,43 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + state: "Processing" + domainSpecHash: 84fdeebf558412817f54b17a0158c6b6d53dda98e8bc7a887aaa7170296841f8 diff --git a/internal/controller/testdata/capapplication/ca-22.expected.yaml b/internal/controller/testdata/capapplication/ca-22.expected.yaml new file mode 100644 index 0000000..76ed81b --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-22.expected.yaml @@ -0,0 +1,53 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + conditions: + - reason: WaitingForReadyCAPApplicationVersion + message: "" + observedGeneration: 2 + status: "False" + type: Ready + - type: LatestVersionReady + reason: WaitingForReadyCAPApplicationVersion + observedGeneration: 2 + status: "False" + observedGeneration: 2 + state: Processing \ No newline at end of file diff --git a/internal/controller/testdata/capapplication/ca-22.initial.yaml b/internal/controller/testdata/capapplication/ca-22.initial.yaml new file mode 100644 index 0000000..74de667 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-22.initial.yaml @@ -0,0 +1,42 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + state: "Processing" \ No newline at end of file diff --git a/internal/controller/testdata/capapplication/ca-23.expected.yaml b/internal/controller/testdata/capapplication/ca-23.expected.yaml new file mode 100644 index 0000000..deb8a7d --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-23.expected.yaml @@ -0,0 +1,58 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + conditions: + - reason: VersionExists + message: "" + observedGeneration: 2 + status: "True" + type: Ready + - type: LatestVersionReady + reason: LatestVersionReady + observedGeneration: 2 + status: "True" + - type: AllTenantsReady + reason: AllTenantsReady + observedGeneration: 2 + status: "True" + observedGeneration: 2 + domainSpecHash: 84fdeebf558412817f54b17a0158c6b6d53dda98e8bc7a887aaa7170296841f8 + state: Consistent diff --git a/internal/controller/testdata/capapplication/ca-23.initial.yaml b/internal/controller/testdata/capapplication/ca-23.initial.yaml new file mode 100644 index 0000000..bb22196 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-23.initial.yaml @@ -0,0 +1,43 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + state: "Processing" + domainSpecHash: 84fdeebf558412817f54b17a0158c6b6d53dda98e8bc7a887aaa7170296841f8 diff --git a/internal/controller/testdata/capapplication/ca-24.expected.yaml b/internal/controller/testdata/capapplication/ca-24.expected.yaml new file mode 100644 index 0000000..c859a3d --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-24.expected.yaml @@ -0,0 +1,50 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + conditions: + - reason: ProcessingDomainResources + message: "" + observedGeneration: 2 + status: "False" + type: Ready + observedGeneration: 2 + domainSpecHash: 84fdeebf558412817f54b17a0158c6b6d53dda98e8bc7a887aaa7170296841f8 + state: Processing diff --git a/internal/controller/testdata/capapplication/ca-24.initial.yaml b/internal/controller/testdata/capapplication/ca-24.initial.yaml new file mode 100644 index 0000000..74de667 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-24.initial.yaml @@ -0,0 +1,42 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + state: "Processing" \ No newline at end of file diff --git a/internal/controller/testdata/capapplication/ca-25.expected.yaml b/internal/controller/testdata/capapplication/ca-25.expected.yaml new file mode 100644 index 0000000..c859a3d --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-25.expected.yaml @@ -0,0 +1,50 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + conditions: + - reason: ProcessingDomainResources + message: "" + observedGeneration: 2 + status: "False" + type: Ready + observedGeneration: 2 + domainSpecHash: 84fdeebf558412817f54b17a0158c6b6d53dda98e8bc7a887aaa7170296841f8 + state: Processing diff --git a/internal/controller/testdata/capapplication/ca-25.initial.yaml b/internal/controller/testdata/capapplication/ca-25.initial.yaml new file mode 100644 index 0000000..6a8e125 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-25.initial.yaml @@ -0,0 +1,43 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + state: "Processing" + domainSpecHash: oudated-hash diff --git a/internal/controller/testdata/capapplication/ca-26.expected.yaml b/internal/controller/testdata/capapplication/ca-26.expected.yaml new file mode 100644 index 0000000..6396dc8 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-26.expected.yaml @@ -0,0 +1,50 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + conditions: + - reason: ProviderTenantError + message: "provider CAPTenant in state ProvisioningError for CAPApplication default.test-cap-01" + observedGeneration: 2 + status: "False" + type: Ready + observedGeneration: 2 + domainSpecHash: 84fdeebf558412817f54b17a0158c6b6d53dda98e8bc7a887aaa7170296841f8 + state: Error diff --git a/internal/controller/testdata/capapplication/ca-26.initial.yaml b/internal/controller/testdata/capapplication/ca-26.initial.yaml new file mode 100644 index 0000000..bb22196 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-26.initial.yaml @@ -0,0 +1,43 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + state: "Processing" + domainSpecHash: 84fdeebf558412817f54b17a0158c6b6d53dda98e8bc7a887aaa7170296841f8 diff --git a/internal/controller/testdata/capapplication/ca-27.expected.yaml b/internal/controller/testdata/capapplication/ca-27.expected.yaml new file mode 100644 index 0000000..b196f9a --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-27.expected.yaml @@ -0,0 +1,50 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + conditions: + - reason: DomainResourcesError + message: "Certificate in state not ready for CAPApplication default.test-cap-01: cert message" + observedGeneration: 2 + status: "False" + type: Ready + observedGeneration: 2 + domainSpecHash: 84fdeebf558412817f54b17a0158c6b6d53dda98e8bc7a887aaa7170296841f8 + state: Error diff --git a/internal/controller/testdata/capapplication/ca-27.initial.yaml b/internal/controller/testdata/capapplication/ca-27.initial.yaml new file mode 100644 index 0000000..bb22196 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-27.initial.yaml @@ -0,0 +1,43 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + state: "Processing" + domainSpecHash: 84fdeebf558412817f54b17a0158c6b6d53dda98e8bc7a887aaa7170296841f8 diff --git a/internal/controller/testdata/capapplication/ca-28.expected.yaml b/internal/controller/testdata/capapplication/ca-28.expected.yaml new file mode 100644 index 0000000..d52d485 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-28.expected.yaml @@ -0,0 +1,50 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + conditions: + - reason: DomainResourcesError + message: "DNSEntry in state Error for CAPApplication default.test-cap-01: dns message" + observedGeneration: 2 + status: "False" + type: Ready + observedGeneration: 2 + domainSpecHash: 84fdeebf558412817f54b17a0158c6b6d53dda98e8bc7a887aaa7170296841f8 + state: Error diff --git a/internal/controller/testdata/capapplication/ca-28.initial.yaml b/internal/controller/testdata/capapplication/ca-28.initial.yaml new file mode 100644 index 0000000..bb22196 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-28.initial.yaml @@ -0,0 +1,43 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + state: "Processing" + domainSpecHash: 84fdeebf558412817f54b17a0158c6b6d53dda98e8bc7a887aaa7170296841f8 diff --git a/internal/controller/testdata/capapplication/ca-29.expected.yaml b/internal/controller/testdata/capapplication/ca-29.expected.yaml new file mode 100644 index 0000000..de70616 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-29.expected.yaml @@ -0,0 +1,55 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 0 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + state: Consistent + domainSpecHash: 84fdeebf558412817f54b17a0158c6b6d53dda98e8bc7a887aaa7170296841f8 + conditions: + - type: Ready + observedGeneration: 0 + status: "True" + - type: LatestVersionReady + reason: LatestVersionReady + observedGeneration: 0 + status: "True" + - type: AllTenantsReady + reason: AllTenantsReady + observedGeneration: 0 + status: "True" diff --git a/internal/controller/testdata/capapplication/ca-29.initial.yaml b/internal/controller/testdata/capapplication/ca-29.initial.yaml new file mode 100644 index 0000000..7dc67e4 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-29.initial.yaml @@ -0,0 +1,52 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 0 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + state: "Consistent" + domainSpecHash: 84fdeebf558412817f54b17a0158c6b6d53dda98e8bc7a887aaa7170296841f8 + conditions: + - type: Ready + status: "True" + - type: LatestVersionReady + reason: LatestVersionReady + status: "True" + - type: AllTenantsReady + reason: AllTenantsReady + status: "True" diff --git a/internal/controller/testdata/capapplication/ca-30.expected.yaml b/internal/controller/testdata/capapplication/ca-30.expected.yaml new file mode 100644 index 0000000..b44507e --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-30.expected.yaml @@ -0,0 +1,50 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 0 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + conditions: + - reason: ResourceDefinitionChanged + message: "re-processing after spec update" + observedGeneration: 0 + status: "False" + type: Ready + observedGeneration: 0 + state: Processing + domainSpecHash: 84fdeebf558412817f54b17a0158c6b6d53dda98e8bc7a887aaa7170296841f8 \ No newline at end of file diff --git a/internal/controller/testdata/capapplication/ca-30.initial.yaml b/internal/controller/testdata/capapplication/ca-30.initial.yaml new file mode 100644 index 0000000..dc0f678 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-30.initial.yaml @@ -0,0 +1,45 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 0 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + conditions: [] + observedGeneration: 999 + state: "Consistent" + domainSpecHash: 84fdeebf558412817f54b17a0158c6b6d53dda98e8bc7a887aaa7170296841f8 \ No newline at end of file diff --git a/internal/controller/testdata/capapplication/ca-31.expected.yaml b/internal/controller/testdata/capapplication/ca-31.expected.yaml new file mode 100644 index 0000000..2572052 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-31.expected.yaml @@ -0,0 +1,58 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 0 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + conditions: + - reason: NewCAVTriggeredTenantUpgrade + message: "new version default.test-cap-01-cav-v1-modified was used to trigger tenant upgrades" + observedGeneration: 0 + status: "False" + type: Ready + - type: LatestVersionReady + reason: LatestVersionReady + observedGeneration: 0 + status: "True" + - type: AllTenantsReady + reason: NotAllTenantsReady + observedGeneration: 0 + status: "False" + observedGeneration: 0 + state: Processing + domainSpecHash: 84fdeebf558412817f54b17a0158c6b6d53dda98e8bc7a887aaa7170296841f8 diff --git a/internal/controller/testdata/capapplication/ca-31.initial.yaml b/internal/controller/testdata/capapplication/ca-31.initial.yaml new file mode 100644 index 0000000..8ad6b34 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-31.initial.yaml @@ -0,0 +1,44 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 0 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + observedGeneration: 0 + state: "Consistent" + domainSpecHash: 84fdeebf558412817f54b17a0158c6b6d53dda98e8bc7a887aaa7170296841f8 \ No newline at end of file diff --git a/internal/controller/testdata/capapplication/ca-32.expected.yaml b/internal/controller/testdata/capapplication/ca-32.expected.yaml new file mode 100644 index 0000000..2572052 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-32.expected.yaml @@ -0,0 +1,58 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 0 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + conditions: + - reason: NewCAVTriggeredTenantUpgrade + message: "new version default.test-cap-01-cav-v1-modified was used to trigger tenant upgrades" + observedGeneration: 0 + status: "False" + type: Ready + - type: LatestVersionReady + reason: LatestVersionReady + observedGeneration: 0 + status: "True" + - type: AllTenantsReady + reason: NotAllTenantsReady + observedGeneration: 0 + status: "False" + observedGeneration: 0 + state: Processing + domainSpecHash: 84fdeebf558412817f54b17a0158c6b6d53dda98e8bc7a887aaa7170296841f8 diff --git a/internal/controller/testdata/capapplication/ca-32.initial.yaml b/internal/controller/testdata/capapplication/ca-32.initial.yaml new file mode 100644 index 0000000..8ad6b34 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-32.initial.yaml @@ -0,0 +1,44 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 0 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + observedGeneration: 0 + state: "Consistent" + domainSpecHash: 84fdeebf558412817f54b17a0158c6b6d53dda98e8bc7a887aaa7170296841f8 \ No newline at end of file diff --git a/internal/controller/testdata/capapplication/ca-33.expected.yaml b/internal/controller/testdata/capapplication/ca-33.expected.yaml new file mode 100644 index 0000000..5f386a2 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-33.expected.yaml @@ -0,0 +1,58 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 0 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + conditions: + - reason: NewCAVTriggeredTenantUpgrade + message: "new version default.test-cap-01-cav-v1 was used to trigger tenant upgrades" + observedGeneration: 0 + status: "False" + type: Ready + - type: LatestVersionReady + reason: LatestVersionReady + observedGeneration: 0 + status: "True" + - type: AllTenantsReady + reason: NotAllTenantsReady + observedGeneration: 0 + status: "False" + observedGeneration: 0 + state: Processing + domainSpecHash: 84fdeebf558412817f54b17a0158c6b6d53dda98e8bc7a887aaa7170296841f8 diff --git a/internal/controller/testdata/capapplication/ca-33.initial.yaml b/internal/controller/testdata/capapplication/ca-33.initial.yaml new file mode 100644 index 0000000..8ad6b34 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-33.initial.yaml @@ -0,0 +1,44 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 0 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + observedGeneration: 0 + state: "Consistent" + domainSpecHash: 84fdeebf558412817f54b17a0158c6b6d53dda98e8bc7a887aaa7170296841f8 \ No newline at end of file diff --git a/internal/controller/testdata/capapplication/ca-34.expected.yaml b/internal/controller/testdata/capapplication/ca-34.expected.yaml new file mode 100644 index 0000000..e2dc6b7 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-34.expected.yaml @@ -0,0 +1,43 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 0 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + observedGeneration: 0 + state: Processing diff --git a/internal/controller/testdata/capapplication/ca-34.initial.yaml b/internal/controller/testdata/capapplication/ca-34.initial.yaml new file mode 100644 index 0000000..6fd0396 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-34.initial.yaml @@ -0,0 +1,41 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + generation: 0 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + observedGeneration: 0 + state: "Processing" \ No newline at end of file diff --git a/internal/controller/testdata/capapplication/ca-35.expected.yaml b/internal/controller/testdata/capapplication/ca-35.expected.yaml new file mode 100644 index 0000000..d167f20 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-35.expected.yaml @@ -0,0 +1,47 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + deletionTimestamp: "2022-03-22T13:24:38Z" + generation: 0 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + conditions: + - reason: DeletionTriggered + observedGeneration: 0 + status: "False" + type: Ready + observedGeneration: 0 + state: Deleting diff --git a/internal/controller/testdata/capapplication/ca-35.initial.yaml b/internal/controller/testdata/capapplication/ca-35.initial.yaml new file mode 100644 index 0000000..61f5f77 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-35.initial.yaml @@ -0,0 +1,46 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + deletionTimestamp: "2022-03-22T13:24:38Z" + generation: 0 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + conditions: + - reason: DeletionTriggered + status: "False" + type: Ready + observedGeneration: 0 + state: Deleting \ No newline at end of file diff --git a/internal/controller/testdata/capapplication/ca-36.expected.yaml b/internal/controller/testdata/capapplication/ca-36.expected.yaml new file mode 100644 index 0000000..2e286cf --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-36.expected.yaml @@ -0,0 +1,50 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + deletionTimestamp: "2022-03-22T13:24:38Z" + generation: 0 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + conditions: + - reason: DeleteTriggered + message: "" + observedGeneration: 0 + status: "False" + type: Ready + observedGeneration: 0 + state: Deleting \ No newline at end of file diff --git a/internal/controller/testdata/capapplication/ca-36.initial.yaml b/internal/controller/testdata/capapplication/ca-36.initial.yaml new file mode 100644 index 0000000..49dfdd0 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-36.initial.yaml @@ -0,0 +1,44 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + deletionTimestamp: "2022-03-22T13:24:38Z" + generation: 0 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + observedGeneration: 0 + state: "Processing" \ No newline at end of file diff --git a/internal/controller/testdata/capapplication/ca-37.expected.yaml b/internal/controller/testdata/capapplication/ca-37.expected.yaml new file mode 100644 index 0000000..aa89b85 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-37.expected.yaml @@ -0,0 +1,48 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: [] + deletionTimestamp: "2022-03-22T13:24:38Z" + generation: 0 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + observedGeneration: 0 + state: Deleting + conditions: + - type: Ready + reason: DeletionTriggered + observedGeneration: 0 + status: "False" diff --git a/internal/controller/testdata/capapplication/ca-37.initial.yaml b/internal/controller/testdata/capapplication/ca-37.initial.yaml new file mode 100644 index 0000000..3630b65 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-37.initial.yaml @@ -0,0 +1,48 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + deletionTimestamp: "2022-03-22T13:24:38Z" + generation: 0 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + observedGeneration: 0 + state: "Deleting" + conditions: + - type: Ready + reason: DeletionTriggered + status: "False" diff --git a/internal/controller/testdata/capapplication/ca-38.expected.yaml b/internal/controller/testdata/capapplication/ca-38.expected.yaml new file mode 100644 index 0000000..1a4863d --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-38.expected.yaml @@ -0,0 +1,49 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + deletionTimestamp: "2022-03-22T13:24:38Z" + generation: 0 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + observedGeneration: 0 + state: Deleting + conditions: + - type: Ready + reason: DeletionTriggered + observedGeneration: 0 + status: "False" diff --git a/internal/controller/testdata/capapplication/ca-38.initial.yaml b/internal/controller/testdata/capapplication/ca-38.initial.yaml new file mode 100644 index 0000000..3630b65 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-38.initial.yaml @@ -0,0 +1,48 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + deletionTimestamp: "2022-03-22T13:24:38Z" + generation: 0 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + observedGeneration: 0 + state: "Deleting" + conditions: + - type: Ready + reason: DeletionTriggered + status: "False" diff --git a/internal/controller/testdata/capapplication/ca-39.expected.yaml b/internal/controller/testdata/capapplication/ca-39.expected.yaml new file mode 100644 index 0000000..1a4863d --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-39.expected.yaml @@ -0,0 +1,49 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + deletionTimestamp: "2022-03-22T13:24:38Z" + generation: 0 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + observedGeneration: 0 + state: Deleting + conditions: + - type: Ready + reason: DeletionTriggered + observedGeneration: 0 + status: "False" diff --git a/internal/controller/testdata/capapplication/ca-39.initial.yaml b/internal/controller/testdata/capapplication/ca-39.initial.yaml new file mode 100644 index 0000000..3630b65 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-39.initial.yaml @@ -0,0 +1,48 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + deletionTimestamp: "2022-03-22T13:24:38Z" + generation: 0 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + observedGeneration: 0 + state: "Deleting" + conditions: + - type: Ready + reason: DeletionTriggered + status: "False" diff --git a/internal/controller/testdata/capapplication/ca-40.expected.yaml b/internal/controller/testdata/capapplication/ca-40.expected.yaml new file mode 100644 index 0000000..aa89b85 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-40.expected.yaml @@ -0,0 +1,48 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: [] + deletionTimestamp: "2022-03-22T13:24:38Z" + generation: 0 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + observedGeneration: 0 + state: Deleting + conditions: + - type: Ready + reason: DeletionTriggered + observedGeneration: 0 + status: "False" diff --git a/internal/controller/testdata/capapplication/ca-40.initial.yaml b/internal/controller/testdata/capapplication/ca-40.initial.yaml new file mode 100644 index 0000000..3630b65 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-40.initial.yaml @@ -0,0 +1,48 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + deletionTimestamp: "2022-03-22T13:24:38Z" + generation: 0 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + observedGeneration: 0 + state: "Deleting" + conditions: + - type: Ready + reason: DeletionTriggered + status: "False" diff --git a/internal/controller/testdata/capapplication/ca-41.expected.yaml b/internal/controller/testdata/capapplication/ca-41.expected.yaml new file mode 100644 index 0000000..1a4863d --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-41.expected.yaml @@ -0,0 +1,49 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + deletionTimestamp: "2022-03-22T13:24:38Z" + generation: 0 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + observedGeneration: 0 + state: Deleting + conditions: + - type: Ready + reason: DeletionTriggered + observedGeneration: 0 + status: "False" diff --git a/internal/controller/testdata/capapplication/ca-41.initial.yaml b/internal/controller/testdata/capapplication/ca-41.initial.yaml new file mode 100644 index 0000000..3630b65 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-41.initial.yaml @@ -0,0 +1,48 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + deletionTimestamp: "2022-03-22T13:24:38Z" + generation: 0 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + observedGeneration: 0 + state: "Deleting" + conditions: + - type: Ready + reason: DeletionTriggered + status: "False" diff --git a/internal/controller/testdata/capapplication/ca-42.expected.yaml b/internal/controller/testdata/capapplication/ca-42.expected.yaml new file mode 100644 index 0000000..0577f23 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-42.expected.yaml @@ -0,0 +1,48 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + state: "Processing" + observedGeneration: 2 + conditions: + - type: Ready + reason: "ApplicationProcessing" + observedGeneration: 2 + status: "False" diff --git a/internal/controller/testdata/capapplication/ca-42.initial.yaml b/internal/controller/testdata/capapplication/ca-42.initial.yaml new file mode 100644 index 0000000..4aeaab1 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-42.initial.yaml @@ -0,0 +1,42 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + state: "" diff --git a/internal/controller/testdata/capapplication/ca-43.expected.yaml b/internal/controller/testdata/capapplication/ca-43.expected.yaml new file mode 100644 index 0000000..7b329b2 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-43.expected.yaml @@ -0,0 +1,50 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + conditions: + - reason: ProviderTenantError + message: "mocked api error (captenants.sme.sap.com/v1alpha1)" + observedGeneration: 2 + status: "False" + type: Ready + observedGeneration: 2 + domainSpecHash: 84fdeebf558412817f54b17a0158c6b6d53dda98e8bc7a887aaa7170296841f8 + state: Error diff --git a/internal/controller/testdata/capapplication/ca-43.initial.yaml b/internal/controller/testdata/capapplication/ca-43.initial.yaml new file mode 100644 index 0000000..1330a9b --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-43.initial.yaml @@ -0,0 +1,48 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + conditions: + - reason: ProcessingDomainResources + status: "False" + type: Ready + observedGeneration: 2 + domainSpecHash: 84fdeebf558412817f54b17a0158c6b6d53dda98e8bc7a887aaa7170296841f8 + state: Processing diff --git a/internal/controller/testdata/capapplication/ca-44.expected.yaml b/internal/controller/testdata/capapplication/ca-44.expected.yaml new file mode 100644 index 0000000..75d67c8 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-44.expected.yaml @@ -0,0 +1,49 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + conditions: + - reason: DomainResourcesProcessing + observedGeneration: 2 + status: "False" + type: Ready + observedGeneration: 2 + domainSpecHash: 84fdeebf558412817f54b17a0158c6b6d53dda98e8bc7a887aaa7170296841f8 + state: Processing diff --git a/internal/controller/testdata/capapplication/ca-45.expected.yaml b/internal/controller/testdata/capapplication/ca-45.expected.yaml new file mode 100644 index 0000000..414981d --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-45.expected.yaml @@ -0,0 +1,55 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 0 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + state: Consistent + domainSpecHash: 84fdeebf558412817f54b17a0158c6b6d53dda98e8bc7a887aaa7170296841f8 + conditions: + - type: Ready + observedGeneration: 0 + status: "True" + - type: LatestVersionReady + reason: LatestVersionReady + observedGeneration: 0 + status: "True" + - type: AllTenantsReady + reason: NotAllTenantsReady + observedGeneration: 0 + status: "False" diff --git a/internal/controller/testdata/capapplication/ca-45.initial.yaml b/internal/controller/testdata/capapplication/ca-45.initial.yaml new file mode 100644 index 0000000..2e07b2d --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-45.initial.yaml @@ -0,0 +1,52 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 0 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider +status: + state: "Consistent" + domainSpecHash: 84fdeebf558412817f54b17a0158c6b6d53dda98e8bc7a887aaa7170296841f8 + conditions: + - type: Ready + status: "True" + - type: LatestVersionReady + reason: LatestVersionReady + status: "True" + - type: AllTenantsReady + reason: NotAllTenantsReady + status: "False" diff --git a/internal/controller/testdata/capapplication/ca-dns-error.yaml b/internal/controller/testdata/capapplication/ca-dns-error.yaml new file mode 100644 index 0000000..8dbcfcc --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-dns-error.yaml @@ -0,0 +1,27 @@ +apiVersion: dns.gardener.cloud/v1alpha1 +kind: DNSEntry +metadata: + annotations: + dns.gardener.cloud/class: garden + sme.sap.com/resource-hash: be44dd98e914aa033f04f18a03338da45b40090b55e0e1c935353f088bd7c583 + sme.sap.com/owner-identifier: CAPTenant.default.test-cap-01-provider + labels: + sme.sap.com/owner-identifier-hash: ec24f9b09337c244cf5ac64b539f8c04f507cd99 + name: test-cap-01-primary-dns + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 +spec: + dnsName: "*.app-domain.test.local" + targets: + - public-ingress.operator.testing.local +status: + state: Error + message: "dns message" + targets: + - public-ingress.operator.testing.local + diff --git a/internal/controller/testdata/capapplication/ca-dns-not-ready.yaml b/internal/controller/testdata/capapplication/ca-dns-not-ready.yaml new file mode 100644 index 0000000..bc82540 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-dns-not-ready.yaml @@ -0,0 +1,23 @@ +apiVersion: dns.gardener.cloud/v1alpha1 +kind: DNSEntry +metadata: + annotations: + dns.gardener.cloud/class: garden + sme.sap.com/resource-hash: be44dd98e914aa033f04f18a03338da45b40090b55e0e1c935353f088bd7c583 + sme.sap.com/owner-identifier: CAPTenant.default.test-cap-01-provider + labels: + sme.sap.com/owner-identifier-hash: ec24f9b09337c244cf5ac64b539f8c04f507cd99 + name: test-cap-01-primary-dns + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 +spec: + dnsName: "*.app-domain.test.local" + targets: + - public-ingress.operator.testing.local +status: + state: Pending diff --git a/internal/controller/testdata/capapplication/ca-dns.yaml b/internal/controller/testdata/capapplication/ca-dns.yaml new file mode 100644 index 0000000..55b80c9 --- /dev/null +++ b/internal/controller/testdata/capapplication/ca-dns.yaml @@ -0,0 +1,26 @@ +apiVersion: dns.gardener.cloud/v1alpha1 +kind: DNSEntry +metadata: + annotations: + dns.gardener.cloud/class: garden + sme.sap.com/resource-hash: be44dd98e914aa033f04f18a03338da45b40090b55e0e1c935353f088bd7c583 + sme.sap.com/owner-identifier: CAPTenant.default.test-cap-01-provider + labels: + sme.sap.com/owner-identifier-hash: ec24f9b09337c244cf5ac64b539f8c04f507cd99 + name: test-cap-01-primary-dns + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 +spec: + dnsName: "*.app-domain.test.local" + targets: + - public-ingress.operator.testing.local +status: + state: Ready + targets: + - public-ingress.operator.testing.local + diff --git a/internal/controller/testdata/capapplication/cat-consumer-no-finalizers-ready.yaml b/internal/controller/testdata/capapplication/cat-consumer-no-finalizers-ready.yaml new file mode 100644 index 0000000..65fe433 --- /dev/null +++ b/internal/controller/testdata/capapplication/cat-consumer-no-finalizers-ready.yaml @@ -0,0 +1,35 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-consumer + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: consumer + name: test-cap-01-consumer + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-consumer + tenantId: tenant-id-for-consumer + version: 5.6.7 + versionUpgradeStrategy: always +status: + conditions: + - message: "CAPTenantOperation default.test-cap-01-provider-s6f4l successfully completed" + reason: ProvisioningCompleted + status: "True" + type: Ready + state: Ready + currentCAPApplicationVersionInstance: test-cap-01-cav-v1 diff --git a/internal/controller/testdata/capapplication/cat-consumer-ready.yaml b/internal/controller/testdata/capapplication/cat-consumer-ready.yaml new file mode 100644 index 0000000..1dbac75 --- /dev/null +++ b/internal/controller/testdata/capapplication/cat-consumer-ready.yaml @@ -0,0 +1,38 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + finalizers: + - sme.sap.com/captenant + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-consumer + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: consumer + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + name: test-cap-01-consumer + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-consumer + tenantId: tenant-id-for-consumer + version: 5.6.7 + versionUpgradeStrategy: always +status: + conditions: + - message: "CAPTenantOperation default.test-cap-01-provider-s6f4l successfully completed" + reason: ProvisioningCompleted + status: "True" + type: Ready + state: Ready + currentCAPApplicationVersionInstance: test-cap-01-cav-v1 diff --git a/internal/controller/testdata/capapplication/cat-consumer-upg-never-deleting.yaml b/internal/controller/testdata/capapplication/cat-consumer-upg-never-deleting.yaml new file mode 100644 index 0000000..3237674 --- /dev/null +++ b/internal/controller/testdata/capapplication/cat-consumer-upg-never-deleting.yaml @@ -0,0 +1,37 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-consumer + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: consumer + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + sme.sap.com/btp-tenant-id: "provider-tenant-id" + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + name: test-cap-01-consumer + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-consumer + tenantId: tenant-id-for-consumer + version: 5.6.7 + versionUpgradeStrategy: never +status: + state: Deleting + currentCAPApplicationVersionInstance: test-cap-01-cav-v1 + conditions: + - message: "waiting for CAPTenantOperation default.test-cap-01-consumer-xyz1 of type deprovisioning to complete" + reason: DeprovisioningOperationCreated + status: "False" + type: Ready diff --git a/internal/controller/testdata/capapplication/cat-consumer-upg-never-ready.yaml b/internal/controller/testdata/capapplication/cat-consumer-upg-never-ready.yaml new file mode 100644 index 0000000..0a72efc --- /dev/null +++ b/internal/controller/testdata/capapplication/cat-consumer-upg-never-ready.yaml @@ -0,0 +1,37 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-consumer + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: consumer + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + sme.sap.com/btp-tenant-id: "provider-tenant-id" + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + name: test-cap-01-consumer + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-consumer + tenantId: tenant-id-for-consumer + version: 5.6.7 + versionUpgradeStrategy: never +status: + state: Ready + currentCAPApplicationVersionInstance: test-cap-01-cav-v1 + conditions: + - message: "CAPTenantOperation default.test-cap-01-consumer-prvn successfully completed" + reason: ProvisioningCompleted + status: "True" + type: Ready diff --git a/internal/controller/testdata/capapplication/cat-provider-error.yaml b/internal/controller/testdata/capapplication/cat-provider-error.yaml new file mode 100644 index 0000000..5bd4ed3 --- /dev/null +++ b/internal/controller/testdata/capapplication/cat-provider-error.yaml @@ -0,0 +1,37 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + finalizers: + - sme.sap.com/captenant + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-provider + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: provider + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-provider + tenantId: tenant-id-for-provider + version: 5.6.7 + versionUpgradeStrategy: always +status: + conditions: + - message: "Error" + reason: ProvisioningError + status: "False" + type: Ready + state: ProvisioningError + currentCAPApplicationVersionInstance: test-cap-01-cav-v1 diff --git a/internal/controller/testdata/capapplication/cat-provider-no-finalizers-error.yaml b/internal/controller/testdata/capapplication/cat-provider-no-finalizers-error.yaml new file mode 100644 index 0000000..4881478 --- /dev/null +++ b/internal/controller/testdata/capapplication/cat-provider-no-finalizers-error.yaml @@ -0,0 +1,35 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-provider + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: provider + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-provider + tenantId: tenant-id-for-provider + version: 5.6.7 + versionUpgradeStrategy: always +status: + conditions: + - message: "Error" + reason: ProvisioningError + status: "False" + type: Ready + state: ProvisioningError + currentCAPApplicationVersionInstance: test-cap-01-cav-v1 diff --git a/internal/controller/testdata/capapplication/cat-provider-no-finalizers-ready.yaml b/internal/controller/testdata/capapplication/cat-provider-no-finalizers-ready.yaml new file mode 100644 index 0000000..af95ca1 --- /dev/null +++ b/internal/controller/testdata/capapplication/cat-provider-no-finalizers-ready.yaml @@ -0,0 +1,35 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-provider + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: provider + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-provider + tenantId: tenant-id-for-provider + version: 5.6.7 + versionUpgradeStrategy: always +status: + conditions: + - message: "CAPTenantOperation default.test-cap-01-provider-s6f4l successfully completed" + reason: ProvisioningCompleted + status: "True" + type: Ready + state: Ready + currentCAPApplicationVersionInstance: test-cap-01-cav-v1 diff --git a/internal/controller/testdata/capapplication/cat-provider-upgrade-error.yaml b/internal/controller/testdata/capapplication/cat-provider-upgrade-error.yaml new file mode 100644 index 0000000..786e6cd --- /dev/null +++ b/internal/controller/testdata/capapplication/cat-provider-upgrade-error.yaml @@ -0,0 +1,37 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + finalizers: + - sme.sap.com/captenant + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-provider + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: provider + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-provider + tenantId: tenant-id-for-provider + version: 5.6.7 + versionUpgradeStrategy: always +status: + conditions: + - message: "UpgradeError" + reason: UpgradeError + status: "False" + type: Ready + state: UpgradeError + currentCAPApplicationVersionInstance: test-cap-01-cav-v1 diff --git a/internal/controller/testdata/capapplication/cav-33-version-updated-ready.yaml b/internal/controller/testdata/capapplication/cav-33-version-updated-ready.yaml new file mode 100644 index 0000000..2c6b77c --- /dev/null +++ b/internal/controller/testdata/capapplication/cav-33-version-updated-ready.yaml @@ -0,0 +1,67 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-03-18T22:14:33Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 9.9.9 + workloads: + - name: cap-backend-srv + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: CAP + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/approuter/approuter:latest + type: Router + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + image: docker.image.repo/content/cap-content:latest + type: Content + - name: mtx-job + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + image: docker.image.repo/srv/server:latest + type: TenantOperation +status: + conditions: + - lastTransitionTime: "2022-03-18T23:07:47Z" + lastUpdateTime: "2022-03-18T23:07:47Z" + reason: CreatedDeployments + status: "True" + type: Ready + observedGeneration: 1 + state: Ready diff --git a/internal/controller/testdata/capapplication/cav-error.yaml b/internal/controller/testdata/capapplication/cav-error.yaml new file mode 100644 index 0000000..430f5ba --- /dev/null +++ b/internal/controller/testdata/capapplication/cav-error.yaml @@ -0,0 +1,67 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-03-18T22:14:33Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 5.6.7 + workloads: + - name: cap-backend-srv + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: CAP + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/approuter/approuter:latest + type: Router + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + image: docker.image.repo/content/cap-content:latest + type: Content + - name: mtx-job + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + image: docker.image.repo/srv/server:latest + type: TenantOperation +status: + conditions: + - lastTransitionTime: "2022-03-18T23:07:47Z" + lastUpdateTime: "2022-03-18T23:07:47Z" + reason: CreatedDeployments + status: "False" + type: Ready + observedGeneration: 1 + state: Error diff --git a/internal/controller/testdata/capapplication/cav-name-modified-ready.yaml b/internal/controller/testdata/capapplication/cav-name-modified-ready.yaml new file mode 100644 index 0000000..fa955f0 --- /dev/null +++ b/internal/controller/testdata/capapplication/cav-name-modified-ready.yaml @@ -0,0 +1,67 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-03-18T22:14:33Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1-modified + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 9.9.9 + workloads: + - name: cap-backend-srv + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: CAP + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/approuter/approuter:latest + type: Router + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + image: docker.image.repo/content/cap-content:latest + type: Content + - name: mtx-job + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + image: docker.image.repo/srv/server:latest + type: TenantOperation +status: + conditions: + - lastTransitionTime: "2022-03-18T23:07:47Z" + lastUpdateTime: "2022-03-18T23:07:47Z" + reason: CreatedDeployments + status: "True" + type: Ready + observedGeneration: 1 + state: Ready diff --git a/internal/controller/testdata/capapplication/gateway.yaml b/internal/controller/testdata/capapplication/gateway.yaml new file mode 100644 index 0000000..08b01d2 --- /dev/null +++ b/internal/controller/testdata/capapplication/gateway.yaml @@ -0,0 +1,30 @@ +apiVersion: networking.istio.io/v1beta1 +kind: Gateway +metadata: + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + name: test-cap-01-gw + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 +spec: + selector: + app: istio-ingressgateway + istio: ingressgateway + servers: + - hosts: + - "*.app-domain.test.local" + port: + name: app-domain.test.local + number: 443 + protocol: HTTPS + tls: + credentialName: default--test-cap-01-secret + mode: SIMPLE diff --git a/internal/controller/testdata/capapplication/istio-ingress-with-cert-error.yaml b/internal/controller/testdata/capapplication/istio-ingress-with-cert-error.yaml new file mode 100644 index 0000000..39f4d67 --- /dev/null +++ b/internal/controller/testdata/capapplication/istio-ingress-with-cert-error.yaml @@ -0,0 +1,65 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: istio-system +status: + phase: Active +--- +apiVersion: v1 +kind: Pod +metadata: + labels: + app: istio-ingressgateway + istio: ingressgateway + name: istio-ingressgateway-5dbdc4cdbb-pvgtp + namespace: istio-system +spec: + containers: + - image: istio.ingress.image.local:1.12.2 + imagePullPolicy: IfNotPresent + name: istio-proxy +--- +apiVersion: v1 +kind: Service +metadata: + annotations: + dns.gardener.cloud/class: garden + dns.gardener.cloud/dnsnames: public-ingress.operator.testing.local + dns.gardener.cloud/ttl: "600" + creationTimestamp: "2022-03-01T15:14:59Z" + finalizers: + - garden.dns.gardener.cloud/service-dns + name: istio-ingressgateway + namespace: istio-system + resourceVersion: "4876" + uid: ee535038-2f0f-4d9a-adbd-1ae05ba1e864 +spec: + ports: + - name: https + port: 443 + protocol: TCP + targetPort: 8443 + selector: + app: istio-ingressgateway + istio: ingressgateway + sessionAffinity: None + type: LoadBalancer +--- +apiVersion: cert.gardener.cloud/v1alpha1 +kind: Certificate +metadata: + name: test-cap-01-certificate + namespace: istio-system + annotations: + sme.sap.com/btp-app-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + finalizers: + - sme.sap.com/capapplication +spec: + commonname: "*.app-domain.test.local" + secretname: default--test-cap-01-secret +status: + observedGeneration: 0 + state: Error + message: "cert message" \ No newline at end of file diff --git a/internal/controller/testdata/capapplication/istio-ingress-with-cert-no-finalizers.yaml b/internal/controller/testdata/capapplication/istio-ingress-with-cert-no-finalizers.yaml new file mode 100644 index 0000000..cd8c9db --- /dev/null +++ b/internal/controller/testdata/capapplication/istio-ingress-with-cert-no-finalizers.yaml @@ -0,0 +1,62 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: istio-system +status: + phase: Active +--- +apiVersion: v1 +kind: Pod +metadata: + labels: + app: istio-ingressgateway + istio: ingressgateway + name: istio-ingressgateway-5dbdc4cdbb-pvgtp + namespace: istio-system +spec: + containers: + - image: istio.ingress.image.local:1.12.2 + imagePullPolicy: IfNotPresent + name: istio-proxy +--- +apiVersion: v1 +kind: Service +metadata: + annotations: + dns.gardener.cloud/class: garden + dns.gardener.cloud/dnsnames: public-ingress.operator.testing.local + dns.gardener.cloud/ttl: "600" + creationTimestamp: "2022-03-01T15:14:59Z" + name: istio-ingressgateway + namespace: istio-system + resourceVersion: "4876" + uid: ee535038-2f0f-4d9a-adbd-1ae05ba1e864 +spec: + ports: + - name: https + port: 443 + protocol: TCP + targetPort: 8443 + selector: + app: istio-ingressgateway + istio: ingressgateway + sessionAffinity: None + type: LoadBalancer +--- +apiVersion: cert.gardener.cloud/v1alpha1 +kind: Certificate +metadata: + name: test-cap-01-certificate + namespace: istio-system + annotations: + sme.sap.com/btp-app-identifier: default.test-cap-01 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 +spec: + commonname: "*.app-domain.test.local" + secretname: default--test-cap-01-secret +status: + observedGeneration: 0 + state: Ready \ No newline at end of file diff --git a/internal/controller/testdata/capapplication/istio-ingress-with-cert.yaml b/internal/controller/testdata/capapplication/istio-ingress-with-cert.yaml new file mode 100644 index 0000000..60ef1a0 --- /dev/null +++ b/internal/controller/testdata/capapplication/istio-ingress-with-cert.yaml @@ -0,0 +1,64 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: istio-system +status: + phase: Active +--- +apiVersion: v1 +kind: Pod +metadata: + labels: + app: istio-ingressgateway + istio: ingressgateway + name: istio-ingressgateway-5dbdc4cdbb-pvgtp + namespace: istio-system +spec: + containers: + - image: istio.ingress.image.local:1.12.2 + imagePullPolicy: IfNotPresent + name: istio-proxy +--- +apiVersion: v1 +kind: Service +metadata: + annotations: + dns.gardener.cloud/class: garden + dns.gardener.cloud/dnsnames: public-ingress.operator.testing.local + dns.gardener.cloud/ttl: "600" + creationTimestamp: "2022-03-01T15:14:59Z" + finalizers: + - garden.dns.gardener.cloud/service-dns + name: istio-ingressgateway + namespace: istio-system + resourceVersion: "4876" + uid: ee535038-2f0f-4d9a-adbd-1ae05ba1e864 +spec: + ports: + - name: https + port: 443 + protocol: TCP + targetPort: 8443 + selector: + app: istio-ingressgateway + istio: ingressgateway + sessionAffinity: None + type: LoadBalancer +--- +apiVersion: cert.gardener.cloud/v1alpha1 +kind: Certificate +metadata: + name: test-cap-01-certificate + namespace: istio-system + annotations: + sme.sap.com/btp-app-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + finalizers: + - sme.sap.com/capapplication +spec: + commonname: "*.app-domain.test.local" + secretname: default--test-cap-01-secret +status: + observedGeneration: 0 + state: Ready \ No newline at end of file diff --git a/internal/controller/testdata/capapplication/istio-ingress-with-certManager-error.yaml b/internal/controller/testdata/capapplication/istio-ingress-with-certManager-error.yaml new file mode 100644 index 0000000..e96e951 --- /dev/null +++ b/internal/controller/testdata/capapplication/istio-ingress-with-certManager-error.yaml @@ -0,0 +1,68 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: istio-system +status: + phase: Active +--- +apiVersion: v1 +kind: Pod +metadata: + labels: + app: istio-ingressgateway + istio: ingressgateway + name: istio-ingressgateway-5dbdc4cdbb-pvgtp + namespace: istio-system +spec: + containers: + - image: istio.ingress.image.local:1.12.2 + imagePullPolicy: IfNotPresent + name: istio-proxy +--- +apiVersion: v1 +kind: Service +metadata: + annotations: + dns.gardener.cloud/class: garden + dns.gardener.cloud/dnsnames: public-ingress.operator.testing.local + dns.gardener.cloud/ttl: "600" + creationTimestamp: "2022-03-01T15:14:59Z" + finalizers: + - garden.dns.gardener.cloud/service-dns + name: istio-ingressgateway + namespace: istio-system + resourceVersion: "4876" + uid: ee535038-2f0f-4d9a-adbd-1ae05ba1e864 +spec: + ports: + - name: https + port: 443 + protocol: TCP + targetPort: 8443 + selector: + app: istio-ingressgateway + istio: ingressgateway + sessionAffinity: None + type: LoadBalancer +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: test-cap-01-certificate + namespace: istio-system + annotations: + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + finalizers: + - sme.sap.com/capapplication +spec: + commonname: "*.app-domain.test.local" + secretname: default--test-cap-01-secret +status: + conditions: + - message: "cert message" + reason: "" + status: "False" + type: Ready + observedGeneration: 0 \ No newline at end of file diff --git a/internal/controller/testdata/capapplication/istio-ingress-with-certManager-no-finalizers.yaml b/internal/controller/testdata/capapplication/istio-ingress-with-certManager-no-finalizers.yaml new file mode 100644 index 0000000..9082b45 --- /dev/null +++ b/internal/controller/testdata/capapplication/istio-ingress-with-certManager-no-finalizers.yaml @@ -0,0 +1,66 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: istio-system +status: + phase: Active +--- +apiVersion: v1 +kind: Pod +metadata: + labels: + app: istio-ingressgateway + istio: ingressgateway + name: istio-ingressgateway-5dbdc4cdbb-pvgtp + namespace: istio-system +spec: + containers: + - image: istio.ingress.image.local:1.12.2 + imagePullPolicy: IfNotPresent + name: istio-proxy +--- +apiVersion: v1 +kind: Service +metadata: + annotations: + dns.gardener.cloud/class: garden + dns.gardener.cloud/dnsnames: public-ingress.operator.testing.local + dns.gardener.cloud/ttl: "600" + creationTimestamp: "2022-03-01T15:14:59Z" + finalizers: + - garden.dns.gardener.cloud/service-dns + name: istio-ingressgateway + namespace: istio-system + resourceVersion: "4876" + uid: ee535038-2f0f-4d9a-adbd-1ae05ba1e864 +spec: + ports: + - name: https + port: 443 + protocol: TCP + targetPort: 8443 + selector: + app: istio-ingressgateway + istio: ingressgateway + sessionAffinity: None + type: LoadBalancer +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: test-cap-01-certificate + namespace: istio-system + annotations: + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 +spec: + commonname: "*.app-domain.test.local" + secretname: default--test-cap-01-secret +status: + conditions: + - message: "cert message" + reason: "" + status: "True" + type: Ready + observedGeneration: 0 \ No newline at end of file diff --git a/internal/controller/testdata/capapplication/istio-ingress-with-certManager.yaml b/internal/controller/testdata/capapplication/istio-ingress-with-certManager.yaml new file mode 100644 index 0000000..cd2a049 --- /dev/null +++ b/internal/controller/testdata/capapplication/istio-ingress-with-certManager.yaml @@ -0,0 +1,68 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: istio-system +status: + phase: Active +--- +apiVersion: v1 +kind: Pod +metadata: + labels: + app: istio-ingressgateway + istio: ingressgateway + name: istio-ingressgateway-5dbdc4cdbb-pvgtp + namespace: istio-system +spec: + containers: + - image: istio.ingress.image.local:1.12.2 + imagePullPolicy: IfNotPresent + name: istio-proxy +--- +apiVersion: v1 +kind: Service +metadata: + annotations: + dns.gardener.cloud/class: garden + dns.gardener.cloud/dnsnames: public-ingress.operator.testing.local + dns.gardener.cloud/ttl: "600" + creationTimestamp: "2022-03-01T15:14:59Z" + finalizers: + - garden.dns.gardener.cloud/service-dns + name: istio-ingressgateway + namespace: istio-system + resourceVersion: "4876" + uid: ee535038-2f0f-4d9a-adbd-1ae05ba1e864 +spec: + ports: + - name: https + port: 443 + protocol: TCP + targetPort: 8443 + selector: + app: istio-ingressgateway + istio: ingressgateway + sessionAffinity: None + type: LoadBalancer +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: test-cap-01-certificate + namespace: istio-system + annotations: + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + finalizers: + - sme.sap.com/capapplication +spec: + commonname: "*.app-domain.test.local" + secretname: default--test-cap-01-secret +status: + conditions: + - message: "cert message" + reason: "" + status: "True" + type: Ready + observedGeneration: 0 \ No newline at end of file diff --git a/internal/controller/testdata/capapplication/istio-ingress-with-no-cert.yaml b/internal/controller/testdata/capapplication/istio-ingress-with-no-cert.yaml new file mode 100644 index 0000000..f4bb51d --- /dev/null +++ b/internal/controller/testdata/capapplication/istio-ingress-with-no-cert.yaml @@ -0,0 +1,46 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: istio-system +status: + phase: Active +--- +apiVersion: v1 +kind: Pod +metadata: + labels: + app: istio-ingressgateway + istio: ingressgateway + name: istio-ingressgateway-5dbdc4cdbb-pvgtp + namespace: istio-system +spec: + containers: + - image: istio.ingress.image.local:1.12.2 + imagePullPolicy: IfNotPresent + name: istio-proxy +--- +apiVersion: v1 +kind: Service +metadata: + annotations: + dns.gardener.cloud/class: garden + dns.gardener.cloud/dnsnames: public-ingress.operator.testing.local + dns.gardener.cloud/ttl: "600" + creationTimestamp: "2022-03-01T15:14:59Z" + finalizers: + - garden.dns.gardener.cloud/service-dns + name: istio-ingressgateway + namespace: istio-system + resourceVersion: "4876" + uid: ee535038-2f0f-4d9a-adbd-1ae05ba1e864 +spec: + ports: + - name: https + port: 443 + protocol: TCP + targetPort: 8443 + selector: + app: istio-ingressgateway + istio: ingressgateway + sessionAffinity: None + type: LoadBalancer diff --git a/internal/controller/testdata/capapplicationversion/cat-provider-version.yaml b/internal/controller/testdata/capapplicationversion/cat-provider-version.yaml new file mode 100644 index 0000000..3dbffd3 --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/cat-provider-version.yaml @@ -0,0 +1,31 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + finalizers: + - sme.sap.com/captenant + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-provider + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: provider + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-provider + tenantId: tenant-id-for-provider + version: 1.2.3 + versionUpgradeStrategy: always +status: + state: Upgrading diff --git a/internal/controller/testdata/capapplicationversion/cav-annotations.yaml b/internal/controller/testdata/capapplicationversion/cav-annotations.yaml new file mode 100644 index 0000000..7a2e0c3 --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/cav-annotations.yaml @@ -0,0 +1,170 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-07-18T06:13:52Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 1.2.3 + workloads: + - name: cap-backend-srv + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: CAP + env: + - name: HOST_IP + valueFrom: + fieldRef: + fieldPath: status.PodIp + ports: + - name: "app-port" + port: 4004 + routerDestinationName: "srv-api" + appProtocol: http + - name: "app-tech-port" + port: 4005 + networkPolicy: "Cluster" + livenessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4004 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + readinessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4004 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + resources: + limits: + cpu: 200m + memory: 500Mi + requests: + cpu: 20m + memory: 50Mi + - name: app-router + annotations: + foo: "bar" + sme.sap.com/some-custom-annotation: "some value for the annotation: 'sme.sap.com/some-custom-annotation'" + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/approuter/approuter:latest + type: Router + env: + - name: debug + valueFrom: + configMapKeyRef: + key: someKey + name: someCM + optional: true + ports: + - name: "router-port" + port: 4000 + appProtocol: http + - name: "router-tech-port" + port: 4004 + networkPolicy: "Cluster" + - name: "router-metrics-port" + port: 4007 + networkPolicy: "Cluster" + livenessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4000 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + readinessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4000 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + resources: + limits: + cpu: 200m + memory: 500Mi + requests: + cpu: 20m + memory: 50Mi + securityContext: + runAsUser: 1000 + runAsGroup: 2000 + podSecurityContext: + runAsUser: 2000 + fsGroup: 2000 + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + image: docker.image.repo/content/cap-content:latest + type: Content + env: + - name: SOME_VERSION + value: 0.0.1 + resources: + limits: + cpu: 100m + memory: 200Mi + requests: + cpu: 10m + memory: 20Mi + - name: mtx-job + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + image: docker.image.repo/srv/server:latest + type: TenantOperation + - name: job-worker + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: Additional + env: + - name: TEST_SEC_REF + valueFrom: + secretKeyRef: + key: someKey + name: someSecret + optional: true +status: + state: Processing diff --git a/internal/controller/testdata/capapplicationversion/cav-cluster-netpol-port.yaml b/internal/controller/testdata/capapplicationversion/cav-cluster-netpol-port.yaml new file mode 100644 index 0000000..73bc898 --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/cav-cluster-netpol-port.yaml @@ -0,0 +1,153 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-07-18T06:13:52Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 1.2.3 + workloads: + - name: cap-backend-srv + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: CAP + env: + - name: HOST_IP + valueFrom: + fieldRef: + fieldPath: status.PodIp + livenessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4004 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + readinessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4004 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + resources: + limits: + cpu: 200m + memory: 500Mi + requests: + cpu: 20m + memory: 50Mi + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/approuter/approuter:latest + type: Router + env: + - name: debug + valueFrom: + configMapKeyRef: + key: someKey + name: someCM + optional: true + ports: + - name: "router-port" + port: 4000 + appProtocol: http + - name: "router-tech-port" + port: 4004 + networkPolicy: "Cluster" + - name: "router-metrics-port" + port: 4007 + networkPolicy: "Cluster" + livenessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4000 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + readinessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4000 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + resources: + limits: + cpu: 200m + memory: 500Mi + requests: + cpu: 20m + memory: 50Mi + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + image: docker.image.repo/content/cap-content:latest + type: Content + env: + - name: SOME_VERSION + value: 0.0.1 + resources: + limits: + cpu: 100m + memory: 200Mi + requests: + cpu: 10m + memory: 20Mi + - name: mtx-job + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + image: docker.image.repo/srv/server:latest + type: TenantOperation + - name: job-worker + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: Additional + env: + - name: TEST_SEC_REF + valueFrom: + secretKeyRef: + key: someKey + name: someSecret + optional: true +status: + state: Processing diff --git a/internal/controller/testdata/capapplicationversion/cav-custom-destination-config.yaml b/internal/controller/testdata/capapplicationversion/cav-custom-destination-config.yaml new file mode 100644 index 0000000..58bc299 --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/cav-custom-destination-config.yaml @@ -0,0 +1,71 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-07-18T06:13:52Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 1.2.3 + workloads: + - name: cap-backend-srv + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: CAP + ports: + - name: server-app-port + port: 4000 + routerDestinationName: custom-srv-api + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/approuter/approuter:latest + type: Router + env: + - name: debug + valueFrom: + configMapKeyRef: + key: someKey + name: someCM + optional: true + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + image: docker.image.repo/content/cap-content:latest + type: Content + - name: mtx-job + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + image: docker.image.repo/srv/server:latest + type: TenantOperation +status: + state: Processing diff --git a/internal/controller/testdata/capapplicationversion/cav-custom-labels.yaml b/internal/controller/testdata/capapplicationversion/cav-custom-labels.yaml new file mode 100644 index 0000000..c977b3b --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/cav-custom-labels.yaml @@ -0,0 +1,101 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-07-18T06:13:52Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 1.2.3 + workloads: + - name: cap-backend-srv + labels: + foo: bar + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: CAP + env: + - name: HOST_IP + valueFrom: + fieldRef: + fieldPath: status.PodIp + - name: app-router + labels: + foo: bar + sme.sap.com/app-type: my-cap-operator-app + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/approuter/approuter:latest + type: Router + env: + - name: debug + valueFrom: + configMapKeyRef: + key: someKey + name: someCM + optional: true + - name: content-job + labels: + foo: bar + consumedBTPServices: + - cap-uaa + jobDefinition: + image: docker.image.repo/content/cap-content:latest + type: Content + env: + - name: SOME_VERSION + value: 0.0.1 + - name: mtx-job + labels: + foo: bar + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + image: docker.image.repo/srv/server:latest + type: TenantOperation + - name: job-worker + labels: + foo: bar + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: Additional + env: + - name: TEST_SEC_REF + valueFrom: + secretKeyRef: + key: someKey + name: someSecret + optional: true +status: + state: Processing diff --git a/internal/controller/testdata/capapplicationversion/cav-empty-status.yaml b/internal/controller/testdata/capapplicationversion/cav-empty-status.yaml new file mode 100644 index 0000000..881987b --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/cav-empty-status.yaml @@ -0,0 +1,48 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-07-18T06:13:52Z" + generation: 1 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier: default.test-cap-01 + name: test-cap-01-cav-v1 + namespace: default + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 1.2.3 + workloads: + - name: cap-backend-srv + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: CAP + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/approuter/approuter:latest + type: Router + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + image: docker.image.repo/content/cap-content:latest + type: Content + - name: mtx-job + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + image: docker.image.repo/srv/server:latest + type: TenantOperation diff --git a/internal/controller/testdata/capapplicationversion/cav-error-condition-status.yaml b/internal/controller/testdata/capapplicationversion/cav-error-condition-status.yaml new file mode 100644 index 0000000..c408ac0 --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/cav-error-condition-status.yaml @@ -0,0 +1,55 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-07-18T06:13:52Z" + generation: 1 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier: default.test-cap-01 + name: test-cap-01-cav-v1 + namespace: default + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 1.2.3 + workloads: + - name: cap-backend-srv + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: CAP + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/approuter/approuter:latest + type: Router + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + image: docker.image.repo/content/cap-content:latest + type: Content + - name: mtx-job + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + image: docker.image.repo/srv/server:latest + type: TenantOperation +status: + conditions: + - reason: Unknown + status: "False" + type: Ready + observedGeneration: 1 + state: Error diff --git a/internal/controller/testdata/capapplicationversion/cav-error-status.yaml b/internal/controller/testdata/capapplicationversion/cav-error-status.yaml new file mode 100644 index 0000000..2d8a23f --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/cav-error-status.yaml @@ -0,0 +1,50 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-07-18T06:13:52Z" + generation: 1 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier: default.test-cap-01 + name: test-cap-01-cav-v1 + namespace: default + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 1.2.3 + workloads: + - name: cap-backend-srv + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: CAP + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/approuter/approuter:latest + type: Router + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + image: docker.image.repo/content/cap-content:latest + type: Content + - name: mtx-job + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + image: docker.image.repo/srv/server:latest + type: TenantOperation +status: + state: Error diff --git a/internal/controller/testdata/capapplicationversion/cav-failed-content-job.yaml b/internal/controller/testdata/capapplicationversion/cav-failed-content-job.yaml new file mode 100644 index 0000000..b9be919 --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/cav-failed-content-job.yaml @@ -0,0 +1,70 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-07-18T06:13:52Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + finalizers: + - "sme.sap.com/capapplicationversion" + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 1.2.3 + workloads: + - name: cap-backend-srv + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: CAP + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/approuter/approuter:latest + type: Router + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + image: docker.image.repo/content/cap-content:latest + type: Content + - name: mtx-job + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + image: docker.image.repo/srv/server:latest + type: TenantOperation +status: + conditions: + - reason: ErrorInWorkloadStatus + status: "False" + type: Ready + message: "content deployer error in job 'test-cap-01-cav-v1-content'" + observedGeneration: 1 + finishedJobs: + - "test-cap-01-cav-v1-content" + state: Error diff --git a/internal/controller/testdata/capapplicationversion/cav-invalid-ca.yaml b/internal/controller/testdata/capapplicationversion/cav-invalid-ca.yaml new file mode 100644 index 0000000..bcdf5bd --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/cav-invalid-ca.yaml @@ -0,0 +1,44 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-07-18T06:13:52Z" + generation: 1 + name: test-cap-01-cav-v1 + namespace: default + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 1.2.3 + workloads: + - name: cap-backend-srv + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: CAP + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/approuter/approuter:latest + type: Router + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + image: docker.image.repo/content/cap-content:latest + type: Content + - name: mtx-job + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + image: docker.image.repo/srv/server:latest + type: TenantOperation diff --git a/internal/controller/testdata/capapplicationversion/cav-invalid-env-cap.yaml b/internal/controller/testdata/capapplicationversion/cav-invalid-env-cap.yaml new file mode 100644 index 0000000..8272340 --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/cav-invalid-env-cap.yaml @@ -0,0 +1,69 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-07-18T06:13:52Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 1.2.3 + workloads: + - name: cap-backend-srv + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: CAP + env: + - name: VCAP_SERVICES + value: "{'foo':'bar'}" + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/approuter/approuter:latest + type: Router + env: + - name: debug + value: "true" + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + image: docker.image.repo/content/cap-content:latest + type: Content + env: + - name: SOME_VERSION + value: 0.0.1 + - name: mtx-job + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + image: docker.image.repo/srv/server:latest + type: TenantOperation +status: + state: Processing diff --git a/internal/controller/testdata/capapplicationversion/cav-invalid-env-content.yaml b/internal/controller/testdata/capapplicationversion/cav-invalid-env-content.yaml new file mode 100644 index 0000000..15e97da --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/cav-invalid-env-content.yaml @@ -0,0 +1,63 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-07-18T06:13:52Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 1.2.3 + workloads: + - name: cap-backend-srv + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: CAP + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/approuter/approuter:latest + type: Router + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + image: docker.image.repo/content/cap-content:latest + type: Content + env: + - name: CAPOP_APP_VERSION + value: 0.0.1 + - name: mtx-job + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + image: docker.image.repo/srv/server:latest + type: TenantOperation +status: + state: Processing diff --git a/internal/controller/testdata/capapplicationversion/cav-invalid-env-job-worker.yaml b/internal/controller/testdata/capapplicationversion/cav-invalid-env-job-worker.yaml new file mode 100644 index 0000000..0cf6be0 --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/cav-invalid-env-job-worker.yaml @@ -0,0 +1,80 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-07-18T06:13:52Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 1.2.3 + workloads: + - name: cap-backend-srv + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: CAP + env: + - name: MISC_ENV + value: "{'foo':'bar'}" + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/approuter/approuter:latest + type: Router + env: + - name: debug + value: "true" + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + image: docker.image.repo/content/cap-content:latest + type: Content + env: + - name: SOME_VERSION + value: 0.0.1 + - name: mtx-job + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + image: docker.image.repo/srv/server:latest + type: TenantOperation + - name: job-worker + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: Additional + env: + - name: CAPOP_APP_VERSION + value: "1.2.3" +status: + state: Processing diff --git a/internal/controller/testdata/capapplicationversion/cav-merged-destinations-router.yaml b/internal/controller/testdata/capapplicationversion/cav-merged-destinations-router.yaml new file mode 100644 index 0000000..4676909 --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/cav-merged-destinations-router.yaml @@ -0,0 +1,66 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-07-18T06:13:52Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 1.2.3 + workloads: + - name: cap-backend-srv + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: CAP + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/approuter/approuter:latest + type: Router + env: + - name: destinations + value: '[{"name": "foo", "url": "http://foo.svc:5000", "strictSSL": true},{"name": "srv-api", "url": "http://some.domain", "timeout": 60000}]' + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + image: docker.image.repo/content/cap-content:latest + type: Content + env: + - name: SOME_VERSION + value: 0.0.1 + - name: mtx-job + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + image: docker.image.repo/srv/server:latest + type: TenantOperation +status: + state: Processing diff --git a/internal/controller/testdata/capapplicationversion/cav-pod-security-context.yaml b/internal/controller/testdata/capapplicationversion/cav-pod-security-context.yaml new file mode 100644 index 0000000..13da048 --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/cav-pod-security-context.yaml @@ -0,0 +1,167 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-07-18T06:13:52Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 1.2.3 + workloads: + - name: cap-backend-srv + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: CAP + env: + - name: HOST_IP + valueFrom: + fieldRef: + fieldPath: status.PodIp + ports: + - name: "app-port" + port: 4004 + routerDestinationName: "srv-api" + appProtocol: http + - name: "app-tech-port" + port: 4005 + networkPolicy: "Cluster" + livenessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4004 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + readinessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4004 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + resources: + limits: + cpu: 200m + memory: 500Mi + requests: + cpu: 20m + memory: 50Mi + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/approuter/approuter:latest + type: Router + env: + - name: debug + valueFrom: + configMapKeyRef: + key: someKey + name: someCM + optional: true + ports: + - name: "router-port" + port: 4000 + appProtocol: http + - name: "router-tech-port" + port: 4004 + networkPolicy: "Cluster" + - name: "router-metrics-port" + port: 4007 + networkPolicy: "Cluster" + livenessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4000 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + readinessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4000 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + resources: + limits: + cpu: 200m + memory: 500Mi + requests: + cpu: 20m + memory: 50Mi + securityContext: + runAsUser: 1000 + runAsGroup: 2000 + podSecurityContext: + runAsUser: 2000 + fsGroup: 2000 + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + image: docker.image.repo/content/cap-content:latest + type: Content + env: + - name: SOME_VERSION + value: 0.0.1 + resources: + limits: + cpu: 100m + memory: 200Mi + requests: + cpu: 10m + memory: 20Mi + - name: mtx-job + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + image: docker.image.repo/srv/server:latest + type: TenantOperation + - name: job-worker + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: Additional + env: + - name: TEST_SEC_REF + valueFrom: + secretKeyRef: + key: someKey + name: someSecret + optional: true +status: + state: Processing diff --git a/internal/controller/testdata/capapplicationversion/cav-probes-and-resources.yaml b/internal/controller/testdata/capapplicationversion/cav-probes-and-resources.yaml new file mode 100644 index 0000000..3d9c2c0 --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/cav-probes-and-resources.yaml @@ -0,0 +1,143 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-07-18T06:13:52Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 1.2.3 + workloads: + - name: cap-backend-srv + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: CAP + env: + - name: HOST_IP + valueFrom: + fieldRef: + fieldPath: status.PodIp + livenessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4004 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + readinessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4004 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + resources: + limits: + cpu: 200m + memory: 500Mi + requests: + cpu: 20m + memory: 50Mi + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/approuter/approuter:latest + type: Router + env: + - name: debug + valueFrom: + configMapKeyRef: + key: someKey + name: someCM + optional: true + livenessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4000 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + readinessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4000 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + resources: + limits: + cpu: 200m + memory: 500Mi + requests: + cpu: 20m + memory: 50Mi + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + image: docker.image.repo/content/cap-content:latest + type: Content + env: + - name: SOME_VERSION + value: 0.0.1 + resources: + limits: + cpu: 100m + memory: 200Mi + requests: + cpu: 10m + memory: 20Mi + - name: mtx-job + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + image: docker.image.repo/srv/server:latest + type: TenantOperation + - name: job-worker + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: Additional + env: + - name: TEST_SEC_REF + valueFrom: + secretKeyRef: + key: someKey + name: someSecret + optional: true +status: + state: Processing diff --git a/internal/controller/testdata/capapplicationversion/cav-processing-job-finished.yaml b/internal/controller/testdata/capapplicationversion/cav-processing-job-finished.yaml new file mode 100644 index 0000000..9f6a18e --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/cav-processing-job-finished.yaml @@ -0,0 +1,67 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-07-18T06:13:52Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 1.2.3 + workloads: + - name: cap-backend-srv + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: CAP + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/approuter/approuter:latest + type: Router + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + image: docker.image.repo/content/cap-content:latest + type: Content + - name: mtx-job + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + image: docker.image.repo/srv/server:latest + type: TenantOperation +status: + conditions: + - reason: ReadyForProcessing + status: "False" + type: Ready + observedGeneration: 1 + finishedJobs: + - "test-cap-01-cav-v1-content" + state: Processing diff --git a/internal/controller/testdata/capapplicationversion/cav-processing.yaml b/internal/controller/testdata/capapplicationversion/cav-processing.yaml new file mode 100644 index 0000000..7d0ed17 --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/cav-processing.yaml @@ -0,0 +1,66 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-07-18T06:13:52Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 1.2.3 + workloads: + - name: cap-backend-srv + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: CAP + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/approuter/approuter:latest + type: Router + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + image: docker.image.repo/content/cap-content:latest + type: Content + - name: mtx-job + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + image: docker.image.repo/srv/server:latest + type: TenantOperation +status: + conditions: + - reason: ReadyForProcessing + status: "False" + observedGeneration: 1 + type: Ready + observedGeneration: 1 + state: Processing diff --git a/internal/controller/testdata/capapplicationversion/cav-ready-deleting.yaml b/internal/controller/testdata/capapplicationversion/cav-ready-deleting.yaml new file mode 100644 index 0000000..622efcc --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/cav-ready-deleting.yaml @@ -0,0 +1,68 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-07-18T06:13:52Z" + deletionTimestamp: "2022-07-18T16:04:15Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + finalizers: + - "sme.sap.com/capapplicationversion" + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 1.2.3 + workloads: + - name: cap-backend-srv + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: CAP + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/approuter/approuter:latest + type: Router + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + image: docker.image.repo/content/cap-content:latest + type: Content + - name: mtx-job + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + image: docker.image.repo/srv/server:latest + type: TenantOperation +status: + conditions: + - reason: CreatedDeployments + status: "True" + type: Ready + observedGeneration: 1 + state: Ready diff --git a/internal/controller/testdata/capapplicationversion/cav-security-context.yaml b/internal/controller/testdata/capapplicationversion/cav-security-context.yaml new file mode 100644 index 0000000..7bc8dbc --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/cav-security-context.yaml @@ -0,0 +1,156 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-07-18T06:13:52Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 1.2.3 + workloads: + - name: cap-backend-srv + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: CAP + env: + - name: HOST_IP + valueFrom: + fieldRef: + fieldPath: status.PodIp + livenessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4004 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + readinessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4004 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + resources: + limits: + cpu: 200m + memory: 500Mi + requests: + cpu: 20m + memory: 50Mi + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/approuter/approuter:latest + type: Router + env: + - name: debug + valueFrom: + configMapKeyRef: + key: someKey + name: someCM + optional: true + ports: + - name: "router-port" + port: 4000 + appProtocol: http + - name: "router-tech-port" + port: 4004 + networkPolicy: "Cluster" + - name: "router-metrics-port" + port: 4007 + networkPolicy: "Cluster" + livenessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4000 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + readinessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4000 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + resources: + limits: + cpu: 200m + memory: 500Mi + requests: + cpu: 20m + memory: 50Mi + securityContext: + runAsUser: 1000 + runAsGroup: 2000 + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + image: docker.image.repo/content/cap-content:latest + type: Content + env: + - name: SOME_VERSION + value: 0.0.1 + resources: + limits: + cpu: 100m + memory: 200Mi + requests: + cpu: 10m + memory: 20Mi + - name: mtx-job + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + image: docker.image.repo/srv/server:latest + type: TenantOperation + - name: job-worker + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: Additional + env: + - name: TEST_SEC_REF + valueFrom: + secretKeyRef: + key: someKey + name: someSecret + optional: true +status: + state: Processing diff --git a/internal/controller/testdata/capapplicationversion/cav-unknown-deleting.yaml b/internal/controller/testdata/capapplicationversion/cav-unknown-deleting.yaml new file mode 100644 index 0000000..e178d76 --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/cav-unknown-deleting.yaml @@ -0,0 +1,59 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-07-18T06:13:52Z" + deletionTimestamp: "2022-07-18T16:04:15Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 2.3.4 + workloads: + - name: cap-backend-srv + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: CAP + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/approuter/approuter:latest + type: Router + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + image: docker.image.repo/content/cap-content:latest + type: Content + - name: mtx-job + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + image: docker.image.repo/srv/server:latest + type: TenantOperation diff --git a/internal/controller/testdata/capapplicationversion/cav-valid-env-config.yaml b/internal/controller/testdata/capapplicationversion/cav-valid-env-config.yaml new file mode 100644 index 0000000..932ca64 --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/cav-valid-env-config.yaml @@ -0,0 +1,91 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-07-18T06:13:52Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 1.2.3 + workloads: + - name: cap-backend-srv + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: CAP + env: + - name: HOST_IP + valueFrom: + fieldRef: + fieldPath: status.PodIp + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/approuter/approuter:latest + imagePullPolicy: Always + type: Router + env: + - name: debug + valueFrom: + configMapKeyRef: + key: someKey + name: someCM + optional: true + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + image: docker.image.repo/content/cap-content:latest + type: Content + env: + - name: SOME_VERSION + value: 0.0.1 + - name: mtx-job + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + image: docker.image.repo/srv/server:latest + type: TenantOperation + - name: job-worker + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: Additional + env: + - name: TEST_SEC_REF + valueFrom: + secretKeyRef: + key: someKey + name: someSecret + optional: true +status: + state: Processing diff --git a/internal/controller/testdata/capapplicationversion/content-job-completed.yaml b/internal/controller/testdata/capapplicationversion/content-job-completed.yaml new file mode 100644 index 0000000..c48fade --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/content-job-completed.yaml @@ -0,0 +1,71 @@ +apiVersion: batch/v1 +kind: Job +metadata: + creationTimestamp: "2022-07-18T12:16:21Z" + labels: + job-name: test-cap-01-cav-v1-content + name: test-cap-01-cav-v1-content + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplicationVersion + name: test-cap-01-cav-v1-content + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 + resourceVersion: "150625273" + uid: afb8bcad-72ce-4567-8337-12fc67ef55acb +spec: + backoffLimit: 2 + completions: 1 + parallelism: 1 + selector: + matchLabels: + controller-uid: afb8bcad-72ce-4567-8337-12fc67ef55acb + template: + metadata: + annotations: + sidecar.istio.io/inject: "false" + creationTimestamp: null + labels: + controller-uid: afb8bcad-72ce-4567-8337-12fc67ef55acb + job-name: test-cap-01-cav-v1-content + x4.sap.com/disable-karydia: "true" + spec: + containers: + - env: + - name: CAPOP_APP_VERSION + value: "14" + - name: TEST + value: Dummy + - name: HOST_IP + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: status.podIP + envFrom: + - secretRef: + name: test-cap-01-cav-v1-content-gen + optional: true + image: bem.some.repo.example.com/content/bem-content + imagePullPolicy: Always + name: content-deploy + resources: {} + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + dnsPolicy: ClusterFirst + imagePullSecrets: + - name: regcred + restartPolicy: OnFailure + schedulerName: default-scheduler + securityContext: {} + terminationGracePeriodSeconds: 30 +status: + completionTime: "2022-07-18T12:16:41Z" + conditions: + - lastProbeTime: "2022-07-18T12:16:41Z" + lastTransitionTime: "2022-07-18T12:16:41Z" + status: "True" + type: Complete + startTime: "2022-07-14T12:16:21Z" + succeeded: 1 diff --git a/internal/controller/testdata/capapplicationversion/content-job-failed.yaml b/internal/controller/testdata/capapplicationversion/content-job-failed.yaml new file mode 100644 index 0000000..9432e0f --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/content-job-failed.yaml @@ -0,0 +1,72 @@ +apiVersion: batch/v1 +kind: Job +metadata: + creationTimestamp: "2022-07-18T12:16:21Z" + labels: + job-name: test-cap-01-cav-v1-content + name: test-cap-01-cav-v1-content + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplicationVersion + name: test-cap-01-cav-v1-content + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 + resourceVersion: "150625273" + uid: afb8bcad-72ce-4567-8337-12fc67ef55acb +spec: + backoffLimit: 2 + completions: 1 + parallelism: 1 + selector: + matchLabels: + controller-uid: afb8bcad-72ce-4567-8337-12fc67ef55acb + template: + metadata: + annotations: + sidecar.istio.io/inject: "false" + creationTimestamp: null + labels: + controller-uid: afb8bcad-72ce-4567-8337-12fc67ef55acb + job-name: test-cap-01-cav-v1-content + x4.sap.com/disable-karydia: "true" + spec: + containers: + - env: + - name: CAPOP_APP_VERSION + value: "14" + - name: TEST + value: Dummy + - name: HOST_IP + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: status.podIP + envFrom: + - secretRef: + name: test-cap-01-cav-v1-content-gen + optional: true + image: bem.some.repo.example.com/content/bem-content + imagePullPolicy: Always + name: content-deploy + resources: {} + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + dnsPolicy: ClusterFirst + imagePullSecrets: + - name: regcred + restartPolicy: OnFailure + schedulerName: default-scheduler + securityContext: {} + terminationGracePeriodSeconds: 30 +status: + completionTime: "2022-07-18T12:16:41Z" + conditions: + - lastProbeTime: "2022-07-18T12:16:41Z" + lastTransitionTime: "2022-07-18T12:16:41Z" + status: "True" + type: Failed + message: "content deployer error in job 'test-cap-01-cav-v1-content'" + startTime: "2022-07-14T12:16:21Z" + succeeded: 0 diff --git a/internal/controller/testdata/capapplicationversion/content-job-pending.yaml b/internal/controller/testdata/capapplicationversion/content-job-pending.yaml new file mode 100644 index 0000000..142fe3c --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/content-job-pending.yaml @@ -0,0 +1,69 @@ +apiVersion: batch/v1 +kind: Job +metadata: + creationTimestamp: "2022-07-18T12:16:21Z" + labels: + job-name: test-cap-01-cav-v1-content + name: test-cap-01-cav-v1-content + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplicationVersion + name: test-cap-01-cav-v1-content + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 + resourceVersion: "150625273" + uid: afb8bcad-72ce-4567-8337-12fc67ef55acb +spec: + backoffLimit: 2 + completions: 1 + parallelism: 1 + selector: + matchLabels: + controller-uid: afb8bcad-72ce-4567-8337-12fc67ef55acb + template: + metadata: + annotations: + sidecar.istio.io/inject: "false" + creationTimestamp: null + labels: + controller-uid: afb8bcad-72ce-4567-8337-12fc67ef55acb + job-name: test-cap-01-cav-v1-content + x4.sap.com/disable-karydia: "true" + spec: + containers: + - env: + - name: CAPOP_APP_VERSION + value: "14" + - name: TEST + value: Dummy + - name: HOST_IP + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: status.podIP + envFrom: + - secretRef: + name: test-cap-01-cav-v1-content-gen + optional: true + image: bem.some.repo.example.com/content/bem-content + imagePullPolicy: Always + name: content-deploy + resources: {} + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + dnsPolicy: ClusterFirst + imagePullSecrets: + - name: regcred + restartPolicy: OnFailure + schedulerName: default-scheduler + securityContext: {} + terminationGracePeriodSeconds: 30 +status: + conditions: + - lastProbeTime: "2022-07-18T12:16:41Z" + lastTransitionTime: "2022-07-18T12:16:41Z" + status: "False" + type: "" + startTime: "2022-07-14T12:16:21Z" diff --git a/internal/controller/testdata/capapplicationversion/expected/cav-deleted-unknown.yaml b/internal/controller/testdata/capapplicationversion/expected/cav-deleted-unknown.yaml new file mode 100644 index 0000000..62f3383 --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/expected/cav-deleted-unknown.yaml @@ -0,0 +1,67 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-07-18T06:13:52Z" + deletionTimestamp: "2022-07-18T16:04:15Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 2.3.4 + workloads: + - name: cap-backend-srv + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: CAP + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/approuter/approuter:latest + type: Router + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + image: docker.image.repo/content/cap-content:latest + type: Content + - name: mtx-job + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + image: docker.image.repo/srv/server:latest + type: TenantOperation +status: + conditions: + - reason: DeleteTriggered + observedGeneration: 1 + status: "False" + type: Ready + observedGeneration: 1 + state: Deleting diff --git a/internal/controller/testdata/capapplicationversion/expected/cav-deleted.yaml b/internal/controller/testdata/capapplicationversion/expected/cav-deleted.yaml new file mode 100644 index 0000000..baee208 --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/expected/cav-deleted.yaml @@ -0,0 +1,68 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-07-18T06:13:52Z" + deletionTimestamp: "2022-07-18T16:04:15Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + finalizers: [] + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 1.2.3 + workloads: + - name: cap-backend-srv + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: CAP + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/approuter/approuter:latest + type: Router + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + image: docker.image.repo/content/cap-content:latest + type: Content + - name: mtx-job + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + image: docker.image.repo/srv/server:latest + type: TenantOperation +status: + conditions: + - reason: DeleteTriggered + observedGeneration: 1 + status: "True" + type: Ready + observedGeneration: 1 + state: Deleting diff --git a/internal/controller/testdata/capapplicationversion/expected/cav-deleting.yaml b/internal/controller/testdata/capapplicationversion/expected/cav-deleting.yaml new file mode 100644 index 0000000..96254e8 --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/expected/cav-deleting.yaml @@ -0,0 +1,69 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-07-18T06:13:52Z" + deletionTimestamp: "2022-07-18T16:04:15Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + finalizers: + - "sme.sap.com/capapplicationversion" + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 1.2.3 + workloads: + - name: cap-backend-srv + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: CAP + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/approuter/approuter:latest + type: Router + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + image: docker.image.repo/content/cap-content:latest + type: Content + - name: mtx-job + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + image: docker.image.repo/srv/server:latest + type: TenantOperation +status: + conditions: + - reason: DeleteTriggered + observedGeneration: 1 + status: "True" + type: Ready + observedGeneration: 1 + state: Deleting diff --git a/internal/controller/testdata/capapplicationversion/expected/cav-error-condition-processing.yaml b/internal/controller/testdata/capapplicationversion/expected/cav-error-condition-processing.yaml new file mode 100644 index 0000000..65cbc43 --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/expected/cav-error-condition-processing.yaml @@ -0,0 +1,68 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-07-18T06:13:52Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + finalizers: + - "sme.sap.com/capapplicationversion" + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 1.2.3 + workloads: + - name: cap-backend-srv + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: CAP + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/approuter/approuter:latest + type: Router + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + image: docker.image.repo/content/cap-content:latest + type: Content + - name: mtx-job + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + image: docker.image.repo/srv/server:latest + type: TenantOperation +status: + conditions: + - reason: Unknown + observedGeneration: 1 + status: "False" + type: Ready + observedGeneration: 1 + state: Processing diff --git a/internal/controller/testdata/capapplicationversion/expected/cav-error-processing.yaml b/internal/controller/testdata/capapplicationversion/expected/cav-error-processing.yaml new file mode 100644 index 0000000..fdf602a --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/expected/cav-error-processing.yaml @@ -0,0 +1,68 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-07-18T06:13:52Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + finalizers: + - "sme.sap.com/capapplicationversion" + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 1.2.3 + workloads: + - name: cap-backend-srv + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: CAP + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/approuter/approuter:latest + type: Router + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + image: docker.image.repo/content/cap-content:latest + type: Content + - name: mtx-job + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + image: docker.image.repo/srv/server:latest + type: TenantOperation +status: + conditions: + - reason: RetryProcessing + observedGeneration: 1 + status: "False" + type: Ready + observedGeneration: 1 + state: Processing diff --git a/internal/controller/testdata/capapplicationversion/expected/cav-failed-content-job.yaml b/internal/controller/testdata/capapplicationversion/expected/cav-failed-content-job.yaml new file mode 100644 index 0000000..c59df00 --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/expected/cav-failed-content-job.yaml @@ -0,0 +1,71 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-07-18T06:13:52Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + finalizers: + - "sme.sap.com/capapplicationversion" + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 1.2.3 + workloads: + - name: cap-backend-srv + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: CAP + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/approuter/approuter:latest + type: Router + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + image: docker.image.repo/content/cap-content:latest + type: Content + - name: mtx-job + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + image: docker.image.repo/srv/server:latest + type: TenantOperation +status: + conditions: + - reason: ErrorInWorkloadStatus + observedGeneration: 1 + status: "False" + type: Ready + message: "content deployer error in job 'test-cap-01-cav-v1-content'" + observedGeneration: 1 + finishedJobs: + - "test-cap-01-cav-v1-content" + state: Error diff --git a/internal/controller/testdata/capapplicationversion/expected/cav-failed-env-cap.yaml b/internal/controller/testdata/capapplicationversion/expected/cav-failed-env-cap.yaml new file mode 100644 index 0000000..35442ac --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/expected/cav-failed-env-cap.yaml @@ -0,0 +1,78 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-07-18T06:13:52Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + finalizers: + - "sme.sap.com/capapplicationversion" + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 1.2.3 + workloads: + - name: cap-backend-srv + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: CAP + env: + - name: VCAP_SERVICES + value: "{'foo':'bar'}" + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/approuter/approuter:latest + type: Router + env: + - name: debug + value: "true" + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + image: docker.image.repo/content/cap-content:latest + type: Content + env: + - name: SOME_VERSION + value: 0.0.1 + - name: mtx-job + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + image: docker.image.repo/srv/server:latest + type: TenantOperation +status: + conditions: + - reason: ErrorInServerDeployment + observedGeneration: 1 + message: "invalid env configuration for workload: cap-backend-srv, remove entry: VCAP_SERVICES from configuration" + status: "False" + type: Ready + observedGeneration: 1 + state: Error diff --git a/internal/controller/testdata/capapplicationversion/expected/cav-failed-env-content.yaml b/internal/controller/testdata/capapplicationversion/expected/cav-failed-env-content.yaml new file mode 100644 index 0000000..b5e421d --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/expected/cav-failed-env-content.yaml @@ -0,0 +1,72 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-07-18T06:13:52Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + finalizers: + - "sme.sap.com/capapplicationversion" + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 1.2.3 + workloads: + - name: cap-backend-srv + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: CAP + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/approuter/approuter:latest + type: Router + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + image: docker.image.repo/content/cap-content:latest + type: Content + env: + - name: CAPOP_APP_VERSION + value: 0.0.1 + - name: mtx-job + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + image: docker.image.repo/srv/server:latest + type: TenantOperation +status: + conditions: + - reason: ErrorInContentDeploymentJob + message: "invalid env configuration for workload: content-job, remove entry: CAPOP_APP_VERSION from configuration" + observedGeneration: 1 + status: "False" + type: Ready + observedGeneration: 1 + state: Error diff --git a/internal/controller/testdata/capapplicationversion/expected/cav-failed-env-job-worker.yaml b/internal/controller/testdata/capapplicationversion/expected/cav-failed-env-job-worker.yaml new file mode 100644 index 0000000..49941a1 --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/expected/cav-failed-env-job-worker.yaml @@ -0,0 +1,89 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-07-18T06:13:52Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + finalizers: + - "sme.sap.com/capapplicationversion" + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 1.2.3 + workloads: + - name: cap-backend-srv + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: CAP + env: + - name: MISC_ENV + value: "{'foo':'bar'}" + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/approuter/approuter:latest + type: Router + env: + - name: debug + value: "true" + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + image: docker.image.repo/content/cap-content:latest + type: Content + env: + - name: SOME_VERSION + value: 0.0.1 + - name: mtx-job + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + image: docker.image.repo/srv/server:latest + type: TenantOperation + - name: job-worker + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: Additional + env: + - name: CAPOP_APP_VERSION + value: "1.2.3" +status: + conditions: + - reason: ErrorInJobWorkerDeployment + message: "invalid env configuration for workload: job-worker, remove entry: CAPOP_APP_VERSION from configuration" + observedGeneration: 1 + status: "False" + type: Ready + observedGeneration: 1 + state: Error diff --git a/internal/controller/testdata/capapplicationversion/expected/cav-missing-content-job.yaml b/internal/controller/testdata/capapplicationversion/expected/cav-missing-content-job.yaml new file mode 100644 index 0000000..21dcd80 --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/expected/cav-missing-content-job.yaml @@ -0,0 +1,69 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-07-18T06:13:52Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + finalizers: + - "sme.sap.com/capapplicationversion" + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 1.2.3 + workloads: + - name: cap-backend-srv + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: CAP + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/approuter/approuter:latest + type: Router + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + image: docker.image.repo/content/cap-content:latest + type: Content + - name: mtx-job + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + image: docker.image.repo/srv/server:latest + type: TenantOperation +status: + conditions: + - reason: ErrorInWorkloadStatus + message: 'job.batch "test-cap-01-cav-v1-content" not found' + observedGeneration: 1 + status: "False" + type: Ready + observedGeneration: 1 + state: Error diff --git a/internal/controller/testdata/capapplicationversion/expected/cav-missing-secrets.yaml b/internal/controller/testdata/capapplicationversion/expected/cav-missing-secrets.yaml new file mode 100644 index 0000000..158b840 --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/expected/cav-missing-secrets.yaml @@ -0,0 +1,68 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-07-18T06:13:52Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + finalizers: + - "sme.sap.com/capapplicationversion" + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 1.2.3 + workloads: + - name: cap-backend-srv + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: CAP + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/approuter/approuter:latest + type: Router + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + image: docker.image.repo/content/cap-content:latest + type: Content + - name: mtx-job + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + image: docker.image.repo/srv/server:latest + type: TenantOperation +status: + conditions: + - reason: WaitingForSecrets + observedGeneration: 1 + status: "False" + type: Ready + observedGeneration: 1 + state: Processing diff --git a/internal/controller/testdata/capapplicationversion/expected/cav-processing.yaml b/internal/controller/testdata/capapplicationversion/expected/cav-processing.yaml new file mode 100644 index 0000000..122d9bd --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/expected/cav-processing.yaml @@ -0,0 +1,68 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-07-18T06:13:52Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + finalizers: + - "sme.sap.com/capapplicationversion" + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 1.2.3 + workloads: + - name: cap-backend-srv + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: CAP + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/approuter/approuter:latest + type: Router + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + image: docker.image.repo/content/cap-content:latest + type: Content + - name: mtx-job + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + image: docker.image.repo/srv/server:latest + type: TenantOperation +status: + conditions: + - reason: ReadyForProcessing + observedGeneration: 1 + status: "False" + type: Ready + observedGeneration: 1 + state: Processing diff --git a/internal/controller/testdata/capapplicationversion/expected/cav-ready-annotations.yaml b/internal/controller/testdata/capapplicationversion/expected/cav-ready-annotations.yaml new file mode 100644 index 0000000..7aefb67 --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/expected/cav-ready-annotations.yaml @@ -0,0 +1,449 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-07-18T06:13:52Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + finalizers: + - "sme.sap.com/capapplicationversion" + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 1.2.3 + workloads: + - name: cap-backend-srv + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: CAP + env: + - name: HOST_IP + valueFrom: + fieldRef: + fieldPath: status.PodIp + ports: + - name: "app-port" + port: 4004 + routerDestinationName: "srv-api" + appProtocol: http + - name: "app-tech-port" + port: 4005 + networkPolicy: "Cluster" + livenessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4004 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + readinessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4004 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + resources: + limits: + cpu: 200m + memory: 500Mi + requests: + cpu: 20m + memory: 50Mi + - name: app-router + annotations: + foo: "bar" + sme.sap.com/some-custom-annotation: "some value for the annotation: 'sme.sap.com/some-custom-annotation'" + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/approuter/approuter:latest + type: Router + env: + - name: debug + valueFrom: + configMapKeyRef: + key: someKey + name: someCM + optional: true + ports: + - name: "router-port" + port: 4000 + appProtocol: http + - name: "router-tech-port" + port: 4004 + networkPolicy: "Cluster" + - name: "router-metrics-port" + port: 4007 + networkPolicy: "Cluster" + livenessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4000 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + readinessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4000 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + resources: + limits: + cpu: 200m + memory: 500Mi + requests: + cpu: 20m + memory: 50Mi + securityContext: + runAsUser: 1000 + runAsGroup: 2000 + podSecurityContext: + runAsUser: 2000 + fsGroup: 2000 + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + image: docker.image.repo/content/cap-content:latest + type: Content + env: + - name: SOME_VERSION + value: 0.0.1 + resources: + limits: + cpu: 100m + memory: 200Mi + requests: + cpu: 10m + memory: 20Mi + - name: mtx-job + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + image: docker.image.repo/srv/server:latest + type: TenantOperation + - name: job-worker + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: Additional + env: + - name: TEST_SEC_REF + valueFrom: + secretKeyRef: + key: someKey + name: someSecret + optional: true +status: + conditions: + - reason: CreatedDeployments + observedGeneration: 1 + status: "True" + type: Ready + observedGeneration: 1 + finishedJobs: + - "test-cap-01-cav-v1-content" + state: Ready +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + foo: "bar" + sme.sap.com/some-custom-annotation: "some value for the annotation: 'sme.sap.com/some-custom-annotation'" + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01-cav-v1 + labels: + app: test-cap-01 + sme.sap.com/category: Workload + sme.sap.com/workload-name: test-cap-01-cav-v1-app-router + sme.sap.com/workload-type: Router + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/cav-version: "1.2.3" + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: e95e0682f33a657e75e1fc435972d19bd407ba3b + name: test-cap-01-cav-v1-app-router + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplicationVersion + name: test-cap-01-cav-v1 + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + selector: + matchLabels: + app: test-cap-01 + sme.sap.com/category: Workload + sme.sap.com/workload-name: test-cap-01-cav-v1-app-router + sme.sap.com/workload-type: Router + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/cav-version: "1.2.3" + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: e95e0682f33a657e75e1fc435972d19bd407ba3b + template: + metadata: + creationTimestamp: null + annotations: + foo: "bar" + sme.sap.com/some-custom-annotation: "some value for the annotation: 'sme.sap.com/some-custom-annotation'" + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01-cav-v1 + labels: + app: test-cap-01 + sme.sap.com/category: Workload + sme.sap.com/workload-name: test-cap-01-cav-v1-app-router + sme.sap.com/workload-type: Router + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/cav-version: "1.2.3" + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: e95e0682f33a657e75e1fc435972d19bd407ba3b + spec: + containers: + - env: + - name: CAPOP_APP_VERSION + value: "1.2.3" + - name: debug + valueFrom: + configMapKeyRef: + key: someKey + name: someCM + optional: true + - name: destinations + value: '[{"name":"srv-api","url":"http://test-cap-01-cav-v1-cap-backend-srv-svc:4004","forwardAuthToken":true}]' + envFrom: + - secretRef: + name: test-cap-01-cav-v1-app-router-gen + optional: true + image: docker.image.repo/approuter/approuter:latest + livenessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4000 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + readinessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4000 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + resources: + limits: + cpu: 200m + memory: 500Mi + requests: + cpu: 20m + memory: 50Mi + securityContext: + runAsUser: 1000 + runAsGroup: 2000 + name: app-router + imagePullSecrets: + - name: regcred + securityContext: + runAsUser: 2000 + fsGroup: 2000 +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: test-cap-01-cav-v1 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplicationVersion + name: test-cap-01-cav-v1 + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + ingress: + - from: + - podSelector: + matchLabels: + app: test-cap-01 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/category: Workload + sme.sap.com/cav-version: "1.2.3" + podSelector: + matchLabels: + app: test-cap-01 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/category: Workload + sme.sap.com/cav-version: "1.2.3" + policyTypes: + - Ingress +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: test-cap-01-cav-v1--in + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplicationVersion + name: test-cap-01-cav-v1 + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + ingress: + - from: + - namespaceSelector: {} + podSelector: + matchLabels: + app: istio-ingressgateway + istio: ingressgateway + podSelector: + matchLabels: + app: test-cap-01 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/category: Workload + sme.sap.com/workload-type: Router + sme.sap.com/cav-version: "1.2.3" + policyTypes: + - Ingress +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: test-cap-01-cav-v1-app-router + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplicationVersion + name: test-cap-01-cav-v1 + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + ingress: + - from: + - namespaceSelector: {} + podSelector: {} + ports: + - port: 4004 + - port: 4007 + podSelector: + matchLabels: + app: test-cap-01 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/category: Workload + sme.sap.com/workload-name: test-cap-01-cav-v1-app-router + sme.sap.com/workload-type: Router + sme.sap.com/cav-version: "1.2.3" + policyTypes: + - Ingress +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: test-cap-01-cav-v1-cap-backend-srv + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplicationVersion + name: test-cap-01-cav-v1 + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + ingress: + - from: + - namespaceSelector: {} + podSelector: {} + ports: + - port: 4005 + podSelector: + matchLabels: + app: test-cap-01 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/category: Workload + sme.sap.com/workload-name: test-cap-01-cav-v1-cap-backend-srv + sme.sap.com/workload-type: CAP + sme.sap.com/cav-version: "1.2.3" + policyTypes: + - Ingress +--- +apiVersion: v1 +kind: Service +metadata: + annotations: + foo: "bar" + sme.sap.com/some-custom-annotation: "some value for the annotation: 'sme.sap.com/some-custom-annotation'" + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01-cav-v1 + labels: + app: test-cap-01 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/category: Service + sme.sap.com/workload-name: test-cap-01-cav-v1-app-router-svc + sme.sap.com/workload-type: Router + sme.sap.com/cav-version: "1.2.3" + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: e95e0682f33a657e75e1fc435972d19bd407ba3b + name: test-cap-01-cav-v1-app-router-svc + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplicationVersion + name: test-cap-01-cav-v1 + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + ports: + - name: router-port + port: 4000 + appProtocol: "http" + - name: router-tech-port + port: 4004 + - name: router-metrics-port + port: 4007 + selector: + app: test-cap-01 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/category: Workload + sme.sap.com/workload-name: test-cap-01-cav-v1-app-router + sme.sap.com/workload-type: Router + sme.sap.com/cav-version: "1.2.3" diff --git a/internal/controller/testdata/capapplicationversion/expected/cav-ready-app-netpol.yaml b/internal/controller/testdata/capapplicationversion/expected/cav-ready-app-netpol.yaml new file mode 100644 index 0000000..1f4041e --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/expected/cav-ready-app-netpol.yaml @@ -0,0 +1,308 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-07-18T06:13:52Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + finalizers: + - "sme.sap.com/capapplicationversion" + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 1.2.3 + workloads: + - name: cap-backend-srv + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: CAP + env: + - name: HOST_IP + valueFrom: + fieldRef: + fieldPath: status.PodIp + livenessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4004 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + readinessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4004 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + resources: + limits: + cpu: 200m + memory: 500Mi + requests: + cpu: 20m + memory: 50Mi + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/approuter/approuter:latest + type: Router + env: + - name: debug + valueFrom: + configMapKeyRef: + key: someKey + name: someCM + optional: true + livenessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4000 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + readinessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4000 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + resources: + limits: + cpu: 200m + memory: 500Mi + requests: + cpu: 20m + memory: 50Mi + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + image: docker.image.repo/content/cap-content:latest + type: Content + env: + - name: SOME_VERSION + value: 0.0.1 + resources: + limits: + cpu: 100m + memory: 200Mi + requests: + cpu: 10m + memory: 20Mi + - name: mtx-job + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + image: docker.image.repo/srv/server:latest + type: TenantOperation + - name: job-worker + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: Additional + env: + - name: TEST_SEC_REF + valueFrom: + secretKeyRef: + key: someKey + name: someSecret + optional: true +status: + conditions: + - reason: CreatedDeployments + observedGeneration: 1 + status: "True" + type: Ready + observedGeneration: 1 + finishedJobs: + - "test-cap-01-cav-v1-content" + state: Ready +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: test-cap-01 + sme.sap.com/category: Workload + sme.sap.com/workload-name: test-cap-01-cav-v1-app-router + sme.sap.com/workload-type: Router + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/cav-version: "1.2.3" + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: e95e0682f33a657e75e1fc435972d19bd407ba3b + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01-cav-v1 + name: test-cap-01-cav-v1-app-router + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplicationVersion + name: test-cap-01-cav-v1 + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + selector: + matchLabels: + app: test-cap-01 + sme.sap.com/category: Workload + sme.sap.com/workload-name: test-cap-01-cav-v1-app-router + sme.sap.com/workload-type: Router + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/cav-version: "1.2.3" + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: e95e0682f33a657e75e1fc435972d19bd407ba3b + template: + metadata: + creationTimestamp: null + labels: + app: test-cap-01 + sme.sap.com/category: Workload + sme.sap.com/workload-name: test-cap-01-cav-v1-app-router + sme.sap.com/workload-type: Router + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/cav-version: "1.2.3" + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: e95e0682f33a657e75e1fc435972d19bd407ba3b + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01-cav-v1 + spec: + containers: + - env: + - name: CAPOP_APP_VERSION + value: "1.2.3" + - name: debug + valueFrom: + configMapKeyRef: + key: someKey + name: someCM + optional: true + - name: destinations + value: '[{"name":"srv-api","url":"http://test-cap-01-cav-v1-cap-backend-srv-svc:4004","forwardAuthToken":true}]' + envFrom: + - secretRef: + name: test-cap-01-cav-v1-app-router-gen + optional: true + image: docker.image.repo/approuter/approuter:latest + livenessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4000 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + readinessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4000 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + resources: + limits: + cpu: 200m + memory: 500Mi + requests: + cpu: 20m + memory: 50Mi + name: app-router + imagePullSecrets: + - name: regcred +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: test-cap-01-cav-v1 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplicationVersion + name: test-cap-01-cav-v1 + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + ingress: + - from: + - podSelector: + matchLabels: + app: test-cap-01 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/category: Workload + sme.sap.com/cav-version: "1.2.3" + podSelector: + matchLabels: + app: test-cap-01 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/category: Workload + sme.sap.com/cav-version: "1.2.3" + policyTypes: + - Ingress +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: test-cap-01-cav-v1--in + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplicationVersion + name: test-cap-01-cav-v1 + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + ingress: + - from: + - namespaceSelector: {} + podSelector: + matchLabels: + app: istio-ingressgateway + istio: ingressgateway + podSelector: + matchLabels: + app: test-cap-01 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/category: Workload + sme.sap.com/workload-type: Router + sme.sap.com/cav-version: "1.2.3" + policyTypes: + - Ingress diff --git a/internal/controller/testdata/capapplicationversion/expected/cav-ready-cluster-netpol-port.yaml b/internal/controller/testdata/capapplicationversion/expected/cav-ready-cluster-netpol-port.yaml new file mode 100644 index 0000000..832feb7 --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/expected/cav-ready-cluster-netpol-port.yaml @@ -0,0 +1,349 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-07-18T06:13:52Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + finalizers: + - "sme.sap.com/capapplicationversion" + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 1.2.3 + workloads: + - name: cap-backend-srv + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: CAP + env: + - name: HOST_IP + valueFrom: + fieldRef: + fieldPath: status.PodIp + livenessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4004 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + readinessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4004 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + resources: + limits: + cpu: 200m + memory: 500Mi + requests: + cpu: 20m + memory: 50Mi + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/approuter/approuter:latest + type: Router + env: + - name: debug + valueFrom: + configMapKeyRef: + key: someKey + name: someCM + optional: true + ports: + - name: "router-port" + port: 4000 + appProtocol: http + - name: "router-tech-port" + port: 4004 + networkPolicy: "Cluster" + - name: "router-metrics-port" + port: 4007 + networkPolicy: "Cluster" + livenessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4000 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + readinessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4000 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + resources: + limits: + cpu: 200m + memory: 500Mi + requests: + cpu: 20m + memory: 50Mi + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + image: docker.image.repo/content/cap-content:latest + type: Content + env: + - name: SOME_VERSION + value: 0.0.1 + resources: + limits: + cpu: 100m + memory: 200Mi + requests: + cpu: 10m + memory: 20Mi + - name: mtx-job + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + image: docker.image.repo/srv/server:latest + type: TenantOperation + - name: job-worker + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: Additional + env: + - name: TEST_SEC_REF + valueFrom: + secretKeyRef: + key: someKey + name: someSecret + optional: true +status: + conditions: + - reason: CreatedDeployments + observedGeneration: 1 + status: "True" + type: Ready + observedGeneration: 1 + finishedJobs: + - "test-cap-01-cav-v1-content" + state: Ready +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: test-cap-01 + sme.sap.com/category: Workload + sme.sap.com/workload-name: test-cap-01-cav-v1-app-router + sme.sap.com/workload-type: Router + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/cav-version: "1.2.3" + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: e95e0682f33a657e75e1fc435972d19bd407ba3b + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01-cav-v1 + name: test-cap-01-cav-v1-app-router + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplicationVersion + name: test-cap-01-cav-v1 + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + selector: + matchLabels: + app: test-cap-01 + sme.sap.com/category: Workload + sme.sap.com/workload-name: test-cap-01-cav-v1-app-router + sme.sap.com/workload-type: Router + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/cav-version: "1.2.3" + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: e95e0682f33a657e75e1fc435972d19bd407ba3b + template: + metadata: + creationTimestamp: null + labels: + app: test-cap-01 + sme.sap.com/category: Workload + sme.sap.com/workload-name: test-cap-01-cav-v1-app-router + sme.sap.com/workload-type: Router + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/cav-version: "1.2.3" + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: e95e0682f33a657e75e1fc435972d19bd407ba3b + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01-cav-v1 + spec: + containers: + - env: + - name: CAPOP_APP_VERSION + value: "1.2.3" + - name: debug + valueFrom: + configMapKeyRef: + key: someKey + name: someCM + optional: true + - name: destinations + value: '[{"name":"srv-api","url":"http://test-cap-01-cav-v1-cap-backend-srv-svc:4004","forwardAuthToken":true}]' + envFrom: + - secretRef: + name: test-cap-01-cav-v1-app-router-gen + optional: true + image: docker.image.repo/approuter/approuter:latest + livenessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4000 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + readinessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4000 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + resources: + limits: + cpu: 200m + memory: 500Mi + requests: + cpu: 20m + memory: 50Mi + name: app-router + imagePullSecrets: + - name: regcred +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: test-cap-01-cav-v1 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplicationVersion + name: test-cap-01-cav-v1 + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + ingress: + - from: + - podSelector: + matchLabels: + app: test-cap-01 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/category: Workload + sme.sap.com/cav-version: "1.2.3" + podSelector: + matchLabels: + app: test-cap-01 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/category: Workload + sme.sap.com/cav-version: "1.2.3" + policyTypes: + - Ingress +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: test-cap-01-cav-v1--in + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplicationVersion + name: test-cap-01-cav-v1 + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + ingress: + - from: + - namespaceSelector: {} + podSelector: + matchLabels: + app: istio-ingressgateway + istio: ingressgateway + podSelector: + matchLabels: + app: test-cap-01 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/category: Workload + sme.sap.com/workload-type: Router + sme.sap.com/cav-version: "1.2.3" + policyTypes: + - Ingress +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: test-cap-01-cav-v1-app-router + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplicationVersion + name: test-cap-01-cav-v1 + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + ingress: + - from: + - namespaceSelector: {} + podSelector: {} + ports: + - port: 4004 + - port: 4007 + podSelector: + matchLabels: + app: test-cap-01 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/category: Workload + sme.sap.com/workload-name: test-cap-01-cav-v1-app-router + sme.sap.com/workload-type: Router + sme.sap.com/cav-version: "1.2.3" + policyTypes: + - Ingress diff --git a/internal/controller/testdata/capapplicationversion/expected/cav-ready-content-job.yaml b/internal/controller/testdata/capapplicationversion/expected/cav-ready-content-job.yaml new file mode 100644 index 0000000..efcee83 --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/expected/cav-ready-content-job.yaml @@ -0,0 +1,70 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-07-18T06:13:52Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + finalizers: + - "sme.sap.com/capapplicationversion" + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 1.2.3 + workloads: + - name: cap-backend-srv + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: CAP + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/approuter/approuter:latest + type: Router + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + image: docker.image.repo/content/cap-content:latest + type: Content + - name: mtx-job + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + image: docker.image.repo/srv/server:latest + type: TenantOperation +status: + conditions: + - reason: CreatedDeployments + observedGeneration: 1 + status: "True" + type: Ready + observedGeneration: 1 + finishedJobs: + - "test-cap-01-cav-v1-content" + state: Ready diff --git a/internal/controller/testdata/capapplicationversion/expected/cav-ready-custom-destination-config.yaml b/internal/controller/testdata/capapplicationversion/expected/cav-ready-custom-destination-config.yaml new file mode 100644 index 0000000..5a71c57 --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/expected/cav-ready-custom-destination-config.yaml @@ -0,0 +1,153 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-07-18T06:13:52Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + finalizers: + - "sme.sap.com/capapplicationversion" + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 1.2.3 + workloads: + - name: cap-backend-srv + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: CAP + ports: + - name: server-app-port + port: 4000 + routerDestinationName: custom-srv-api + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/approuter/approuter:latest + type: Router + env: + - name: debug + valueFrom: + configMapKeyRef: + key: someKey + name: someCM + optional: true + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + image: docker.image.repo/content/cap-content:latest + type: Content + - name: mtx-job + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + image: docker.image.repo/srv/server:latest + type: TenantOperation +status: + conditions: + - reason: CreatedDeployments + observedGeneration: 1 + status: "True" + type: Ready + observedGeneration: 1 + finishedJobs: + - "test-cap-01-cav-v1-content" + state: Ready +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: test-cap-01 + sme.sap.com/category: Workload + sme.sap.com/workload-name: test-cap-01-cav-v1-app-router + sme.sap.com/workload-type: Router + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/cav-version: "1.2.3" + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: e95e0682f33a657e75e1fc435972d19bd407ba3b + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01-cav-v1 + name: test-cap-01-cav-v1-app-router + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplicationVersion + name: test-cap-01-cav-v1 + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + selector: + matchLabels: + app: test-cap-01 + sme.sap.com/category: Workload + sme.sap.com/workload-name: test-cap-01-cav-v1-app-router + sme.sap.com/workload-type: Router + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/cav-version: "1.2.3" + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: e95e0682f33a657e75e1fc435972d19bd407ba3b + template: + metadata: + creationTimestamp: null + labels: + app: test-cap-01 + sme.sap.com/category: Workload + sme.sap.com/workload-name: test-cap-01-cav-v1-app-router + sme.sap.com/workload-type: Router + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/cav-version: "1.2.3" + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: e95e0682f33a657e75e1fc435972d19bd407ba3b + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01-cav-v1 + spec: + containers: + - env: + - name: CAPOP_APP_VERSION + value: "1.2.3" + - name: debug + valueFrom: + configMapKeyRef: + key: someKey + name: someCM + optional: true + - name: destinations + value: '[{"name":"custom-srv-api","url":"http://test-cap-01-cav-v1-cap-backend-srv-svc:4000","forwardAuthToken":true}]' + envFrom: + - secretRef: + name: test-cap-01-cav-v1-app-router-gen + optional: true + image: docker.image.repo/approuter/approuter:latest + name: app-router + imagePullSecrets: + - name: regcred diff --git a/internal/controller/testdata/capapplicationversion/expected/cav-ready-custom-labels-config.yaml b/internal/controller/testdata/capapplicationversion/expected/cav-ready-custom-labels-config.yaml new file mode 100644 index 0000000..1c6e9dc --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/expected/cav-ready-custom-labels-config.yaml @@ -0,0 +1,189 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-07-18T06:13:52Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + finalizers: + - "sme.sap.com/capapplicationversion" + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 1.2.3 + workloads: + - name: cap-backend-srv + labels: + foo: bar + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: CAP + env: + - name: HOST_IP + valueFrom: + fieldRef: + fieldPath: status.PodIp + - name: app-router + labels: + foo: bar + sme.sap.com/app-type: my-cap-operator-app + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/approuter/approuter:latest + type: Router + env: + - name: debug + valueFrom: + configMapKeyRef: + key: someKey + name: someCM + optional: true + - name: content-job + labels: + foo: bar + consumedBTPServices: + - cap-uaa + jobDefinition: + image: docker.image.repo/content/cap-content:latest + type: Content + env: + - name: SOME_VERSION + value: 0.0.1 + - name: mtx-job + labels: + foo: bar + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + image: docker.image.repo/srv/server:latest + type: TenantOperation + - name: job-worker + labels: + foo: bar + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: Additional + env: + - name: TEST_SEC_REF + valueFrom: + secretKeyRef: + key: someKey + name: someSecret + optional: true +status: + conditions: + - reason: CreatedDeployments + observedGeneration: 1 + status: "True" + type: Ready + observedGeneration: 1 + finishedJobs: + - "test-cap-01-cav-v1-content" + state: Ready +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: test-cap-01 + foo: bar + sme.sap.com/app-type: my-cap-operator-app + sme.sap.com/category: Workload + sme.sap.com/workload-name: test-cap-01-cav-v1-app-router + sme.sap.com/workload-type: Router + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/cav-version: "1.2.3" + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: e95e0682f33a657e75e1fc435972d19bd407ba3b + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01-cav-v1 + name: test-cap-01-cav-v1-app-router + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplicationVersion + name: test-cap-01-cav-v1 + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + selector: + matchLabels: + app: test-cap-01 + foo: bar + sme.sap.com/app-type: my-cap-operator-app + sme.sap.com/category: Workload + sme.sap.com/workload-name: test-cap-01-cav-v1-app-router + sme.sap.com/workload-type: Router + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/cav-version: "1.2.3" + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: e95e0682f33a657e75e1fc435972d19bd407ba3b + template: + metadata: + creationTimestamp: null + labels: + app: test-cap-01 + foo: bar + sme.sap.com/app-type: my-cap-operator-app + sme.sap.com/category: Workload + sme.sap.com/workload-name: test-cap-01-cav-v1-app-router + sme.sap.com/workload-type: Router + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/cav-version: "1.2.3" + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: e95e0682f33a657e75e1fc435972d19bd407ba3b + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01-cav-v1 + spec: + containers: + - env: + - name: CAPOP_APP_VERSION + value: "1.2.3" + - name: debug + valueFrom: + configMapKeyRef: + key: someKey + name: someCM + optional: true + - name: destinations + value: '[{"name":"srv-api","url":"http://test-cap-01-cav-v1-cap-backend-srv-svc:4004","forwardAuthToken":true}]' + envFrom: + - secretRef: + name: test-cap-01-cav-v1-app-router-gen + optional: true + image: docker.image.repo/approuter/approuter:latest + name: app-router + imagePullSecrets: + - name: regcred diff --git a/internal/controller/testdata/capapplicationversion/expected/cav-ready-merged-destinations-router.yaml b/internal/controller/testdata/capapplicationversion/expected/cav-ready-merged-destinations-router.yaml new file mode 100644 index 0000000..311cd66 --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/expected/cav-ready-merged-destinations-router.yaml @@ -0,0 +1,142 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-07-18T06:13:52Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + finalizers: + - "sme.sap.com/capapplicationversion" + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 1.2.3 + workloads: + - name: cap-backend-srv + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: CAP + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/approuter/approuter:latest + type: Router + env: + - name: destinations + value: '[{"name": "foo", "url": "http://foo.svc:5000", "strictSSL": true},{"name": "srv-api", "url": "http://some.domain", "timeout": 60000}]' + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + image: docker.image.repo/content/cap-content:latest + type: Content + env: + - name: SOME_VERSION + value: 0.0.1 + - name: mtx-job + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + image: docker.image.repo/srv/server:latest + type: TenantOperation +status: + conditions: + - reason: CreatedDeployments + observedGeneration: 1 + status: "True" + type: Ready + observedGeneration: 1 + finishedJobs: + - "test-cap-01-cav-v1-content" + state: Ready +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: test-cap-01 + sme.sap.com/category: Workload + sme.sap.com/workload-name: test-cap-01-cav-v1-app-router + sme.sap.com/workload-type: Router + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/cav-version: "1.2.3" + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: e95e0682f33a657e75e1fc435972d19bd407ba3b + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01-cav-v1 + name: test-cap-01-cav-v1-app-router + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplicationVersion + name: test-cap-01-cav-v1 + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + selector: + matchLabels: + app: test-cap-01 + sme.sap.com/category: Workload + sme.sap.com/workload-name: test-cap-01-cav-v1-app-router + sme.sap.com/workload-type: Router + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/cav-version: "1.2.3" + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: e95e0682f33a657e75e1fc435972d19bd407ba3b + template: + metadata: + creationTimestamp: null + labels: + app: test-cap-01 + sme.sap.com/category: Workload + sme.sap.com/workload-name: test-cap-01-cav-v1-app-router + sme.sap.com/workload-type: Router + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/cav-version: "1.2.3" + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: e95e0682f33a657e75e1fc435972d19bd407ba3b + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01-cav-v1 + spec: + containers: + - env: + - name: CAPOP_APP_VERSION + value: "1.2.3" + - name: destinations + value: '[{"name":"srv-api","url":"http://test-cap-01-cav-v1-cap-backend-srv-svc:4004","timeout":60000},{"name":"foo","url":"http://foo.svc:5000","strictSSL":true}]' + envFrom: + - secretRef: + name: test-cap-01-cav-v1-app-router-gen + optional: true + image: docker.image.repo/approuter/approuter:latest + name: app-router + imagePullSecrets: + - name: regcred diff --git a/internal/controller/testdata/capapplicationversion/expected/cav-ready-pod-security-context.yaml b/internal/controller/testdata/capapplicationversion/expected/cav-ready-pod-security-context.yaml new file mode 100644 index 0000000..735b417 --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/expected/cav-ready-pod-security-context.yaml @@ -0,0 +1,399 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-07-18T06:13:52Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + finalizers: + - "sme.sap.com/capapplicationversion" + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 1.2.3 + workloads: + - name: cap-backend-srv + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: CAP + env: + - name: HOST_IP + valueFrom: + fieldRef: + fieldPath: status.PodIp + ports: + - name: "app-port" + port: 4004 + routerDestinationName: "srv-api" + appProtocol: http + - name: "app-tech-port" + port: 4005 + networkPolicy: "Cluster" + livenessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4004 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + readinessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4004 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + resources: + limits: + cpu: 200m + memory: 500Mi + requests: + cpu: 20m + memory: 50Mi + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/approuter/approuter:latest + type: Router + env: + - name: debug + valueFrom: + configMapKeyRef: + key: someKey + name: someCM + optional: true + ports: + - name: "router-port" + port: 4000 + appProtocol: http + - name: "router-tech-port" + port: 4004 + networkPolicy: "Cluster" + - name: "router-metrics-port" + port: 4007 + networkPolicy: "Cluster" + livenessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4000 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + readinessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4000 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + resources: + limits: + cpu: 200m + memory: 500Mi + requests: + cpu: 20m + memory: 50Mi + securityContext: + runAsUser: 1000 + runAsGroup: 2000 + podSecurityContext: + runAsUser: 2000 + fsGroup: 2000 + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + image: docker.image.repo/content/cap-content:latest + type: Content + env: + - name: SOME_VERSION + value: 0.0.1 + resources: + limits: + cpu: 100m + memory: 200Mi + requests: + cpu: 10m + memory: 20Mi + - name: mtx-job + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + image: docker.image.repo/srv/server:latest + type: TenantOperation + - name: job-worker + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: Additional + env: + - name: TEST_SEC_REF + valueFrom: + secretKeyRef: + key: someKey + name: someSecret + optional: true +status: + conditions: + - reason: CreatedDeployments + observedGeneration: 1 + status: "True" + type: Ready + observedGeneration: 1 + finishedJobs: + - "test-cap-01-cav-v1-content" + state: Ready +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: test-cap-01 + sme.sap.com/category: Workload + sme.sap.com/workload-name: test-cap-01-cav-v1-app-router + sme.sap.com/workload-type: Router + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/cav-version: "1.2.3" + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: e95e0682f33a657e75e1fc435972d19bd407ba3b + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01-cav-v1 + name: test-cap-01-cav-v1-app-router + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplicationVersion + name: test-cap-01-cav-v1 + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + selector: + matchLabels: + app: test-cap-01 + sme.sap.com/category: Workload + sme.sap.com/workload-name: test-cap-01-cav-v1-app-router + sme.sap.com/workload-type: Router + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/cav-version: "1.2.3" + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: e95e0682f33a657e75e1fc435972d19bd407ba3b + template: + metadata: + creationTimestamp: null + labels: + app: test-cap-01 + sme.sap.com/category: Workload + sme.sap.com/workload-name: test-cap-01-cav-v1-app-router + sme.sap.com/workload-type: Router + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/cav-version: "1.2.3" + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: e95e0682f33a657e75e1fc435972d19bd407ba3b + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01-cav-v1 + spec: + containers: + - env: + - name: CAPOP_APP_VERSION + value: "1.2.3" + - name: debug + valueFrom: + configMapKeyRef: + key: someKey + name: someCM + optional: true + - name: destinations + value: '[{"name":"srv-api","url":"http://test-cap-01-cav-v1-cap-backend-srv-svc:4004","forwardAuthToken":true}]' + envFrom: + - secretRef: + name: test-cap-01-cav-v1-app-router-gen + optional: true + image: docker.image.repo/approuter/approuter:latest + livenessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4000 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + readinessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4000 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + resources: + limits: + cpu: 200m + memory: 500Mi + requests: + cpu: 20m + memory: 50Mi + securityContext: + runAsUser: 1000 + runAsGroup: 2000 + name: app-router + imagePullSecrets: + - name: regcred + securityContext: + runAsUser: 2000 + fsGroup: 2000 +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: test-cap-01-cav-v1 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplicationVersion + name: test-cap-01-cav-v1 + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + ingress: + - from: + - podSelector: + matchLabels: + app: test-cap-01 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/category: Workload + sme.sap.com/cav-version: "1.2.3" + podSelector: + matchLabels: + app: test-cap-01 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/category: Workload + sme.sap.com/cav-version: "1.2.3" + policyTypes: + - Ingress +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: test-cap-01-cav-v1--in + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplicationVersion + name: test-cap-01-cav-v1 + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + ingress: + - from: + - namespaceSelector: {} + podSelector: + matchLabels: + app: istio-ingressgateway + istio: ingressgateway + podSelector: + matchLabels: + app: test-cap-01 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/category: Workload + sme.sap.com/workload-type: Router + sme.sap.com/cav-version: "1.2.3" + policyTypes: + - Ingress +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: test-cap-01-cav-v1-app-router + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplicationVersion + name: test-cap-01-cav-v1 + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + ingress: + - from: + - namespaceSelector: {} + podSelector: {} + ports: + - port: 4004 + - port: 4007 + podSelector: + matchLabels: + app: test-cap-01 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/category: Workload + sme.sap.com/workload-name: test-cap-01-cav-v1-app-router + sme.sap.com/workload-type: Router + sme.sap.com/cav-version: "1.2.3" + policyTypes: + - Ingress +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: test-cap-01-cav-v1-cap-backend-srv + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplicationVersion + name: test-cap-01-cav-v1 + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + ingress: + - from: + - namespaceSelector: {} + podSelector: {} + ports: + - port: 4005 + podSelector: + matchLabels: + app: test-cap-01 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/category: Workload + sme.sap.com/workload-name: test-cap-01-cav-v1-cap-backend-srv + sme.sap.com/workload-type: CAP + sme.sap.com/cav-version: "1.2.3" + policyTypes: + - Ingress diff --git a/internal/controller/testdata/capapplicationversion/expected/cav-ready-probes-and-resources.yaml b/internal/controller/testdata/capapplicationversion/expected/cav-ready-probes-and-resources.yaml new file mode 100644 index 0000000..1e39244 --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/expected/cav-ready-probes-and-resources.yaml @@ -0,0 +1,248 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-07-18T06:13:52Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + finalizers: + - "sme.sap.com/capapplicationversion" + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 1.2.3 + workloads: + - name: cap-backend-srv + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: CAP + env: + - name: HOST_IP + valueFrom: + fieldRef: + fieldPath: status.PodIp + livenessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4004 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + readinessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4004 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + resources: + limits: + cpu: 200m + memory: 500Mi + requests: + cpu: 20m + memory: 50Mi + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/approuter/approuter:latest + type: Router + env: + - name: debug + valueFrom: + configMapKeyRef: + key: someKey + name: someCM + optional: true + livenessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4000 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + readinessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4000 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + resources: + limits: + cpu: 200m + memory: 500Mi + requests: + cpu: 20m + memory: 50Mi + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + image: docker.image.repo/content/cap-content:latest + type: Content + env: + - name: SOME_VERSION + value: 0.0.1 + resources: + limits: + cpu: 100m + memory: 200Mi + requests: + cpu: 10m + memory: 20Mi + - name: mtx-job + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + image: docker.image.repo/srv/server:latest + type: TenantOperation + - name: job-worker + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: Additional + env: + - name: TEST_SEC_REF + valueFrom: + secretKeyRef: + key: someKey + name: someSecret + optional: true +status: + conditions: + - reason: CreatedDeployments + observedGeneration: 1 + status: "True" + type: Ready + observedGeneration: 1 + finishedJobs: + - "test-cap-01-cav-v1-content" + state: Ready +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: test-cap-01 + sme.sap.com/category: Workload + sme.sap.com/workload-name: test-cap-01-cav-v1-app-router + sme.sap.com/workload-type: Router + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/cav-version: "1.2.3" + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: e95e0682f33a657e75e1fc435972d19bd407ba3b + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01-cav-v1 + name: test-cap-01-cav-v1-app-router + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplicationVersion + name: test-cap-01-cav-v1 + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + selector: + matchLabels: + app: test-cap-01 + sme.sap.com/category: Workload + sme.sap.com/workload-name: test-cap-01-cav-v1-app-router + sme.sap.com/workload-type: Router + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/cav-version: "1.2.3" + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: e95e0682f33a657e75e1fc435972d19bd407ba3b + template: + metadata: + creationTimestamp: null + labels: + app: test-cap-01 + sme.sap.com/category: Workload + sme.sap.com/workload-name: test-cap-01-cav-v1-app-router + sme.sap.com/workload-type: Router + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/cav-version: "1.2.3" + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: e95e0682f33a657e75e1fc435972d19bd407ba3b + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01-cav-v1 + spec: + containers: + - env: + - name: CAPOP_APP_VERSION + value: "1.2.3" + - name: debug + valueFrom: + configMapKeyRef: + key: someKey + name: someCM + optional: true + - name: destinations + value: '[{"name":"srv-api","url":"http://test-cap-01-cav-v1-cap-backend-srv-svc:4004","forwardAuthToken":true}]' + envFrom: + - secretRef: + name: test-cap-01-cav-v1-app-router-gen + optional: true + image: docker.image.repo/approuter/approuter:latest + livenessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4000 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + readinessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4000 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + resources: + limits: + cpu: 200m + memory: 500Mi + requests: + cpu: 20m + memory: 50Mi + name: app-router + imagePullSecrets: + - name: regcred diff --git a/internal/controller/testdata/capapplicationversion/expected/cav-ready-security-context.yaml b/internal/controller/testdata/capapplicationversion/expected/cav-ready-security-context.yaml new file mode 100644 index 0000000..92a2984 --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/expected/cav-ready-security-context.yaml @@ -0,0 +1,355 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-07-18T06:13:52Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + finalizers: + - "sme.sap.com/capapplicationversion" + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 1.2.3 + workloads: + - name: cap-backend-srv + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: CAP + env: + - name: HOST_IP + valueFrom: + fieldRef: + fieldPath: status.PodIp + livenessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4004 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + readinessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4004 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + resources: + limits: + cpu: 200m + memory: 500Mi + requests: + cpu: 20m + memory: 50Mi + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/approuter/approuter:latest + type: Router + env: + - name: debug + valueFrom: + configMapKeyRef: + key: someKey + name: someCM + optional: true + ports: + - name: "router-port" + port: 4000 + appProtocol: http + - name: "router-tech-port" + port: 4004 + networkPolicy: "Cluster" + - name: "router-metrics-port" + port: 4007 + networkPolicy: "Cluster" + livenessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4000 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + readinessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4000 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + resources: + limits: + cpu: 200m + memory: 500Mi + requests: + cpu: 20m + memory: 50Mi + securityContext: + runAsUser: 1000 + runAsGroup: 2000 + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + image: docker.image.repo/content/cap-content:latest + type: Content + env: + - name: SOME_VERSION + value: 0.0.1 + resources: + limits: + cpu: 100m + memory: 200Mi + requests: + cpu: 10m + memory: 20Mi + - name: mtx-job + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + image: docker.image.repo/srv/server:latest + type: TenantOperation + - name: job-worker + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: Additional + env: + - name: TEST_SEC_REF + valueFrom: + secretKeyRef: + key: someKey + name: someSecret + optional: true +status: + conditions: + - reason: CreatedDeployments + observedGeneration: 1 + status: "True" + type: Ready + observedGeneration: 1 + finishedJobs: + - "test-cap-01-cav-v1-content" + state: Ready +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: test-cap-01 + sme.sap.com/category: Workload + sme.sap.com/workload-name: test-cap-01-cav-v1-app-router + sme.sap.com/workload-type: Router + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/cav-version: "1.2.3" + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: e95e0682f33a657e75e1fc435972d19bd407ba3b + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01-cav-v1 + name: test-cap-01-cav-v1-app-router + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplicationVersion + name: test-cap-01-cav-v1 + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + selector: + matchLabels: + app: test-cap-01 + sme.sap.com/category: Workload + sme.sap.com/workload-name: test-cap-01-cav-v1-app-router + sme.sap.com/workload-type: Router + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/cav-version: "1.2.3" + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: e95e0682f33a657e75e1fc435972d19bd407ba3b + template: + metadata: + creationTimestamp: null + labels: + app: test-cap-01 + sme.sap.com/category: Workload + sme.sap.com/workload-name: test-cap-01-cav-v1-app-router + sme.sap.com/workload-type: Router + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/cav-version: "1.2.3" + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: e95e0682f33a657e75e1fc435972d19bd407ba3b + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01-cav-v1 + spec: + containers: + - env: + - name: CAPOP_APP_VERSION + value: "1.2.3" + - name: debug + valueFrom: + configMapKeyRef: + key: someKey + name: someCM + optional: true + - name: destinations + value: '[{"name":"srv-api","url":"http://test-cap-01-cav-v1-cap-backend-srv-svc:4004","forwardAuthToken":true}]' + envFrom: + - secretRef: + name: test-cap-01-cav-v1-app-router-gen + optional: true + image: docker.image.repo/approuter/approuter:latest + livenessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4000 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + readinessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4000 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + resources: + limits: + cpu: 200m + memory: 500Mi + requests: + cpu: 20m + memory: 50Mi + securityContext: + runAsUser: 1000 + runAsGroup: 2000 + name: app-router + imagePullSecrets: + - name: regcred +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: test-cap-01-cav-v1 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplicationVersion + name: test-cap-01-cav-v1 + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + ingress: + - from: + - podSelector: + matchLabels: + app: test-cap-01 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/category: Workload + sme.sap.com/cav-version: "1.2.3" + podSelector: + matchLabels: + app: test-cap-01 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/category: Workload + sme.sap.com/cav-version: "1.2.3" + policyTypes: + - Ingress +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: test-cap-01-cav-v1--in + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplicationVersion + name: test-cap-01-cav-v1 + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + ingress: + - from: + - namespaceSelector: {} + podSelector: + matchLabels: + app: istio-ingressgateway + istio: ingressgateway + podSelector: + matchLabels: + app: test-cap-01 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/category: Workload + sme.sap.com/workload-type: Router + sme.sap.com/cav-version: "1.2.3" + policyTypes: + - Ingress +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: test-cap-01-cav-v1-app-router + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplicationVersion + name: test-cap-01-cav-v1 + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + ingress: + - from: + - namespaceSelector: {} + podSelector: {} + ports: + - port: 4004 + - port: 4007 + podSelector: + matchLabels: + app: test-cap-01 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/category: Workload + sme.sap.com/workload-name: test-cap-01-cav-v1-app-router + sme.sap.com/workload-type: Router + sme.sap.com/cav-version: "1.2.3" + policyTypes: + - Ingress diff --git a/internal/controller/testdata/capapplicationversion/expected/cav-ready-valid-env-config.yaml b/internal/controller/testdata/capapplicationversion/expected/cav-ready-valid-env-config.yaml new file mode 100644 index 0000000..152b178 --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/expected/cav-ready-valid-env-config.yaml @@ -0,0 +1,174 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-07-18T06:13:52Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + finalizers: + - "sme.sap.com/capapplicationversion" + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 1.2.3 + workloads: + - name: cap-backend-srv + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: CAP + env: + - name: HOST_IP + valueFrom: + fieldRef: + fieldPath: status.PodIp + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/approuter/approuter:latest + imagePullPolicy: "Always" + type: Router + env: + - name: debug + valueFrom: + configMapKeyRef: + key: someKey + name: someCM + optional: true + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + image: docker.image.repo/content/cap-content:latest + type: Content + env: + - name: SOME_VERSION + value: 0.0.1 + - name: mtx-job + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + image: docker.image.repo/srv/server:latest + type: TenantOperation + - name: job-worker + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: Additional + env: + - name: TEST_SEC_REF + valueFrom: + secretKeyRef: + key: someKey + name: someSecret + optional: true +status: + conditions: + - reason: CreatedDeployments + observedGeneration: 1 + status: "True" + type: Ready + observedGeneration: 1 + finishedJobs: + - "test-cap-01-cav-v1-content" + state: Ready +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: test-cap-01 + sme.sap.com/category: Workload + sme.sap.com/workload-name: test-cap-01-cav-v1-app-router + sme.sap.com/workload-type: Router + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/cav-version: "1.2.3" + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: e95e0682f33a657e75e1fc435972d19bd407ba3b + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01-cav-v1 + name: test-cap-01-cav-v1-app-router + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplicationVersion + name: test-cap-01-cav-v1 + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + selector: + matchLabels: + app: test-cap-01 + sme.sap.com/category: Workload + sme.sap.com/workload-name: test-cap-01-cav-v1-app-router + sme.sap.com/workload-type: Router + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/cav-version: "1.2.3" + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: e95e0682f33a657e75e1fc435972d19bd407ba3b + template: + metadata: + creationTimestamp: null + labels: + app: test-cap-01 + sme.sap.com/category: Workload + sme.sap.com/workload-name: test-cap-01-cav-v1-app-router + sme.sap.com/workload-type: Router + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/cav-version: "1.2.3" + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: e95e0682f33a657e75e1fc435972d19bd407ba3b + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01-cav-v1 + spec: + containers: + - env: + - name: CAPOP_APP_VERSION + value: "1.2.3" + - name: debug + valueFrom: + configMapKeyRef: + key: someKey + name: someCM + optional: true + - name: destinations + value: '[{"name":"srv-api","url":"http://test-cap-01-cav-v1-cap-backend-srv-svc:4004","forwardAuthToken":true}]' + envFrom: + - secretRef: + name: test-cap-01-cav-v1-app-router-gen + optional: true + image: docker.image.repo/approuter/approuter:latest + imagePullPolicy: "Always" + name: app-router + imagePullSecrets: + - name: regcred diff --git a/internal/controller/testdata/capapplicationversion/expected/cav-ready.yaml b/internal/controller/testdata/capapplicationversion/expected/cav-ready.yaml new file mode 100644 index 0000000..6f9969b --- /dev/null +++ b/internal/controller/testdata/capapplicationversion/expected/cav-ready.yaml @@ -0,0 +1,68 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-07-18T06:13:52Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + finalizers: + - "sme.sap.com/capapplicationversion" + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 1.2.3 + workloads: + - name: cap-backend-srv + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/srv/server:latest + type: CAP + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + image: docker.image.repo/approuter/approuter:latest + type: Router + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + image: docker.image.repo/content/cap-content:latest + type: Content + - name: mtx-job + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + image: docker.image.repo/srv/server:latest + type: TenantOperation +status: + conditions: + - reason: CreatedDeployments + observedGeneration: 1 + status: "True" + type: Ready + observedGeneration: 1 + state: Ready diff --git a/internal/controller/testdata/captenant/cat-01.initial.yaml b/internal/controller/testdata/captenant/cat-01.initial.yaml new file mode 100644 index 0000000..5391ab6 --- /dev/null +++ b/internal/controller/testdata/captenant/cat-01.initial.yaml @@ -0,0 +1,11 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + name: test-cap-01-provider + namespace: default +spec: + capApplicationInstance: test-cap-01 + subDomain: my-provider + tenantId: tenant-id-for-provider + version: 5.6.7 + versionUpgradeStrategy: always diff --git a/internal/controller/testdata/captenant/cat-02.expected.yaml b/internal/controller/testdata/captenant/cat-02.expected.yaml new file mode 100644 index 0000000..c6f1db7 --- /dev/null +++ b/internal/controller/testdata/captenant/cat-02.expected.yaml @@ -0,0 +1,36 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + finalizers: + - sme.sap.com/captenant + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-consumer + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: consumer + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + name: test-cap-01-consumer + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-consumer + tenantId: tenant-id-for-consumer + version: 5.6.7 + versionUpgradeStrategy: always +status: + state: "" + conditions: + - type: Ready + status: "False" + reason: "ProcessingStarted" + message: "" diff --git a/internal/controller/testdata/captenant/cat-02.initial.yaml b/internal/controller/testdata/captenant/cat-02.initial.yaml new file mode 100644 index 0000000..81e7b43 --- /dev/null +++ b/internal/controller/testdata/captenant/cat-02.initial.yaml @@ -0,0 +1,11 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + name: test-cap-01-consumer + namespace: default +spec: + capApplicationInstance: test-cap-01 + subDomain: my-consumer + tenantId: tenant-id-for-consumer + version: 5.6.7 + versionUpgradeStrategy: always diff --git a/internal/controller/testdata/captenant/cat-03.expected.yaml b/internal/controller/testdata/captenant/cat-03.expected.yaml new file mode 100644 index 0000000..459db89 --- /dev/null +++ b/internal/controller/testdata/captenant/cat-03.expected.yaml @@ -0,0 +1,89 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + finalizers: + - sme.sap.com/captenant + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-consumer + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: consumer + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + name: test-cap-01-consumer + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-consumer + tenantId: tenant-id-for-consumer + version: 5.6.7 + versionUpgradeStrategy: always +status: + observedGeneration: 0 + state: Provisioning + conditions: + - type: Ready + status: "False" + observedGeneration: 0 + reason: ProvisioningOperationCreated + message: "waiting for CAPTenantOperation default.test-cap-01-consumer-gen of type provisioning to complete" +--- +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + generateName: test-cap-01-consumer- + name: test-cap-01-consumer-gen + namespace: default + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-consumer + labels: + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: a9df2080f99fd77b1b2c7e4cee1e1bff69498511 + sme.sap.com/tenant-operation-type: provisioning + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-consumer +spec: + capApplicationVersionInstance: test-cap-01-cav-v1 + subDomain: my-consumer + tenantId: tenant-id-for-consumer + operation: provisioning + steps: + - name: mtx + type: TenantOperation +--- +apiVersion: dns.gardener.cloud/v1alpha1 +kind: DNSEntry +metadata: + annotations: + dns.gardener.cloud/class: garden + sme.sap.com/resource-hash: c38b79c0bac0cf1b5b8e6757b31d892898fe18472338996f7103adbf0249c79e + sme.sap.com/owner-identifier: CAPTenant.default.test-cap-01-consumer + labels: + sme.sap.com/owner-identifier-hash: f57a7105fbe1d899a7757ddd04747b6b33dde594 + name: test-cap-01-consumer0 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-consumer +spec: + dnsName: my-consumer.foo.bar.local + targets: + - public-ingress.operator.testing.local diff --git a/internal/controller/testdata/captenant/cat-03.initial.yaml b/internal/controller/testdata/captenant/cat-03.initial.yaml new file mode 100644 index 0000000..61b257d --- /dev/null +++ b/internal/controller/testdata/captenant/cat-03.initial.yaml @@ -0,0 +1,36 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + finalizers: + - sme.sap.com/captenant + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-consumer + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: consumer + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + name: test-cap-01-consumer + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-consumer + tenantId: tenant-id-for-consumer + version: 5.6.7 + versionUpgradeStrategy: always +status: + state: "" + conditions: + - type: Ready + status: "False" + reason: "ProcessingStarted" + message: "" diff --git a/internal/controller/testdata/captenant/cat-04.expected.yaml b/internal/controller/testdata/captenant/cat-04.expected.yaml new file mode 100644 index 0000000..cfdbf5f --- /dev/null +++ b/internal/controller/testdata/captenant/cat-04.expected.yaml @@ -0,0 +1,99 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + finalizers: + - sme.sap.com/captenant + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-provider + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: provider + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-provider + tenantId: tenant-id-for-provider + version: 5.6.7 + versionUpgradeStrategy: always +status: + conditions: + - message: "CAPTenantOperation default.test-cap-01-provider-s6f4l successfully completed" + reason: ProvisioningCompleted + status: "True" + type: Ready + state: Ready + currentCAPApplicationVersionInstance: test-cap-01-cav-v1 +--- +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + annotations: + sme.sap.com/resource-hash: 2309c27d407c5e1d71529d3448da227ab25f63fb5f9d631d7c8a3e9fd0a1ff34 + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + gateways: + - test-cap-01-gw + - istio-system/cap-operator-domains-gen + hosts: + - my-provider.app-domain.test.local + - my-provider.foo.bar.local + http: + - match: + - uri: + prefix: / + route: + - destination: + host: test-cap-01-cav-v1-app-router-svc.default.svc.cluster.local + port: + number: 5000 + weight: 100 +--- +apiVersion: networking.istio.io/v1beta1 +kind: DestinationRule +metadata: + annotations: + sme.sap.com/resource-hash: 5d065e2112f26ad9b5ace902461365ba9cbf539123dea326f060534bc30e22d1 + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + host: test-cap-01-cav-v1-app-router-svc.default.svc.cluster.local + trafficPolicy: + loadBalancer: + consistentHash: + httpCookie: + name: JSESSIONID + path: / + ttl: 0s diff --git a/internal/controller/testdata/captenant/cat-04.initial.yaml b/internal/controller/testdata/captenant/cat-04.initial.yaml new file mode 100644 index 0000000..49339c7 --- /dev/null +++ b/internal/controller/testdata/captenant/cat-04.initial.yaml @@ -0,0 +1,72 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + finalizers: + - sme.sap.com/captenant + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-provider + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: provider + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-provider + tenantId: tenant-id-for-provider + version: 5.6.7 + versionUpgradeStrategy: always +status: + conditions: + - message: "created CAPTenantOperation default.test-cap-01-provider-s6f4l of type provisioning" + reason: ProvisioningRequestCreated + status: "False" + type: Ready + state: Provisioning +--- +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + generateName: test-cap-01-provider- + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/tenant-operation-type: provisioning + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + name: test-cap-01-provider-s6f4l + namespace: default + finalizers: + - sme.sap.com/captenantoperation + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + capApplicationVersionInstance: test-cap-01-cav-v1 + subDomain: my-provider + tenantId: tenant-id-for-provider + operation: provisioning + steps: + - name: mtx + type: TenantOperation +status: + conditions: + - message: job default.test-cap-01-provider-s6f4l-abc completed + reason: StepCompleted + status: "True" + type: Ready + state: Completed diff --git a/internal/controller/testdata/captenant/cat-05.expected.yaml b/internal/controller/testdata/captenant/cat-05.expected.yaml new file mode 100644 index 0000000..bbcc316 --- /dev/null +++ b/internal/controller/testdata/captenant/cat-05.expected.yaml @@ -0,0 +1,36 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + finalizers: + - sme.sap.com/captenant + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-provider + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: provider + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-provider + tenantId: tenant-id-for-provider + version: 5.6.7 + versionUpgradeStrategy: always +status: + conditions: + - message: "CAPTenantOperation default.test-cap-01-provider-s6f4l failed" + reason: ProvisioningFailed + status: "False" + type: Ready + state: ProvisioningError diff --git a/internal/controller/testdata/captenant/cat-05.initial.yaml b/internal/controller/testdata/captenant/cat-05.initial.yaml new file mode 100644 index 0000000..19fe291 --- /dev/null +++ b/internal/controller/testdata/captenant/cat-05.initial.yaml @@ -0,0 +1,72 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + finalizers: + - sme.sap.com/captenant + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-provider + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: provider + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-provider + tenantId: tenant-id-for-provider + version: 5.6.7 + versionUpgradeStrategy: always +status: + conditions: + - message: "waiting for CAPTenantOperation default.test-cap-01-provider-s6f4l of type provisioning to complete" + reason: ProvisioningRequestCreated + status: "False" + type: Ready + state: Provisioning +--- +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + generateName: test-cap-01-provider- + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/tenant-operation-type: provisioning + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + name: test-cap-01-provider-s6f4l + namespace: default + finalizers: + - sme.sap.com/captenantoperation + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + capApplicationVersionInstance: test-cap-01-cav-v1 + subDomain: my-provider + tenantId: tenant-id-for-provider + operation: provisioning + steps: + - name: mtx + type: TenantOperation +status: + conditions: + - message: job default.test-cap-01-provider-s6f4l-abcs failed + reason: JobFailed + status: "True" + type: Ready + state: Failed diff --git a/internal/controller/testdata/captenant/cat-06.expected.yaml b/internal/controller/testdata/captenant/cat-06.expected.yaml new file mode 100644 index 0000000..a3a1370 --- /dev/null +++ b/internal/controller/testdata/captenant/cat-06.expected.yaml @@ -0,0 +1,36 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + finalizers: + - sme.sap.com/captenant + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-provider + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: provider + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-provider + tenantId: tenant-id-for-provider + version: 5.6.7 + versionUpgradeStrategy: always +status: + conditions: + - message: "waiting for CAPTenantOperation default.test-cap-01-provider-s6f4l of type provisioning to complete" + reason: ProvisioningOperationCreated + status: "False" + type: Ready + state: Provisioning diff --git a/internal/controller/testdata/captenant/cat-06.initial.yaml b/internal/controller/testdata/captenant/cat-06.initial.yaml new file mode 100644 index 0000000..d3194da --- /dev/null +++ b/internal/controller/testdata/captenant/cat-06.initial.yaml @@ -0,0 +1,69 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + finalizers: + - sme.sap.com/captenant + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-provider + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: provider + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-provider + tenantId: tenant-id-for-provider + version: 5.6.7 + versionUpgradeStrategy: always +status: + conditions: + - message: "created CAPTenantOperation default.test-cap-01-provider-s6f4l of type provisioning" + reason: ProvisioningOperationCreated + status: "False" + type: Ready + state: Provisioning +--- +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + generateName: test-cap-01-provider- + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/tenant-operation-type: provisioning + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + name: test-cap-01-provider-s6f4l + namespace: default + finalizers: + - sme.sap.com/captenantoperation + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + capApplicationVersionInstance: test-cap-01-cav-v1 + subDomain: my-provider + tenantId: tenant-id-for-provider + operation: provisioning +status: + conditions: + - message: waiting for job default.test-cap-01-provider-s6f4l-abcd + reason: JobRunning + status: "False" + type: Ready + state: Processing diff --git a/internal/controller/testdata/captenant/cat-07.expected.yaml b/internal/controller/testdata/captenant/cat-07.expected.yaml new file mode 100644 index 0000000..cd16b27 --- /dev/null +++ b/internal/controller/testdata/captenant/cat-07.expected.yaml @@ -0,0 +1,67 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + finalizers: + - sme.sap.com/captenant + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-provider + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: provider + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-provider + tenantId: tenant-id-for-provider + version: 8.9.10 + versionUpgradeStrategy: always +status: + conditions: + - message: "waiting for CAPTenantOperation default.test-cap-01-provider-gen of type upgrade to complete" + reason: UpgradeOperationCreated + status: "True" + type: Ready + state: Upgrading + currentCAPApplicationVersionInstance: test-cap-01-cav-v1 +--- +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + generateName: test-cap-01-provider- + name: test-cap-01-provider-gen + namespace: default + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/cav-version: "8.9.10" + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + capApplicationVersionInstance: test-cap-01-cav-v2 + subDomain: my-provider + tenantId: tenant-id-for-provider + operation: upgrade + steps: + - name: mtx + type: TenantOperation diff --git a/internal/controller/testdata/captenant/cat-07.initial.yaml b/internal/controller/testdata/captenant/cat-07.initial.yaml new file mode 100644 index 0000000..d465dde --- /dev/null +++ b/internal/controller/testdata/captenant/cat-07.initial.yaml @@ -0,0 +1,37 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + finalizers: + - sme.sap.com/captenant + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-provider + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: provider + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-provider + tenantId: tenant-id-for-provider + version: 8.9.10 + versionUpgradeStrategy: always +status: + conditions: + - message: "CAPTenantOperation default.test-cap-01-provider-s6f4l successfully completed" + reason: ProvisioningCompleted + status: "True" + type: Ready + state: Ready + currentCAPApplicationVersionInstance: test-cap-01-cav-v1 diff --git a/internal/controller/testdata/captenant/cat-08.expected.yaml b/internal/controller/testdata/captenant/cat-08.expected.yaml new file mode 100644 index 0000000..0aba4c6 --- /dev/null +++ b/internal/controller/testdata/captenant/cat-08.expected.yaml @@ -0,0 +1,37 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + finalizers: + - sme.sap.com/captenant + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-provider + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: provider + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-provider + tenantId: tenant-id-for-provider + version: 8.9.10 + versionUpgradeStrategy: never +status: + conditions: + - message: "CAPTenantOperation default.test-cap-01-provider-s6f4l successfully completed" + reason: ProvisioningCompleted + status: "True" + type: Ready + state: Ready + currentCAPApplicationVersionInstance: test-cap-01-cav-v1 diff --git a/internal/controller/testdata/captenant/cat-08.initial.yaml b/internal/controller/testdata/captenant/cat-08.initial.yaml new file mode 100644 index 0000000..0aba4c6 --- /dev/null +++ b/internal/controller/testdata/captenant/cat-08.initial.yaml @@ -0,0 +1,37 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + finalizers: + - sme.sap.com/captenant + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-provider + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: provider + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-provider + tenantId: tenant-id-for-provider + version: 8.9.10 + versionUpgradeStrategy: never +status: + conditions: + - message: "CAPTenantOperation default.test-cap-01-provider-s6f4l successfully completed" + reason: ProvisioningCompleted + status: "True" + type: Ready + state: Ready + currentCAPApplicationVersionInstance: test-cap-01-cav-v1 diff --git a/internal/controller/testdata/captenant/cat-09.expected.yaml b/internal/controller/testdata/captenant/cat-09.expected.yaml new file mode 100644 index 0000000..a2a3d0c --- /dev/null +++ b/internal/controller/testdata/captenant/cat-09.expected.yaml @@ -0,0 +1,67 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + deletionTimestamp: "2022-03-22T13:24:38Z" + finalizers: + - sme.sap.com/captenant + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-provider + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: provider + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-provider + tenantId: tenant-id-for-provider + version: 5.6.7 + versionUpgradeStrategy: always +status: + conditions: + - message: "waiting for CAPTenantOperation default.test-cap-01-provider-gen of type deprovisioning to complete" + reason: DeprovisioningOperationCreated + status: "False" + type: Ready + state: Deleting + currentCAPApplicationVersionInstance: test-cap-01-cav-v1 +--- +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + generateName: test-cap-01-provider- + name: test-cap-01-provider-gen + namespace: default + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + sme.sap.com/tenant-operation-type: deprovisioning + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + capApplicationVersionInstance: test-cap-01-cav-v1 + subDomain: my-provider + tenantId: tenant-id-for-provider + operation: deprovisioning + steps: + - name: mtx + type: TenantOperation diff --git a/internal/controller/testdata/captenant/cat-09.initial.yaml b/internal/controller/testdata/captenant/cat-09.initial.yaml new file mode 100644 index 0000000..6eb2a21 --- /dev/null +++ b/internal/controller/testdata/captenant/cat-09.initial.yaml @@ -0,0 +1,38 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + deletionTimestamp: "2022-03-22T13:24:38Z" + finalizers: + - sme.sap.com/captenant + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-provider + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: provider + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-provider + tenantId: tenant-id-for-provider + version: 5.6.7 + versionUpgradeStrategy: always +status: + conditions: + - message: "CAPTenantOperation default.test-cap-01-provider-s6f4l successfully completed" + reason: ProvisioningCompleted + status: "True" + type: Ready + state: Ready + currentCAPApplicationVersionInstance: test-cap-01-cav-v1 diff --git a/internal/controller/testdata/captenant/cat-10.expected.yaml b/internal/controller/testdata/captenant/cat-10.expected.yaml new file mode 100644 index 0000000..47eeb1c --- /dev/null +++ b/internal/controller/testdata/captenant/cat-10.expected.yaml @@ -0,0 +1,37 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + deletionTimestamp: "2022-03-22T13:24:38Z" + finalizers: [] + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-provider + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: provider + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-provider + tenantId: tenant-id-for-provider + version: 5.6.7 + versionUpgradeStrategy: always +status: + conditions: + - message: "created CAPTenantOperation default.test-cap-01-provider-gen of type deprovisioning" + reason: DeprovisioningOperationCreated + status: "False" + type: Ready + state: Deleting + currentCAPApplicationVersionInstance: test-cap-01-cav-v1 diff --git a/internal/controller/testdata/captenant/cat-10.initial.yaml b/internal/controller/testdata/captenant/cat-10.initial.yaml new file mode 100644 index 0000000..329004c --- /dev/null +++ b/internal/controller/testdata/captenant/cat-10.initial.yaml @@ -0,0 +1,74 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + deletionTimestamp: "2022-03-22T13:24:38Z" + finalizers: + - sme.sap.com/captenant + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-provider + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: provider + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-provider + tenantId: tenant-id-for-provider + version: 5.6.7 + versionUpgradeStrategy: always +status: + conditions: + - message: "created CAPTenantOperation default.test-cap-01-provider-gen of type deprovisioning" + reason: DeprovisioningOperationCreated + status: "False" + type: Ready + state: Deleting + currentCAPApplicationVersionInstance: test-cap-01-cav-v1 +--- +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + generateName: test-cap-01-provider- + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/tenant-operation-type: deprovisioning + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + name: test-cap-01-provider-gen + namespace: default + finalizers: + - sme.sap.com/captenantoperation + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + capApplicationVersionInstance: test-cap-01-cav-v1 + subDomain: my-provider + tenantId: tenant-id-for-provider + operation: deprovisioning + steps: + - name: mtx + type: TenantOperation +status: + conditions: + - message: job default.test-cap-01-provider-gen-abcd completed + reason: StepCompleted + status: "True" + type: Ready + state: Completed diff --git a/internal/controller/testdata/captenant/cat-11.expected.yaml b/internal/controller/testdata/captenant/cat-11.expected.yaml new file mode 100644 index 0000000..b3f1422 --- /dev/null +++ b/internal/controller/testdata/captenant/cat-11.expected.yaml @@ -0,0 +1,36 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + deletionTimestamp: "2022-03-22T13:24:38Z" + finalizers: [] + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-provider + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: provider + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-provider + tenantId: tenant-id-for-provider + version: 5.6.7 + versionUpgradeStrategy: always +status: + conditions: + - message: "CAPTenantOperation default.test-cap-01-provider-s6f4l failed" + reason: ProvisioningFailed + status: "False" + type: Ready + state: ProvisioningError diff --git a/internal/controller/testdata/captenant/cat-11.initial.yaml b/internal/controller/testdata/captenant/cat-11.initial.yaml new file mode 100644 index 0000000..650fd05 --- /dev/null +++ b/internal/controller/testdata/captenant/cat-11.initial.yaml @@ -0,0 +1,37 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + deletionTimestamp: "2022-03-22T13:24:38Z" + finalizers: + - sme.sap.com/captenant + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-provider + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: provider + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-provider + tenantId: tenant-id-for-provider + version: 5.6.7 + versionUpgradeStrategy: always +status: + conditions: + - message: "CAPTenantOperation default.test-cap-01-provider-s6f4l failed" + reason: ProvisioningFailed + status: "False" + type: Ready + state: ProvisioningError diff --git a/internal/controller/testdata/captenant/cat-13.expected.yaml b/internal/controller/testdata/captenant/cat-13.expected.yaml new file mode 100644 index 0000000..3847064 --- /dev/null +++ b/internal/controller/testdata/captenant/cat-13.expected.yaml @@ -0,0 +1,105 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + generation: 2 + finalizers: + - sme.sap.com/captenant + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-provider + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: provider + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-provider + tenantId: tenant-id-for-provider + version: 8.9.10 + versionUpgradeStrategy: always +status: + conditions: + - message: "CAPTenantOperation default.test-cap-01-provider-upg successfully completed" + reason: UpgradeCompleted + observedGeneration: 2 + status: "True" + type: Ready + state: Ready + currentCAPApplicationVersionInstance: test-cap-01-cav-v2 + previousCAPApplicationVersions: + - test-cap-01-cav-v1 + observedGeneration: 2 +--- +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + annotations: + sme.sap.com/resource-hash: "4973adfd25df6a6ef553c2d02ec88a8165e83bc25cc46d608904ffb7c6a77cbd" + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + gateways: + - test-cap-01-gw + - istio-system/cap-operator-domains-gen + hosts: + - my-provider.app-domain.test.local + - my-provider.foo.bar.local + http: + - match: + - uri: + prefix: / + route: + - destination: + host: test-cap-01-cav-v2-app-router-svc.default.svc.cluster.local + port: + number: 5000 + weight: 100 +--- +apiVersion: networking.istio.io/v1beta1 +kind: DestinationRule +metadata: + annotations: + sme.sap.com/resource-hash: ca930fa3ab9c0c67bf3d2d948cdacefd93505d6089f56ebcd7cc7cf06e66ad7d + sme.sap.com/owner-identifier: default.test-cap-01-provider + generation: 1 + labels: + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + host: test-cap-01-cav-v2-app-router-svc.default.svc.cluster.local + trafficPolicy: + loadBalancer: + consistentHash: + httpCookie: + name: JSESSIONID + path: / + ttl: 0s diff --git a/internal/controller/testdata/captenant/cat-13.initial.yaml b/internal/controller/testdata/captenant/cat-13.initial.yaml new file mode 100644 index 0000000..c830e00 --- /dev/null +++ b/internal/controller/testdata/captenant/cat-13.initial.yaml @@ -0,0 +1,75 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + generation: 2 + finalizers: + - sme.sap.com/captenant + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-provider + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: provider + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-provider + tenantId: tenant-id-for-provider + version: 8.9.10 + versionUpgradeStrategy: always +status: + conditions: + - message: "waiting for CAPTenantOperation default.test-cap-01-provider-upg of type upgrade to complete" + reason: UpgradeOperationCreated + status: "False" + type: Ready + state: Upgrading + currentCAPApplicationVersionInstance: test-cap-01-cav-v1 +--- +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + generateName: test-cap-01-provider- + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/cav-version: "8.9.10" + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + name: test-cap-01-provider-upg + namespace: default + finalizers: + - sme.sap.com/captenantoperation + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + capApplicationVersionInstance: test-cap-01-cav-v2 + subDomain: my-provider + tenantId: tenant-id-for-provider + operation: upgrade + steps: + - name: mtx + type: TenantOperation +status: + conditions: + - message: job default.test-cap-01-provider-upg-abcd completed + reason: StepCompleted + status: "True" + type: Ready + state: Completed diff --git a/internal/controller/testdata/captenant/cat-14.initial.yaml b/internal/controller/testdata/captenant/cat-14.initial.yaml new file mode 100644 index 0000000..5cfcdab --- /dev/null +++ b/internal/controller/testdata/captenant/cat-14.initial.yaml @@ -0,0 +1,29 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + finalizers: + - sme.sap.com/captenant + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-consumer + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: consumer + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + name: test-cap-01-consumer + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-consumer + tenantId: tenant-id-for-consumer + version: 5.6.7 + versionUpgradeStrategy: always diff --git a/internal/controller/testdata/captenant/cat-15.expected.yaml b/internal/controller/testdata/captenant/cat-15.expected.yaml new file mode 100644 index 0000000..ba4fe88 --- /dev/null +++ b/internal/controller/testdata/captenant/cat-15.expected.yaml @@ -0,0 +1,102 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + generation: 3 + finalizers: + - sme.sap.com/captenant + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-provider + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: provider + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-changed-provider + tenantId: tenant-id-for-provider + version: 8.9.10 + versionUpgradeStrategy: always +status: + conditions: + - message: "VirtualService (and DestinationRule) default.test-cap-01-provider was reconciled" + reason: TenantNetworkingModified + observedGeneration: 3 + status: "True" + type: Ready + state: Ready + currentCAPApplicationVersionInstance: test-cap-01-cav-v2 + observedGeneration: 3 +--- +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + annotations: + sme.sap.com/resource-hash: 45f51780d8501067cf08285170eb13e1fbfbccbfcd3ba57bf7116d026ce98af8 + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/owner-generation: "3" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + gateways: + - test-cap-01-gw + - istio-system/cap-operator-domains-gen + hosts: + - my-changed-provider.app-domain.test.local + - my-changed-provider.foo.bar.local + http: + - match: + - uri: + prefix: / + route: + - destination: + host: test-cap-01-cav-v2-app-router-svc.default.svc.cluster.local + port: + number: 5000 + weight: 100 +--- +apiVersion: networking.istio.io/v1beta1 +kind: DestinationRule +metadata: + annotations: + sme.sap.com/resource-hash: ca930fa3ab9c0c67bf3d2d948cdacefd93505d6089f56ebcd7cc7cf06e66ad7d + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/owner-generation: "3" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + host: test-cap-01-cav-v2-app-router-svc.default.svc.cluster.local + trafficPolicy: + loadBalancer: + consistentHash: + httpCookie: + name: JSESSIONID + path: / + ttl: 0s diff --git a/internal/controller/testdata/captenant/cat-15.initial.yaml b/internal/controller/testdata/captenant/cat-15.initial.yaml new file mode 100644 index 0000000..16180a6 --- /dev/null +++ b/internal/controller/testdata/captenant/cat-15.initial.yaml @@ -0,0 +1,74 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + generation: 3 + finalizers: + - sme.sap.com/captenant + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-provider + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: provider + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-changed-provider + tenantId: tenant-id-for-provider + version: 8.9.10 + versionUpgradeStrategy: always +status: + conditions: + - message: "CAPTenantOperation default.test-cap-01-provider-upg successfully completed" + reason: UpgradeCompleted + observedGeneration: 2 + status: "True" + type: Ready + state: Ready + currentCAPApplicationVersionInstance: test-cap-01-cav-v2 + observedGeneration: 2 +--- +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + annotations: + sme.sap.com/resource-hash: "6be23b7628815f7a8b5ea532763e4474f0c7b05f7209fcba047cabd0b29d879e" + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + gateways: + - test-cap-01-gw + hosts: + - my-provider.app-domain.test.local + - my-provider.foo.bar.local + http: + - match: + - uri: + prefix: / + route: + - destination: + host: test-cap-01-cav-v2-app-router-svc.default.svc.cluster.local + port: + number: 4000 + weight: 100 diff --git a/internal/controller/testdata/captenant/cat-16.expected.yaml b/internal/controller/testdata/captenant/cat-16.expected.yaml new file mode 100644 index 0000000..16180a6 --- /dev/null +++ b/internal/controller/testdata/captenant/cat-16.expected.yaml @@ -0,0 +1,74 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + generation: 3 + finalizers: + - sme.sap.com/captenant + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-provider + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: provider + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-changed-provider + tenantId: tenant-id-for-provider + version: 8.9.10 + versionUpgradeStrategy: always +status: + conditions: + - message: "CAPTenantOperation default.test-cap-01-provider-upg successfully completed" + reason: UpgradeCompleted + observedGeneration: 2 + status: "True" + type: Ready + state: Ready + currentCAPApplicationVersionInstance: test-cap-01-cav-v2 + observedGeneration: 2 +--- +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + annotations: + sme.sap.com/resource-hash: "6be23b7628815f7a8b5ea532763e4474f0c7b05f7209fcba047cabd0b29d879e" + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + gateways: + - test-cap-01-gw + hosts: + - my-provider.app-domain.test.local + - my-provider.foo.bar.local + http: + - match: + - uri: + prefix: / + route: + - destination: + host: test-cap-01-cav-v2-app-router-svc.default.svc.cluster.local + port: + number: 4000 + weight: 100 diff --git a/internal/controller/testdata/captenant/cat-17.expected.yaml b/internal/controller/testdata/captenant/cat-17.expected.yaml new file mode 100644 index 0000000..aa4c128 --- /dev/null +++ b/internal/controller/testdata/captenant/cat-17.expected.yaml @@ -0,0 +1,124 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + generation: 3 + finalizers: + - sme.sap.com/captenant + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-provider + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: provider + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-changed-provider + tenantId: tenant-id-for-provider + version: 8.9.10 + versionUpgradeStrategy: always +status: + conditions: + - message: "VirtualService (and DestinationRule) default.test-cap-01-provider was reconciled" + reason: TenantNetworkingModified + observedGeneration: 3 + status: "True" + type: Ready + state: Ready + currentCAPApplicationVersionInstance: test-cap-01-cav-v2 + observedGeneration: 3 +--- +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + annotations: + sme.sap.com/resource-hash: 45f51780d8501067cf08285170eb13e1fbfbccbfcd3ba57bf7116d026ce98af8 + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/owner-generation: "3" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + gateways: + - test-cap-01-gw + - istio-system/cap-operator-domains-gen + hosts: + - my-changed-provider.app-domain.test.local + - my-changed-provider.foo.bar.local + http: + - match: + - uri: + prefix: / + route: + - destination: + host: test-cap-01-cav-v2-app-router-svc.default.svc.cluster.local + port: + number: 5000 + weight: 100 +--- +apiVersion: networking.istio.io/v1beta1 +kind: DestinationRule +metadata: + annotations: + sme.sap.com/resource-hash: ca930fa3ab9c0c67bf3d2d948cdacefd93505d6089f56ebcd7cc7cf06e66ad7d + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/owner-generation: "3" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + host: test-cap-01-cav-v2-app-router-svc.default.svc.cluster.local + trafficPolicy: + loadBalancer: + consistentHash: + httpCookie: + name: JSESSIONID + path: / + ttl: 0s +--- +apiVersion: dns.gardener.cloud/v1alpha1 +kind: DNSEntry +metadata: + annotations: + dns.gardener.cloud/class: garden + sme.sap.com/resource-hash: be44dd98e914aa033f04f18a03338da45b40090b55e0e1c935353f088bd7c583 + sme.sap.com/owner-identifier: CAPTenant.default.test-cap-01-provider + labels: + sme.sap.com/owner-identifier-hash: ec24f9b09337c244cf5ac64b539f8c04f507cd99 + name: test-cap-01-provider0 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + dnsName: my-changed-provider.foo.bar.local + targets: + - public-ingress.operator.testing.local diff --git a/internal/controller/testdata/captenant/cat-17.initial.yaml b/internal/controller/testdata/captenant/cat-17.initial.yaml new file mode 100644 index 0000000..d3dfea9 --- /dev/null +++ b/internal/controller/testdata/captenant/cat-17.initial.yaml @@ -0,0 +1,101 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + generation: 3 + finalizers: + - sme.sap.com/captenant + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-provider + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: provider + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-changed-provider + tenantId: tenant-id-for-provider + version: 8.9.10 + versionUpgradeStrategy: always +status: + conditions: + - message: "CAPTenantOperation default.test-cap-01-provider-upg successfully completed" + reason: UpgradeCompleted + status: "True" + type: Ready + state: Ready + currentCAPApplicationVersionInstance: test-cap-01-cav-v2 + observedGeneration: 2 +--- +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + annotations: + sme.sap.com/resource-hash: "6be23b7628815f7a8b5ea532763e4474f0c7b05f7209fcba047cabd0b29d879e" + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + gateways: + - test-cap-01-gw + - istio-system/cap-operator-domains-gen + hosts: + - my-changed-provider.app-domain.test.local + - my-changed-provider.foo.bar.local + http: + - match: + - uri: + prefix: / + route: + - destination: + host: test-cap-01-cav-v2-app-router-svc.default.svc.cluster.local + port: + number: 4000 + weight: 100 +--- +apiVersion: networking.istio.io/v1beta1 +kind: DestinationRule +metadata: + annotations: + sme.sap.com/resource-hash: ca930fa3ab9c0c67bf3d2d948cdacefd93505d6089f56ebcd7cc7cf06e66ad7d + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/owner-generation: "3" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + host: test-cap-01-cav-v2-app-router-svc.default.svc.cluster.local + trafficPolicy: + loadBalancer: + consistentHash: + httpCookie: + name: JSESSIONID + path: / + ttl: 0s diff --git a/internal/controller/testdata/captenant/cat-20.expected.yaml b/internal/controller/testdata/captenant/cat-20.expected.yaml new file mode 100644 index 0000000..b7d9d96 --- /dev/null +++ b/internal/controller/testdata/captenant/cat-20.expected.yaml @@ -0,0 +1,36 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + finalizers: + - sme.sap.com/captenant + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-provider + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: provider + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-provider + tenantId: tenant-id-for-provider + version: 5.6.7 + versionUpgradeStrategy: always +status: + conditions: + - message: "waiting for CAPTenantOperation default.test-cap-01-provider-gen of type provisioning to complete" + reason: ProvisioningOperationCreated + status: "False" + type: Ready + state: Provisioning diff --git a/internal/controller/testdata/captenant/cat-20.initial.yaml b/internal/controller/testdata/captenant/cat-20.initial.yaml new file mode 100644 index 0000000..a3a1370 --- /dev/null +++ b/internal/controller/testdata/captenant/cat-20.initial.yaml @@ -0,0 +1,36 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + finalizers: + - sme.sap.com/captenant + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-provider + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: provider + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-provider + tenantId: tenant-id-for-provider + version: 5.6.7 + versionUpgradeStrategy: always +status: + conditions: + - message: "waiting for CAPTenantOperation default.test-cap-01-provider-s6f4l of type provisioning to complete" + reason: ProvisioningOperationCreated + status: "False" + type: Ready + state: Provisioning diff --git a/internal/controller/testdata/captenant/cat-21.expected.yaml b/internal/controller/testdata/captenant/cat-21.expected.yaml new file mode 100644 index 0000000..8f2b9f8 --- /dev/null +++ b/internal/controller/testdata/captenant/cat-21.expected.yaml @@ -0,0 +1,78 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + deletionTimestamp: "2022-03-22T13:24:38Z" + generation: 2 + finalizers: + - sme.sap.com/captenant + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-provider + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: provider + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-provider + tenantId: tenant-id-for-provider + version: 8.9.10 + versionUpgradeStrategy: always +status: + conditions: + - message: "CAPTenantOperation default.test-cap-01-provider-upg successfully completed" + reason: UpgradeCompleted + observedGeneration: 2 + status: "True" + type: Ready + state: Ready + currentCAPApplicationVersionInstance: test-cap-01-cav-v2 + previousCAPApplicationVersions: + - test-cap-01-cav-v1 + observedGeneration: 2 +--- +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + annotations: + sme.sap.com/resource-hash: "4973adfd25df6a6ef553c2d02ec88a8165e83bc25cc46d608904ffb7c6a77cbd" + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + gateways: + - test-cap-01-gw + - istio-system/cap-operator-domains-gen + hosts: + - my-provider.app-domain.test.local + - my-provider.foo.bar.local + http: + - match: + - uri: + prefix: / + route: + - destination: + host: test-cap-01-cav-v2-app-router-svc.default.svc.cluster.local + port: + number: 5000 + weight: 100 diff --git a/internal/controller/testdata/captenant/cat-21.initial.yaml b/internal/controller/testdata/captenant/cat-21.initial.yaml new file mode 100644 index 0000000..13af6ea --- /dev/null +++ b/internal/controller/testdata/captenant/cat-21.initial.yaml @@ -0,0 +1,109 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + deletionTimestamp: "2022-03-22T13:24:38Z" + generation: 2 + finalizers: + - sme.sap.com/captenant + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-provider + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: provider + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-provider + tenantId: tenant-id-for-provider + version: 8.9.10 + versionUpgradeStrategy: always +status: + conditions: + - message: "created CAPTenantOperation default.test-cap-01-provider-upg of type upgrade" + reason: UpgradeOperationCreated + status: "False" + type: Ready + state: Upgrading + currentCAPApplicationVersionInstance: test-cap-01-cav-v1 +--- +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + generateName: test-cap-01-provider- + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/cav-version: "8.9.10" + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + name: test-cap-01-provider-upg + namespace: default + finalizers: + - sme.sap.com/captenantoperation + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + capApplicationVersionInstance: test-cap-01-cav-v2 + subDomain: my-provider + tenantId: tenant-id-for-provider + operation: upgrade + steps: + - name: mtx + type: TenantOperation +status: + conditions: + - message: job default.test-cap-01-provider-upg-abcd completed + reason: StepCompleted + status: "True" + type: Ready + state: Completed +--- +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + gateways: + - test-cap-01-gw + hosts: + - my-provider.app-domain.test.local + - my-provider.foo.bar.local + http: + - match: + - uri: + prefix: / + route: + - destination: + host: test-cap-01-cav-v2-app-router-svc.default.svc.cluster.local + port: + number: 4000 + weight: 100 diff --git a/internal/controller/testdata/captenant/cat-22.initial.yaml b/internal/controller/testdata/captenant/cat-22.initial.yaml new file mode 100644 index 0000000..e587c4b --- /dev/null +++ b/internal/controller/testdata/captenant/cat-22.initial.yaml @@ -0,0 +1,106 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + generation: 2 + finalizers: + - sme.sap.com/captenant + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-provider + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: provider + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-provider + tenantId: tenant-id-for-provider + version: 8.9.10 + versionUpgradeStrategy: always +status: + conditions: + - message: "waiting for CAPTenantOperation default.test-cap-01-provider-upg of type upgrade to complete" + reason: UpgradeOperationCreated + status: "False" + type: Ready + state: Upgrading + currentCAPApplicationVersionInstance: test-cap-01-cav-v1 +--- +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + generateName: test-cap-01-provider- + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/cav-version: "8.9.10" + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + name: test-cap-01-provider-upg + namespace: default + finalizers: + - sme.sap.com/captenantoperation + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + capApplicationVersionInstance: test-cap-01-cav-v2 + subDomain: my-provider + tenantId: tenant-id-for-provider + operation: upgrade + steps: + - name: mtx + type: TenantOperation +status: + conditions: + - message: job default.test-cap-01-provider-upg-abcs completed + reason: StepCompleted + status: "True" + type: Ready + state: Completed +--- +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + labels: + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier: default.test-cap-01-wrong-tenant + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-wrong-tenant +spec: + gateways: + - test-cap-01-gw + hosts: + - my-provider.app-domain.test.local + - my-provider.foo.bar.local + http: + - match: + - uri: + prefix: / + route: + - destination: + host: test-cap-01-cav-v1-router-svc.default.svc.cluster.local + port: + number: 4000 + weight: 100 diff --git a/internal/controller/testdata/captenant/cat-23.expected.yaml b/internal/controller/testdata/captenant/cat-23.expected.yaml new file mode 100644 index 0000000..2d6737c --- /dev/null +++ b/internal/controller/testdata/captenant/cat-23.expected.yaml @@ -0,0 +1,40 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + generation: 2 + finalizers: + - sme.sap.com/captenant + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-provider + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: provider + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-provider + tenantId: tenant-id-for-provider + version: 8.9.10 + versionUpgradeStrategy: always +status: + conditions: + - message: "CAPTenantOperation default.test-cap-01-provider-upg failed" + reason: UpgradeFailed + observedGeneration: 2 + status: "False" + type: Ready + state: UpgradeError + currentCAPApplicationVersionInstance: test-cap-01-cav-v1 + observedGeneration: 2 diff --git a/internal/controller/testdata/captenant/cat-23.initial.yaml b/internal/controller/testdata/captenant/cat-23.initial.yaml new file mode 100644 index 0000000..9c008c7 --- /dev/null +++ b/internal/controller/testdata/captenant/cat-23.initial.yaml @@ -0,0 +1,75 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + generation: 2 + finalizers: + - sme.sap.com/captenant + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-provider + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: provider + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-provider + tenantId: tenant-id-for-provider + version: 8.9.10 + versionUpgradeStrategy: always +status: + conditions: + - message: "CAPTenantOperation default.test-cap-01-provider-upg failed" + reason: UpgradeFailed + observedGeneration: 2 + status: "False" + type: Ready + state: UpgradeError + currentCAPApplicationVersionInstance: test-cap-01-cav-v1 + observedGeneration: 2 +--- +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/cav-version: "8.9.10" + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + name: test-cap-01-provider-upg + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + capApplicationVersionInstance: test-cap-01-cav-v2 + subDomain: my-provider + tenantId: tenant-id-for-provider + operation: upgrade + steps: + - name: mtx + type: TenantOperation +status: + conditions: + - message: job default.test-cap-01-provider-upg-abcd failed + reason: StepFailed + status: "True" + type: Ready + observedGeneration: 1 + state: Failed diff --git a/internal/controller/testdata/captenant/cat-24.expected.yaml b/internal/controller/testdata/captenant/cat-24.expected.yaml new file mode 100644 index 0000000..0d2e497 --- /dev/null +++ b/internal/controller/testdata/captenant/cat-24.expected.yaml @@ -0,0 +1,70 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + generation: 3 + finalizers: + - sme.sap.com/captenant + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-provider + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: provider + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-provider + tenantId: tenant-id-for-provider + version: 11.12.13 + versionUpgradeStrategy: always +status: + conditions: + - message: "waiting for CAPTenantOperation default.test-cap-01-provider-gen of type upgrade to complete" + reason: UpgradeOperationCreated + observedGeneration: 3 + status: "True" + type: Ready + state: Upgrading + currentCAPApplicationVersionInstance: test-cap-01-cav-v1 + observedGeneration: 3 +--- +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + generateName: test-cap-01-provider- + name: test-cap-01-provider-gen + namespace: default + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/owner-generation: "3" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/cav-version: "11.12.13" + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + capApplicationVersionInstance: test-cap-01-cav-v3 + subDomain: my-provider + tenantId: tenant-id-for-provider + operation: upgrade + steps: + - name: mtx + type: TenantOperation diff --git a/internal/controller/testdata/captenant/cat-24.initial.yaml b/internal/controller/testdata/captenant/cat-24.initial.yaml new file mode 100644 index 0000000..579c4c9 --- /dev/null +++ b/internal/controller/testdata/captenant/cat-24.initial.yaml @@ -0,0 +1,74 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + generation: 3 + finalizers: + - sme.sap.com/captenant + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-provider + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: provider + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-provider + tenantId: tenant-id-for-provider + version: 11.12.13 + versionUpgradeStrategy: always +status: + conditions: + - message: "CAPTenantOperation default.test-cap-01-provider-upg failed" + reason: UpgradeFailed + status: "True" + type: Ready + state: UpgradeError + currentCAPApplicationVersionInstance: test-cap-01-cav-v1 + observedGeneration: 2 +--- +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/cav-version: "8.9.10" + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + name: test-cap-01-provider-upg + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + capApplicationVersionInstance: test-cap-01-cav-v2 + subDomain: my-provider + tenantId: tenant-id-for-provider + operation: upgrade + steps: + - name: mtx + type: TenantOperation +status: + conditions: + - message: job default.test-cap-01-provider-upg-xyz failed + reason: StepFailed + status: "True" + type: Ready + observedGeneration: 1 + state: Failed diff --git a/internal/controller/testdata/captenant/cat-25.expected.yaml b/internal/controller/testdata/captenant/cat-25.expected.yaml new file mode 100644 index 0000000..fc05413 --- /dev/null +++ b/internal/controller/testdata/captenant/cat-25.expected.yaml @@ -0,0 +1,70 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + generation: 2 + finalizers: + - sme.sap.com/captenant + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-provider + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: provider + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-provider + tenantId: tenant-id-for-provider + version: 8.9.10 + versionUpgradeStrategy: always +status: + conditions: + - message: "waiting for CAPTenantOperation default.test-cap-01-provider-gen of type upgrade to complete" + reason: UpgradeOperationCreated + observedGeneration: 2 + status: "True" + type: Ready + state: Upgrading + currentCAPApplicationVersionInstance: test-cap-01-cav-v1 + observedGeneration: 2 +--- +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + generateName: test-cap-01-provider- + name: test-cap-01-provider-gen + namespace: default + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: "default.test-cap-01-provider" + labels: + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/cav-version: "8.9.10" + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + capApplicationVersionInstance: test-cap-01-cav-v2 + subDomain: my-provider + tenantId: tenant-id-for-provider + operation: upgrade + steps: + - name: mtx + type: TenantOperation diff --git a/internal/controller/testdata/captenant/cat-25.initial.yaml b/internal/controller/testdata/captenant/cat-25.initial.yaml new file mode 100644 index 0000000..e9f95cd --- /dev/null +++ b/internal/controller/testdata/captenant/cat-25.initial.yaml @@ -0,0 +1,39 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + generation: 2 + finalizers: + - sme.sap.com/captenant + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-provider + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: provider + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-provider + tenantId: tenant-id-for-provider + version: 8.9.10 + versionUpgradeStrategy: always +status: + conditions: + - message: "CAPTenantOperation default.test-cap-01-provider-upg failed" + reason: UpgradeFailed + status: "True" + type: Ready + state: UpgradeError + currentCAPApplicationVersionInstance: test-cap-01-cav-v1 + observedGeneration: 2 diff --git a/internal/controller/testdata/captenant/cat-26.initial.yaml b/internal/controller/testdata/captenant/cat-26.initial.yaml new file mode 100644 index 0000000..901e775 --- /dev/null +++ b/internal/controller/testdata/captenant/cat-26.initial.yaml @@ -0,0 +1,72 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + finalizers: + - sme.sap.com/captenant + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-provider + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: provider + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-provider + tenantId: tenant-id-for-provider + version: 5.6.7 + versionUpgradeStrategy: always +status: + conditions: + - message: "waiting for CAPTenantOperation default.test-cap-01-provider-s6f4l of type provisioning to complete" + reason: ProvisioningRequestCreated + status: "False" + type: Ready + state: Provisioning +--- +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + generateName: test-cap-01-provider- + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/tenant-operation-type: provisioning + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + name: test-cap-01-provider-s6f4l + namespace: default + finalizers: + - sme.sap.com/mtxrequest + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + capApplicationVersionInstance: test-cap-01-cav-v1 + subDomain: my-provider + tenantId: tenant-id-for-provider + operation: provisioning + steps: + - name: mtx + type: CAPTenantOperation +status: + conditions: + - message: "invalid env configuration for workload: mtx job, remove entry: VCAP_SERVICES from configuration" + reason: InvalidEnv + status: "True" + type: Ready + state: Failed diff --git a/internal/controller/testdata/captenant/cat-27.expected.yaml b/internal/controller/testdata/captenant/cat-27.expected.yaml new file mode 100644 index 0000000..9d9acc4 --- /dev/null +++ b/internal/controller/testdata/captenant/cat-27.expected.yaml @@ -0,0 +1,67 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + finalizers: + - sme.sap.com/captenant + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-provider + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: provider + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-provider + tenantId: tenant-id-for-provider + version: 8.9.10 + versionUpgradeStrategy: always +status: + conditions: + - message: "waiting for CAPTenantOperation default.test-cap-01-provider-gen of type upgrade to complete" + reason: UpgradeOperationCreated + status: "True" + type: Ready + state: Upgrading + currentCAPApplicationVersionInstance: test-cap-01-cav-v1 +--- +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + generateName: test-cap-01-provider- + name: test-cap-01-provider-gen + namespace: default + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/cav-version: "8.9.10" + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + capApplicationVersionInstance: test-cap-01-cav-v2 + subDomain: my-provider + tenantId: tenant-id-for-provider + operation: upgrade + steps: + - name: cap-backend + type: TenantOperation diff --git a/internal/controller/testdata/captenant/cat-28.expected.yaml b/internal/controller/testdata/captenant/cat-28.expected.yaml new file mode 100644 index 0000000..2709cf5 --- /dev/null +++ b/internal/controller/testdata/captenant/cat-28.expected.yaml @@ -0,0 +1,91 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + finalizers: + - sme.sap.com/captenant + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-consumer + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: consumer + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + name: test-cap-01-consumer + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-consumer + tenantId: tenant-id-for-consumer + version: 8.9.10 + versionUpgradeStrategy: always +status: + observedGeneration: 0 + state: Provisioning + conditions: + - type: Ready + status: "False" + observedGeneration: 0 + reason: ProvisioningOperationCreated + message: "waiting for CAPTenantOperation default.test-cap-01-consumer-gen of type provisioning to complete" +--- +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + generateName: test-cap-01-consumer- + name: test-cap-01-consumer-gen + namespace: default + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-consumer + labels: + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: a9df2080f99fd77b1b2c7e4cee1e1bff69498511 + sme.sap.com/tenant-operation-type: provisioning + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-consumer +spec: + capApplicationVersionInstance: test-cap-01-cav-v2 + subDomain: my-consumer + tenantId: tenant-id-for-consumer + operation: provisioning + steps: + - name: custom-say + type: CustomTenantOperation + - name: ten-op + type: TenantOperation +--- +apiVersion: dns.gardener.cloud/v1alpha1 +kind: DNSEntry +metadata: + annotations: + dns.gardener.cloud/class: garden + sme.sap.com/resource-hash: c38b79c0bac0cf1b5b8e6757b31d892898fe18472338996f7103adbf0249c79e + sme.sap.com/owner-identifier: CAPTenant.default.test-cap-01-consumer + labels: + sme.sap.com/owner-identifier-hash: f57a7105fbe1d899a7757ddd04747b6b33dde594 + name: test-cap-01-consumer0 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-consumer +spec: + dnsName: my-consumer.foo.bar.local + targets: + - public-ingress.operator.testing.local diff --git a/internal/controller/testdata/captenant/cat-28.initial.yaml b/internal/controller/testdata/captenant/cat-28.initial.yaml new file mode 100644 index 0000000..8b061af --- /dev/null +++ b/internal/controller/testdata/captenant/cat-28.initial.yaml @@ -0,0 +1,36 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + finalizers: + - sme.sap.com/captenant + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-consumer + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: consumer + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + name: test-cap-01-consumer + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-consumer + tenantId: tenant-id-for-consumer + version: 8.9.10 + versionUpgradeStrategy: always +status: + state: "" + conditions: + - type: Ready + status: "False" + reason: "ProcessingStarted" + message: "" diff --git a/internal/controller/testdata/captenant/cat-29.expected.yaml b/internal/controller/testdata/captenant/cat-29.expected.yaml new file mode 100644 index 0000000..5575931 --- /dev/null +++ b/internal/controller/testdata/captenant/cat-29.expected.yaml @@ -0,0 +1,44 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + generation: 2 + finalizers: + - sme.sap.com/captenant + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-provider + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: provider + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-provider + tenantId: tenant-id-for-provider + version: 8.9.10 + versionUpgradeStrategy: always +status: + conditions: + - message: "CAPTenantOperation default.test-cap-01-provider-upg successfully completed" + reason: UpgradeCompleted + observedGeneration: 2 + status: "True" + type: Ready + state: Ready + currentCAPApplicationVersionInstance: test-cap-01-cav-v2 + previousCAPApplicationVersions: + - old-version-2 + - old-version-3 + - test-cap-01-cav-v1 + observedGeneration: 2 diff --git a/internal/controller/testdata/captenant/cat-29.initial.yaml b/internal/controller/testdata/captenant/cat-29.initial.yaml new file mode 100644 index 0000000..0c2b8af --- /dev/null +++ b/internal/controller/testdata/captenant/cat-29.initial.yaml @@ -0,0 +1,79 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + generation: 2 + finalizers: + - sme.sap.com/captenant + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-provider + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: provider + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-provider + tenantId: tenant-id-for-provider + version: 8.9.10 + versionUpgradeStrategy: always +status: + conditions: + - message: "waiting for CAPTenantOperation default.test-cap-01-provider-upg of type upgrade to complete" + reason: UpgradeOperationCreated + status: "False" + type: Ready + state: Upgrading + currentCAPApplicationVersionInstance: test-cap-01-cav-v1 + previousCAPApplicationVersions: + - old-version-1 + - old-version-2 + - old-version-3 +--- +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + generateName: test-cap-01-provider- + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/cav-version: "8.9.10" + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + name: test-cap-01-provider-upg + namespace: default + finalizers: + - sme.sap.com/captenantoperation + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + capApplicationVersionInstance: test-cap-01-cav-v2 + subDomain: my-provider + tenantId: tenant-id-for-provider + operation: upgrade + steps: + - name: mtx + type: TenantOperation +status: + conditions: + - message: job default.test-cap-01-provider-upg-abcd completed + reason: StepCompleted + status: "True" + type: Ready + state: Completed diff --git a/internal/controller/testdata/captenant/cat-with-no-version.yaml b/internal/controller/testdata/captenant/cat-with-no-version.yaml new file mode 100644 index 0000000..dc21039 --- /dev/null +++ b/internal/controller/testdata/captenant/cat-with-no-version.yaml @@ -0,0 +1,35 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + finalizers: + - sme.sap.com/captenant + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-consumer + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: consumer + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + name: test-cap-01-consumer + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-consumer + tenantId: tenant-id-for-consumer + versionUpgradeStrategy: always +status: + state: "" + conditions: + - type: Ready + status: "False" + reason: "ProcessingStarted" + message: "" diff --git a/internal/controller/testdata/captenant/changed-provider-tenant-dnsentry.yaml b/internal/controller/testdata/captenant/changed-provider-tenant-dnsentry.yaml new file mode 100644 index 0000000..f4922ae --- /dev/null +++ b/internal/controller/testdata/captenant/changed-provider-tenant-dnsentry.yaml @@ -0,0 +1,27 @@ +apiVersion: dns.gardener.cloud/v1alpha1 +kind: DNSEntry +metadata: + annotations: + dns.gardener.cloud/class: garden + sme.sap.com/resource-hash: be44dd98e914aa033f04f18a03338da45b40090b55e0e1c935353f088bd7c583 + sme.sap.com/owner-identifier: CAPTenant.default.test-cap-01-provider + labels: + sme.sap.com/owner-identifier-hash: ec24f9b09337c244cf5ac64b539f8c04f507cd99 + name: test-cap-01-provider0 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + dnsName: my-provider.foo.bar.local + targets: + - public-ingress.operator.testing.local +status: + message: dns entry active + state: Ready + targets: + - public-ingress.operator.testing.local + ttl: 300 diff --git a/internal/controller/testdata/captenant/provider-tenant-dnsentry-not-ready.yaml b/internal/controller/testdata/captenant/provider-tenant-dnsentry-not-ready.yaml new file mode 100644 index 0000000..d9f7ce5 --- /dev/null +++ b/internal/controller/testdata/captenant/provider-tenant-dnsentry-not-ready.yaml @@ -0,0 +1,27 @@ +apiVersion: dns.gardener.cloud/v1alpha1 +kind: DNSEntry +metadata: + annotations: + dns.gardener.cloud/class: garden + sme.sap.com/resource-hash: 8a2ffda0d9efb285c9653316b443d8f6a40246532eb5aee009e65a8a8389e8fe + sme.sap.com/owner-identifier: CAPTenant.default.test-cap-01-provider + labels: + sme.sap.com/owner-identifier-hash: ec24f9b09337c244cf5ac64b539f8c04f507cd99 + name: test-cap-01-provider0 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + dnsName: my-provider.foo.bar.local + targets: + - public-ingress.operator.testing.local +status: + message: processing + state: Processing + targets: + - public-ingress.operator.testing.local + ttl: 300 diff --git a/internal/controller/testdata/captenant/provider-tenant-dnsentry.yaml b/internal/controller/testdata/captenant/provider-tenant-dnsentry.yaml new file mode 100644 index 0000000..9a95c31 --- /dev/null +++ b/internal/controller/testdata/captenant/provider-tenant-dnsentry.yaml @@ -0,0 +1,27 @@ +apiVersion: dns.gardener.cloud/v1alpha1 +kind: DNSEntry +metadata: + annotations: + dns.gardener.cloud/class: garden + sme.sap.com/resource-hash: 8a2ffda0d9efb285c9653316b443d8f6a40246532eb5aee009e65a8a8389e8fe + sme.sap.com/owner-identifier: CAPTenant.default.test-cap-01-provider + labels: + sme.sap.com/owner-identifier-hash: ec24f9b09337c244cf5ac64b539f8c04f507cd99 + name: test-cap-01-provider0 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + dnsName: my-provider.foo.bar.local + targets: + - public-ingress.operator.testing.local +status: + message: dns entry active + state: Ready + targets: + - public-ingress.operator.testing.local + ttl: 300 diff --git a/internal/controller/testdata/captenant/provider-tenant-dr-v1.yaml b/internal/controller/testdata/captenant/provider-tenant-dr-v1.yaml new file mode 100644 index 0000000..06d0700 --- /dev/null +++ b/internal/controller/testdata/captenant/provider-tenant-dr-v1.yaml @@ -0,0 +1,27 @@ +apiVersion: networking.istio.io/v1beta1 +kind: DestinationRule +metadata: + annotations: + sme.sap.com/resource-hash: 5d065e2112f26ad9b5ace902461365ba9cbf539123dea326f060534bc30e22d1 + sme.sap.com/owner-identifier: default.test-cap-01-provider + generation: 1 + labels: + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + host: test-cap-01-cav-v1-app-router-svc.default.svc.cluster.local + trafficPolicy: + loadBalancer: + consistentHash: + httpCookie: + name: JSESSIONID + path: / + ttl: 0s diff --git a/internal/controller/testdata/captenant/provider-tenant-vs-v1.yaml b/internal/controller/testdata/captenant/provider-tenant-vs-v1.yaml new file mode 100644 index 0000000..a218117 --- /dev/null +++ b/internal/controller/testdata/captenant/provider-tenant-vs-v1.yaml @@ -0,0 +1,34 @@ +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + annotations: + sme.sap.com/resource-hash: "2309c27d407c5e1d71529d3448da227ab25f63fb5f9d631d7c8a3e9fd0a1ff34" + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + gateways: + - test-cap-01-gw + - istio-system/cap-operator-domains-gen + hosts: + - my-provider.app-domain.test.local + - my-provider.foo.bar.local + http: + - match: + - uri: + prefix: / + route: + - destination: + host: test-cap-01-cav-v1-app-router-svc.default.svc.cluster.local + port: + number: 5000 + weight: 100 diff --git a/internal/controller/testdata/captenant/to-be-updated-provider-tenant-dnsentry.yaml b/internal/controller/testdata/captenant/to-be-updated-provider-tenant-dnsentry.yaml new file mode 100644 index 0000000..1598adf --- /dev/null +++ b/internal/controller/testdata/captenant/to-be-updated-provider-tenant-dnsentry.yaml @@ -0,0 +1,27 @@ +apiVersion: dns.gardener.cloud/v1alpha1 +kind: DNSEntry +metadata: + annotations: + dns.gardener.cloud/class: garden + sme.sap.com/resource-hash: foobarsomehash + sme.sap.com/owner-identifier: CAPTenant.default.test-cap-01-provider + labels: + sme.sap.com/owner-identifier-hash: ec24f9b09337c244cf5ac64b539f8c04f507cd99 + name: test-cap-01-provider0 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + dnsName: my-provider.foo.bar.local + targets: + - public-ingress.operator.testing.local +status: + message: dns entry active + state: Ready + targets: + - public-ingress.operator.testing.local + ttl: 300 diff --git a/internal/controller/testdata/captenantoperation/ctop-01.expected.yaml b/internal/controller/testdata/captenantoperation/ctop-01.expected.yaml new file mode 100644 index 0000000..beae87e --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-01.expected.yaml @@ -0,0 +1,30 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-provider-abcd + namespace: default + generation: 1 + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/tenant-operation-type: provisioning + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + tenantId: tenant-id-for-provider + operation: provisioning + capApplicationVersionInstance: test-cap-01-cav-v1 + steps: + - name: mtx + type: TenantOperation +status: + state: Processing + conditions: [] diff --git a/internal/controller/testdata/captenantoperation/ctop-01.initial.yaml b/internal/controller/testdata/captenantoperation/ctop-01.initial.yaml new file mode 100644 index 0000000..99008d2 --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-01.initial.yaml @@ -0,0 +1,13 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-provider-abcd + namespace: default + generation: 1 +spec: + tenantId: tenant-id-for-provider + operation: provisioning + capApplicationVersionInstance: test-cap-01-cav-v1 + steps: + - name: mtx + type: TenantOperation diff --git a/internal/controller/testdata/captenantoperation/ctop-02.expected.yaml b/internal/controller/testdata/captenantoperation/ctop-02.expected.yaml new file mode 100644 index 0000000..c34b850 --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-02.expected.yaml @@ -0,0 +1,31 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-provider-abcd + namespace: default + generation: 1 + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/tenant-operation-type: provisioning + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + tenantId: tenant-id-for-provider + operation: provisioning + capApplicationVersionInstance: test-cap-01-cav-v1 + steps: + - name: mtx + type: TenantOperation +status: + state: Processing + conditions: [] + currentStep: 1 diff --git a/internal/controller/testdata/captenantoperation/ctop-02.initial.yaml b/internal/controller/testdata/captenantoperation/ctop-02.initial.yaml new file mode 100644 index 0000000..beae87e --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-02.initial.yaml @@ -0,0 +1,30 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-provider-abcd + namespace: default + generation: 1 + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/tenant-operation-type: provisioning + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + tenantId: tenant-id-for-provider + operation: provisioning + capApplicationVersionInstance: test-cap-01-cav-v1 + steps: + - name: mtx + type: TenantOperation +status: + state: Processing + conditions: [] diff --git a/internal/controller/testdata/captenantoperation/ctop-03.expected.yaml b/internal/controller/testdata/captenantoperation/ctop-03.expected.yaml new file mode 100644 index 0000000..54459fb --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-03.expected.yaml @@ -0,0 +1,34 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-provider-abcd + namespace: default + generation: 1 + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/tenant-operation-type: provisioning + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + tenantId: tenant-id-for-provider + operation: provisioning + capApplicationVersionInstance: test-cap-01-cav-v1 + steps: [] +status: + state: Failed + observedGeneration: 1 + conditions: + - type: Ready + status: "True" + observedGeneration: 1 + reason: StepProcessingError + message: "operation steps missing in CAPTenantOperation default.test-cap-01-provider-abcd" diff --git a/internal/controller/testdata/captenantoperation/ctop-03.initial.yaml b/internal/controller/testdata/captenantoperation/ctop-03.initial.yaml new file mode 100644 index 0000000..87a5507 --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-03.initial.yaml @@ -0,0 +1,28 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-provider-abcd + namespace: default + generation: 1 + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/tenant-operation-type: provisioning + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + tenantId: tenant-id-for-provider + operation: provisioning + capApplicationVersionInstance: test-cap-01-cav-v1 + steps: [] +status: + state: Processing + conditions: [] diff --git a/internal/controller/testdata/captenantoperation/ctop-04.expected.yaml b/internal/controller/testdata/captenantoperation/ctop-04.expected.yaml new file mode 100644 index 0000000..ba87a03 --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-04.expected.yaml @@ -0,0 +1,129 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-provider-abcd + namespace: default + generation: 1 + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/tenant-operation-type: provisioning + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + tenantId: tenant-id-for-provider + subDomain: my-provider + operation: provisioning + capApplicationVersionInstance: test-cap-01-cav-v1 + steps: + - name: mtx + type: TenantOperation +status: + state: Processing + observedGeneration: 1 + conditions: + - type: Ready + status: "False" + observedGeneration: 1 + reason: StepInitiated + message: "step 1/1 : job default.test-cap-01-provider-mtx-gen created" + currentStep: 1 + activeJob: "test-cap-01-provider-mtx-gen" +--- +apiVersion: batch/v1 +kind: Job +metadata: + annotations: + sidecar.istio.io/inject: "false" + sme.sap.com/owner-identifier: default.test-cap-01-provider-abcd + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/tenant-operation-step: "1" + sme.sap.com/tenant-operation-type: provisioning + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: ce6bb3ae0b5ebbd0116259415ccac00bff0dc431 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + app: test-cap-01 + sme.sap.com/category: "Workload" + sme.sap.com/workload-name: "mtx" + sme.sap.com/workload-type: "TenantOperation" + generateName: test-cap-01-provider-mtx- + name: test-cap-01-provider-mtx-gen + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenantOperation + name: test-cap-01-provider-abcd +spec: + template: + metadata: + annotations: + sidecar.istio.io/inject: "false" + sme.sap.com/owner-identifier: default.test-cap-01-provider-abcd + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/tenant-operation-step: "1" + sme.sap.com/tenant-operation-type: provisioning + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: ce6bb3ae0b5ebbd0116259415ccac00bff0dc431 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + app: test-cap-01 + sme.sap.com/category: "Workload" + sme.sap.com/workload-name: "mtx" + sme.sap.com/workload-type: "TenantOperation" + spec: + containers: + - env: + - name: WAIT_FOR_SIDECAR + value: "false" + - name: XSUAA_INSTANCE_NAME + value: cap-uaa + - name: MTX_SERVICE_URL + value: http://localhost:4004 + - name: MTX_REQUEST_TYPE + value: provisioning + - name: MTX_TENANT_ID + value: tenant-id-for-provider + - name: MTX_REQUEST_PAYLOAD + value: '{"subscribedSubdomain":"my-provider","eventType":"CREATE"}' + envFrom: + - secretRef: + name: test-cap-01-provider-abcd-mtx-gen + optional: true + image: ghcr.io/sap/cap-operator/mtx-job + name: trigger + - env: + - name: CAPOP_APP_VERSION + value: 5.6.7 + - name: CAPOP_TENANT_ID + value: tenant-id-for-provider + - name: CAPOP_TENANT_OPERATION + value: provisioning + - name: CAPOP_TENANT_SUBDOMAIN + value: my-provider + - name: "IS_MTXS_ENABLED" + value: "false" + envFrom: + - secretRef: + name: test-cap-01-provider-abcd-mtx-gen + optional: true + command: + - "/bin/sh" + - "-c" + args: + - node ./node_modules/@sap/cds/bin/cds run & nc -lv -s localhost -p 8080 + image: docker.image.repo/srv/server:latest + name: mtx + imagePullSecrets: + - name: regcred + restartPolicy: Never diff --git a/internal/controller/testdata/captenantoperation/ctop-04.initial.yaml b/internal/controller/testdata/captenantoperation/ctop-04.initial.yaml new file mode 100644 index 0000000..8086d46 --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-04.initial.yaml @@ -0,0 +1,32 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-provider-abcd + namespace: default + generation: 1 + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/tenant-operation-type: provisioning + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + tenantId: tenant-id-for-provider + subDomain: my-provider + operation: provisioning + capApplicationVersionInstance: test-cap-01-cav-v1 + steps: + - name: mtx + type: TenantOperation +status: + state: Processing + conditions: [] + currentStep: 1 diff --git a/internal/controller/testdata/captenantoperation/ctop-05.expected.yaml b/internal/controller/testdata/captenantoperation/ctop-05.expected.yaml new file mode 100644 index 0000000..0934e02 --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-05.expected.yaml @@ -0,0 +1,31 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-provider-abcd + namespace: default + generation: 1 + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + sme.sap.com/cav-version: "5.6.7" + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + tenantId: tenant-id-for-provider + operation: upgrade + capApplicationVersionInstance: test-cap-01-cav-v1 + steps: + - name: mtx + type: TenantOperation +status: + state: Processing + conditions: [] diff --git a/internal/controller/testdata/captenantoperation/ctop-05.initial.yaml b/internal/controller/testdata/captenantoperation/ctop-05.initial.yaml new file mode 100644 index 0000000..39895fc --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-05.initial.yaml @@ -0,0 +1,13 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-provider-abcd + namespace: default + generation: 1 +spec: + tenantId: tenant-id-for-provider + operation: upgrade + capApplicationVersionInstance: test-cap-01-cav-v1 + steps: + - name: mtx + type: TenantOperation diff --git a/internal/controller/testdata/captenantoperation/ctop-06.expected.yaml b/internal/controller/testdata/captenantoperation/ctop-06.expected.yaml new file mode 100644 index 0000000..603b264 --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-06.expected.yaml @@ -0,0 +1,134 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-provider-abcd + namespace: default + generation: 1 + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + sme.sap.com/cav-version: 8.9.10 + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + tenantId: tenant-id-for-provider + subDomain: my-provider + operation: upgrade + capApplicationVersionInstance: test-cap-01-cav-v2 + steps: + - name: cap-backend + type: TenantOperation +status: + state: Processing + observedGeneration: 1 + conditions: + - type: Ready + status: "False" + observedGeneration: 1 + reason: StepInitiated + message: "step 1/1 : job default.test-cap-01-provider-cap-backend-gen created" + currentStep: 1 + activeJob: "test-cap-01-provider-cap-backend-gen" +--- +apiVersion: batch/v1 +kind: Job +metadata: + annotations: + sidecar.istio.io/inject: "false" + sme.sap.com/owner-identifier: default.test-cap-01-provider-abcd + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/tenant-operation-step: "1" + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: ce6bb3ae0b5ebbd0116259415ccac00bff0dc431 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + app: test-cap-01 + sme.sap.com/category: "Workload" + sme.sap.com/workload-name: "cap-backend" + sme.sap.com/workload-type: "TenantOperation" + generateName: test-cap-01-provider-cap-backend- + name: test-cap-01-provider-cap-backend-gen + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenantOperation + name: test-cap-01-provider-abcd +spec: + backoffLimit: 2 + ttlSecondsAfterFinished: 300 + template: + metadata: + annotations: + sidecar.istio.io/inject: "false" + sme.sap.com/owner-identifier: default.test-cap-01-provider-abcd + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/tenant-operation-step: "1" + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: ce6bb3ae0b5ebbd0116259415ccac00bff0dc431 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + app: test-cap-01 + sme.sap.com/category: "Workload" + sme.sap.com/workload-name: "cap-backend" + sme.sap.com/workload-type: "TenantOperation" + spec: + containers: + - env: + - name: WAIT_FOR_SIDECAR + value: "false" + - name: XSUAA_INSTANCE_NAME + value: cap-uaa + - name: MTX_SERVICE_URL + value: http://localhost:4004 + - name: MTX_REQUEST_TYPE + value: upgrade + - name: MTX_TENANT_ID + value: tenant-id-for-provider + - name: MTX_REQUEST_PAYLOAD + value: '{"tenants":["tenant-id-for-provider"],"autoUndeploy":true}' + envFrom: + - secretRef: + name: test-cap-01-provider-abcd-cap-backend-gen + optional: true + image: ghcr.io/sap/cap-operator/mtx-job + name: trigger + - env: + - name: CAPOP_APP_VERSION + value: 8.9.10 + - name: CAPOP_TENANT_ID + value: tenant-id-for-provider + - name: CAPOP_TENANT_OPERATION + value: upgrade + - name: CAPOP_TENANT_SUBDOMAIN + value: my-provider + - name: foo + value: bar + - name: "IS_MTXS_ENABLED" + value: "false" + envFrom: + - secretRef: + name: test-cap-01-provider-abcd-cap-backend-gen + optional: true + command: + - "/bin/sh" + - "-c" + args: + - node ./node_modules/@sap/cds/bin/cds run & nc -lv -s localhost -p 8080 + image: docker.image.repo/srv/server:v2 + name: cap-backend + imagePullSecrets: + - name: regcred + restartPolicy: Never diff --git a/internal/controller/testdata/captenantoperation/ctop-06.initial.yaml b/internal/controller/testdata/captenantoperation/ctop-06.initial.yaml new file mode 100644 index 0000000..521e8f1 --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-06.initial.yaml @@ -0,0 +1,33 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-provider-abcd + namespace: default + generation: 1 + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: "default.test-cap-01-provider" + labels: + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + sme.sap.com/cav-version: "8.9.10" + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + tenantId: tenant-id-for-provider + subDomain: my-provider + operation: upgrade + capApplicationVersionInstance: test-cap-01-cav-v2 + steps: + - name: cap-backend + type: TenantOperation +status: + state: Processing + conditions: [] + currentStep: 1 diff --git a/internal/controller/testdata/captenantoperation/ctop-07.expected.yaml b/internal/controller/testdata/captenantoperation/ctop-07.expected.yaml new file mode 100644 index 0000000..ecf49d7 --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-07.expected.yaml @@ -0,0 +1,114 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-provider-abcd + namespace: default + generation: 1 + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + sme.sap.com/cav-version: 8.9.10 + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + tenantId: tenant-id-for-provider + subDomain: my-provider + operation: upgrade + capApplicationVersionInstance: test-cap-01-cav-v2 + steps: + - name: custom-say + type: CustomTenantOperation + - name: ten-op + type: TenantOperation + - name: custom-say + type: CustomTenantOperation +status: + state: Processing + observedGeneration: 1 + conditions: + - type: Ready + status: "False" + observedGeneration: 1 + reason: StepInitiated + message: "step 1/3 : job default.test-cap-01-provider-custom-say-gen created" + currentStep: 1 + activeJob: "test-cap-01-provider-custom-say-gen" +--- +apiVersion: batch/v1 +kind: Job +metadata: + annotations: + sidecar.istio.io/inject: "false" + sme.sap.com/owner-identifier: default.test-cap-01-provider-abcd + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/tenant-operation-step: "1" + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: ce6bb3ae0b5ebbd0116259415ccac00bff0dc431 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + app: test-cap-01 + sme.sap.com/category: "Workload" + sme.sap.com/workload-name: "custom-say" + sme.sap.com/workload-type: "CustomTenantOperation" + generateName: test-cap-01-provider-custom-say- + name: test-cap-01-provider-custom-say-gen + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenantOperation + name: test-cap-01-provider-abcd +spec: + backoffLimit: 1 + ttlSecondsAfterFinished: 150 + template: + metadata: + annotations: + sidecar.istio.io/inject: "false" + sme.sap.com/owner-identifier: default.test-cap-01-provider-abcd + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/tenant-operation-step: "1" + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: ce6bb3ae0b5ebbd0116259415ccac00bff0dc431 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + app: test-cap-01 + sme.sap.com/category: "Workload" + sme.sap.com/workload-name: "custom-say" + sme.sap.com/workload-type: "CustomTenantOperation" + spec: + containers: + - env: + - name: CAPOP_APP_VERSION + value: 8.9.10 + - name: CAPOP_TENANT_ID + value: tenant-id-for-provider + - name: CAPOP_TENANT_OPERATION + value: upgrade + - name: CAPOP_TENANT_SUBDOMAIN + value: my-provider + - name: close + value: encounter + envFrom: + - secretRef: + name: test-cap-01-provider-abcd-custom-say-gen + optional: true + image: docker/whalesay + imagePullPolicy: IfNotPresent + command: ["cowsay", "$(CAPOP_TENANT_OPERATION)", "$(CAPOP_TENANT_ID)"] + name: custom-say + imagePullSecrets: + - name: regcred + restartPolicy: Never diff --git a/internal/controller/testdata/captenantoperation/ctop-07.initial.yaml b/internal/controller/testdata/captenantoperation/ctop-07.initial.yaml new file mode 100644 index 0000000..d95fb1e --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-07.initial.yaml @@ -0,0 +1,37 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-provider-abcd + namespace: default + generation: 1 + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: "default.test-cap-01-provider" + labels: + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + sme.sap.com/cav-version: "8.9.10" + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + tenantId: tenant-id-for-provider + subDomain: my-provider + operation: upgrade + capApplicationVersionInstance: test-cap-01-cav-v2 + steps: + - name: custom-say + type: CustomTenantOperation + - name: ten-op + type: TenantOperation + - name: custom-say + type: CustomTenantOperation +status: + state: Processing + conditions: [] + currentStep: 1 diff --git a/internal/controller/testdata/captenantoperation/ctop-08.expected.yaml b/internal/controller/testdata/captenantoperation/ctop-08.expected.yaml new file mode 100644 index 0000000..8f7bfad --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-08.expected.yaml @@ -0,0 +1,119 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-provider-abcd + namespace: default + generation: 1 + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + sme.sap.com/cav-version: 8.9.10 + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + tenantId: tenant-id-for-provider + subDomain: my-provider + operation: upgrade + capApplicationVersionInstance: test-cap-01-cav-v2 + steps: + - name: custom-say + type: CustomTenantOperation + - name: ten-op + type: TenantOperation + - name: custom-say + type: CustomTenantOperation +status: + state: Processing + observedGeneration: 1 + conditions: + - type: Ready + status: "False" + observedGeneration: 1 + reason: StepCompleted + message: "step 1/3 : job default.test-cap-01-provider-custom-say-xyz completed" + currentStep: 2 +--- +apiVersion: batch/v1 +kind: Job +metadata: + annotations: + sidecar.istio.io/inject: "false" + sme.sap.com/owner-identifier: default.test-cap-01-provider-abcd + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/tenant-operation-step: "1" + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: ce6bb3ae0b5ebbd0116259415ccac00bff0dc431 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + app: test-cap-01 + sme.sap.com/category: "Workload" + sme.sap.com/workload-name: "custom-say" + sme.sap.com/workload-type: "CustomTenantOperation" + generateName: test-cap-01-provider-custom-say- + name: test-cap-01-provider-custom-say-xyz + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenantOperation + name: test-cap-01-provider-abcd +spec: + backoffLimit: 1 + ttlSecondsAfterFinished: 150 + template: + metadata: + annotations: + sidecar.istio.io/inject: "false" + sme.sap.com/owner-identifier: default.test-cap-01-provider-abcd + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/tenant-operation-step: "1" + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: ce6bb3ae0b5ebbd0116259415ccac00bff0dc431 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + app: test-cap-01 + sme.sap.com/category: "Workload" + sme.sap.com/workload-name: "custom-say" + sme.sap.com/workload-type: "CustomTenantOperation" + spec: + containers: + - env: + - name: CAPOP_APP_VERSION + value: 8.9.10 + - name: CAPOP_TENANT_ID + value: tenant-id-for-provider + - name: CAPOP_TENANT_OPERATION + value: upgrade + - name: CAPOP_TENANT_SUBDOMAIN + value: my-provider + - name: close + value: encounter + envFrom: + - secretRef: + name: test-cap-01-provider-abcd-custom-say-gen + optional: true + image: docker/whalesay + imagePullPolicy: IfNotPresent + command: ["cowsay", "$(CAPOP_TENANT_OPERATION)", "$(CAPOP_TENANT_ID)"] + name: custom-say + imagePullSecrets: + - name: regcred + restartPolicy: Never +status: + activeItems: 0 + conditions: + - type: Complete + status: "True" + Reason: PodCompleted diff --git a/internal/controller/testdata/captenantoperation/ctop-08.initial.yaml b/internal/controller/testdata/captenantoperation/ctop-08.initial.yaml new file mode 100644 index 0000000..d7cac20 --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-08.initial.yaml @@ -0,0 +1,119 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-provider-abcd + namespace: default + generation: 1 + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + sme.sap.com/cav-version: 8.9.10 + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + tenantId: tenant-id-for-provider + subDomain: my-provider + operation: upgrade + capApplicationVersionInstance: test-cap-01-cav-v2 + steps: + - name: custom-say + type: CustomTenantOperation + - name: ten-op + type: TenantOperation + - name: custom-say + type: CustomTenantOperation +status: + state: Processing + observedGeneration: 1 + conditions: + - type: Ready + status: "False" + reason: StepInitiated + message: "step 1/3 : job default.test-cap-01-provider-custom-say-xyz created" + currentStep: 1 + activeJob: "test-cap-01-provider-custom-say-xyz" +--- +apiVersion: batch/v1 +kind: Job +metadata: + annotations: + sidecar.istio.io/inject: "false" + sme.sap.com/owner-identifier: default.test-cap-01-provider-abcd + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/tenant-operation-step: "1" + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: ce6bb3ae0b5ebbd0116259415ccac00bff0dc431 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + app: test-cap-01 + sme.sap.com/category: "Workload" + sme.sap.com/workload-name: "custom-say" + sme.sap.com/workload-type: "CustomTenantOperation" + generateName: test-cap-01-provider-custom-say- + name: test-cap-01-provider-custom-say-xyz + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenantOperation + name: test-cap-01-provider-abcd +spec: + backoffLimit: 1 + ttlSecondsAfterFinished: 150 + template: + metadata: + annotations: + sidecar.istio.io/inject: "false" + sme.sap.com/owner-identifier: default.test-cap-01-provider-abcd + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/tenant-operation-step: "1" + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: ce6bb3ae0b5ebbd0116259415ccac00bff0dc431 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + app: test-cap-01 + sme.sap.com/category: "Workload" + sme.sap.com/workload-name: "custom-say" + sme.sap.com/workload-type: "CustomTenantOperation" + spec: + containers: + - env: + - name: CAPOP_APP_VERSION + value: 8.9.10 + - name: CAPOP_TENANT_ID + value: tenant-id-for-provider + - name: CAPOP_TENANT_OPERATION + value: upgrade + - name: CAPOP_TENANT_SUBDOMAIN + value: my-provider + - name: close + value: encounter + envFrom: + - secretRef: + name: test-cap-01-provider-abcd-custom-say-gen + optional: true + image: docker/whalesay + imagePullPolicy: IfNotPresent + command: ["cowsay", "$(CAPOP_TENANT_OPERATION)", "$(CAPOP_TENANT_ID)"] + name: custom-say + imagePullSecrets: + - name: regcred + restartPolicy: Never +status: + activeItems: 0 + conditions: + - type: Complete + status: "True" + Reason: PodCompleted diff --git a/internal/controller/testdata/captenantoperation/ctop-09.expected.yaml b/internal/controller/testdata/captenantoperation/ctop-09.expected.yaml new file mode 100644 index 0000000..e979581 --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-09.expected.yaml @@ -0,0 +1,138 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-provider-abcd + namespace: default + generation: 1 + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + sme.sap.com/cav-version: 8.9.10 + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + tenantId: tenant-id-for-provider + subDomain: my-provider + operation: upgrade + capApplicationVersionInstance: test-cap-01-cav-v2 + steps: + - name: custom-say + type: CustomTenantOperation + - name: ten-op + type: TenantOperation + - name: custom-say + type: CustomTenantOperation +status: + state: Processing + observedGeneration: 1 + conditions: + - type: Ready + status: "False" + observedGeneration: 1 + reason: StepInitiated + message: "step 2/3 : job default.test-cap-01-provider-ten-op-gen created" + currentStep: 2 + activeJob: "test-cap-01-provider-ten-op-gen" +--- +apiVersion: batch/v1 +kind: Job +metadata: + annotations: + sidecar.istio.io/inject: "false" + sme.sap.com/owner-identifier: default.test-cap-01-provider-abcd + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/tenant-operation-step: "2" + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: ce6bb3ae0b5ebbd0116259415ccac00bff0dc431 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + app: test-cap-01 + sme.sap.com/category: "Workload" + sme.sap.com/workload-name: "ten-op" + sme.sap.com/workload-type: "TenantOperation" + generateName: test-cap-01-provider-ten-op- + name: test-cap-01-provider-ten-op-gen + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenantOperation + name: test-cap-01-provider-abcd +spec: + backoffLimit: 1 + ttlSecondsAfterFinished: 150 + template: + metadata: + annotations: + sidecar.istio.io/inject: "false" + sme.sap.com/owner-identifier: default.test-cap-01-provider-abcd + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/tenant-operation-step: "2" + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: ce6bb3ae0b5ebbd0116259415ccac00bff0dc431 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + app: test-cap-01 + sme.sap.com/category: "Workload" + sme.sap.com/workload-name: "ten-op" + sme.sap.com/workload-type: "TenantOperation" + spec: + containers: + - env: + - name: WAIT_FOR_SIDECAR + value: "false" + - name: XSUAA_INSTANCE_NAME + value: cap-uaa + - name: MTX_SERVICE_URL + value: http://localhost:4004 + - name: MTX_REQUEST_TYPE + value: upgrade + - name: MTX_TENANT_ID + value: tenant-id-for-provider + - name: MTX_REQUEST_PAYLOAD + value: '{"tenants":["tenant-id-for-provider"],"autoUndeploy":true}' + envFrom: + - secretRef: + name: test-cap-01-provider-abcd-ten-op-gen + optional: true + image: ghcr.io/sap/cap-operator/mtx-job + name: trigger + - env: + - name: CAPOP_APP_VERSION + value: 8.9.10 + - name: CAPOP_TENANT_ID + value: tenant-id-for-provider + - name: CAPOP_TENANT_OPERATION + value: upgrade + - name: CAPOP_TENANT_SUBDOMAIN + value: my-provider + - name: flow + value: glow + - name: "IS_MTXS_ENABLED" + value: "false" + envFrom: + - secretRef: + name: test-cap-01-provider-abcd-ten-op-gen + optional: true + command: + - "/bin/sh" + - "-c" + args: + - node ./node_modules/@sap/cds/bin/cds run & nc -lv -s localhost -p 8080 + image: docker.image.repo/srv/server:latest + name: ten-op + imagePullSecrets: + - name: regcred + restartPolicy: Never diff --git a/internal/controller/testdata/captenantoperation/ctop-09.initial.yaml b/internal/controller/testdata/captenantoperation/ctop-09.initial.yaml new file mode 100644 index 0000000..7f28d6c --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-09.initial.yaml @@ -0,0 +1,42 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-provider-abcd + namespace: default + generation: 1 + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + sme.sap.com/cav-version: 8.9.10 + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + tenantId: tenant-id-for-provider + subDomain: my-provider + operation: upgrade + capApplicationVersionInstance: test-cap-01-cav-v2 + steps: + - name: custom-say + type: CustomTenantOperation + - name: ten-op + type: TenantOperation + - name: custom-say + type: CustomTenantOperation +status: + state: Processing + observedGeneration: 1 + conditions: + - type: Ready + status: "False" + reason: StepCompleted + message: "step 1/3 : job default.test-cap-01-provider-abcd-xyz completed" + currentStep: 2 diff --git a/internal/controller/testdata/captenantoperation/ctop-10.expected.yaml b/internal/controller/testdata/captenantoperation/ctop-10.expected.yaml new file mode 100644 index 0000000..90eafdc --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-10.expected.yaml @@ -0,0 +1,42 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-provider-abcd + namespace: default + generation: 1 + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + sme.sap.com/cav-version: 8.9.10 + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + tenantId: tenant-id-for-provider + subDomain: my-provider + operation: upgrade + capApplicationVersionInstance: test-cap-01-cav-v2 + steps: + - name: custom-say + type: CustomTenantOperation + - name: ten-op + type: TenantOperation + - name: custom-say + type: CustomTenantOperation +status: + state: Failed + observedGeneration: 1 + conditions: + - type: Ready + status: "True" + observedGeneration: 1 + reason: StepFailed + message: "step 2/3 : job default.test-cap-01-provider-mtx-xyz failed" diff --git a/internal/controller/testdata/captenantoperation/ctop-10.initial.yaml b/internal/controller/testdata/captenantoperation/ctop-10.initial.yaml new file mode 100644 index 0000000..1092b5e --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-10.initial.yaml @@ -0,0 +1,141 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-provider-abcd + namespace: default + generation: 1 + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + sme.sap.com/cav-version: 8.9.10 + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + tenantId: tenant-id-for-provider + subDomain: my-provider + operation: upgrade + capApplicationVersionInstance: test-cap-01-cav-v2 + steps: + - name: custom-say + type: CustomTenantOperation + - name: ten-op + type: TenantOperation + - name: custom-say + type: CustomTenantOperation +status: + state: Processing + observedGeneration: 1 + conditions: + - type: Ready + status: "False" + reason: StepInitiated + message: "step 2/3 : job default.test-cap-01-provider-mtx-xyz created" + currentStep: 2 + activeJob: "test-cap-01-provider-mtx-xyz" +--- +apiVersion: batch/v1 +kind: Job +metadata: + annotations: + sidecar.istio.io/inject: "false" + sme.sap.com/owner-identifier: default.test-cap-01-provider-abcd + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/tenant-operation-step: "2" + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: ce6bb3ae0b5ebbd0116259415ccac00bff0dc431 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + app: test-cap-01 + sme.sap.com/category: "Workload" + sme.sap.com/workload-name: "ten-op" + sme.sap.com/workload-type: "TenantOperation" + generateName: test-cap-01-provider-mtx- + name: test-cap-01-provider-mtx-xyz + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenantOperation + name: test-cap-01-provider-abcd +spec: + backoffLimit: 1 + ttlSecondsAfterFinished: 150 + template: + metadata: + annotations: + sidecar.istio.io/inject: "false" + sme.sap.com/owner-identifier: default.test-cap-01-provider-abcd + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/tenant-operation-step: "2" + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: ce6bb3ae0b5ebbd0116259415ccac00bff0dc431 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + app: test-cap-01 + sme.sap.com/category: "Workload" + sme.sap.com/workload-name: "ten-op" + sme.sap.com/workload-type: "TenantOperation" + spec: + containers: + - env: + - name: WAIT_FOR_SIDECAR + value: "false" + - name: XSUAA_INSTANCE_NAME + value: cap-uaa + - name: MTX_SERVICE_URL + value: http://localhost:4004 + - name: MTX_REQUEST_TYPE + value: upgrade + - name: MTX_TENANT_ID + value: tenant-id-for-provider + - name: MTX_REQUEST_PAYLOAD + value: '{"tenants":["tenant-id-for-provider"],"autoUndeploy":true}' + envFrom: + - secretRef: + name: test-cap-01-provider-abcd-ten-op-gen + optional: true + image: ghcr.io/sap/cap-operator/mtx-job + name: trigger + - env: + - name: CAPOP_APP_VERSION + value: 8.9.10 + - name: CAPOP_TENANT_ID + value: tenant-id-for-provider + - name: CAPOP_TENANT_OPERATION + value: upgrade + - name: CAPOP_TENANT_SUBDOMAIN + value: my-provider + - name: flow + value: glow + envFrom: + - secretRef: + name: test-cap-01-provider-abcd-ten-op-gen + optional: true + command: + - "/bin/sh" + - "-c" + args: + - node ./node_modules/@sap/cds/bin/cds run & nc -lv -s localhost -p 8080 + image: docker.image.repo/srv/server:latest + name: mtx + imagePullSecrets: + - name: regcred + restartPolicy: Never +status: + activeItems: 0 + conditions: + - type: Failed + status: "True" + Reason: PodError diff --git a/internal/controller/testdata/captenantoperation/ctop-11.expected.yaml b/internal/controller/testdata/captenantoperation/ctop-11.expected.yaml new file mode 100644 index 0000000..f3b29d2 --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-11.expected.yaml @@ -0,0 +1,42 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-provider-abcd + namespace: default + generation: 1 + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + sme.sap.com/cav-version: 8.9.10 + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + tenantId: tenant-id-for-provider + subDomain: my-provider + operation: upgrade + capApplicationVersionInstance: test-cap-01-cav-v2 + steps: + - name: custom-say + type: CustomTenantOperation + - name: ten-op + type: TenantOperation + - name: custom-say + type: CustomTenantOperation +status: + state: Completed + observedGeneration: 1 + conditions: + - type: Ready + status: "True" + observedGeneration: 1 + reason: StepCompleted + message: "step 3/3 : job default.test-cap-01-provider-custom-say-xyz completed" diff --git a/internal/controller/testdata/captenantoperation/ctop-11.initial.yaml b/internal/controller/testdata/captenantoperation/ctop-11.initial.yaml new file mode 100644 index 0000000..f55f9f2 --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-11.initial.yaml @@ -0,0 +1,118 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-provider-abcd + namespace: default + generation: 1 + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + sme.sap.com/cav-version: 8.9.10 + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + tenantId: tenant-id-for-provider + subDomain: my-provider + operation: upgrade + capApplicationVersionInstance: test-cap-01-cav-v2 + steps: + - name: custom-say + type: CustomTenantOperation + - name: ten-op + type: TenantOperation + - name: custom-say + type: CustomTenantOperation +status: + state: Processing + observedGeneration: 1 + conditions: + - type: Ready + status: "False" + reason: StepInitiated + message: "step 3/3 : job default.test-cap-01-provider-custom-say-xyz created" + currentStep: 3 + activeJob: "test-cap-01-provider-custom-say-xyz" +--- +apiVersion: batch/v1 +kind: Job +metadata: + annotations: + sidecar.istio.io/inject: "false" + sme.sap.com/owner-identifier: default.test-cap-01-provider-abcd + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/tenant-operation-step: "3" + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: ce6bb3ae0b5ebbd0116259415ccac00bff0dc431 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + app: test-cap-01 + sme.sap.com/category: "Workload" + sme.sap.com/workload-name: "custom-say" + sme.sap.com/workload-type: "CustomTenantOperation" + generateName: test-cap-01-provider-custom-say- + name: test-cap-01-provider-custom-say-xyz + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenantOperation + name: test-cap-01-provider-abcd +spec: + backoffLimit: 1 + ttlSecondsAfterFinished: 150 + template: + metadata: + annotations: + sidecar.istio.io/inject: "false" + sme.sap.com/owner-identifier: default.test-cap-01-provider-abcd + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/tenant-operation-step: "3" + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: ce6bb3ae0b5ebbd0116259415ccac00bff0dc431 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + app: test-cap-01 + sme.sap.com/category: "Workload" + sme.sap.com/workload-name: "custom-say" + sme.sap.com/workload-type: "CustomTenantOperation" + spec: + containers: + - env: + - name: CAPOP_APP_VERSION + value: 8.9.10 + - name: CAPOP_TENANT_ID + value: tenant-id-for-provider + - name: CAPOP_TENANT_OPERATION + value: upgrade + - name: CAPOP_TENANT_SUBDOMAIN + value: my-provider + - name: close + value: encounter + envFrom: + - secretRef: + name: test-cap-01-provider-abcd-custom-say-gen + optional: true + image: docker/whalesay + command: ["cowsay", "$(CAPOP_TENANT_OPERATION)", "$(CAPOP_TENANT_ID)"] + name: custom-say + imagePullSecrets: + - name: regcred + restartPolicy: Never +status: + activeItems: 0 + conditions: + - type: Complete + status: "True" + Reason: PodCompleted diff --git a/internal/controller/testdata/captenantoperation/ctop-12.expected.yaml b/internal/controller/testdata/captenantoperation/ctop-12.expected.yaml new file mode 100644 index 0000000..716fa42 --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-12.expected.yaml @@ -0,0 +1,44 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-provider-abcd + namespace: default + deletionTimestamp: "2022-08-22T13:24:38Z" + generation: 1 + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + sme.sap.com/cav-version: 8.9.10 + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + tenantId: tenant-id-for-provider + subDomain: my-provider + operation: upgrade + capApplicationVersionInstance: test-cap-01-cav-v2 + steps: + - name: custom-say + type: CustomTenantOperation + - name: ten-op + type: TenantOperation + - name: custom-say + type: CustomTenantOperation +status: + state: Deleting + observedGeneration: 1 + conditions: + - type: Ready + status: "False" + observedGeneration: 1 + reason: StepCompleted + message: "step 1/3 : job default.test-cap-01-provider-custom-say-xyz completed" + currentStep: 2 diff --git a/internal/controller/testdata/captenantoperation/ctop-12.initial.yaml b/internal/controller/testdata/captenantoperation/ctop-12.initial.yaml new file mode 100644 index 0000000..a5e9d30 --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-12.initial.yaml @@ -0,0 +1,119 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-provider-abcd + namespace: default + deletionTimestamp: "2022-08-22T13:24:38Z" + generation: 1 + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + sme.sap.com/cav-version: 8.9.10 + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + tenantId: tenant-id-for-provider + subDomain: my-provider + operation: upgrade + capApplicationVersionInstance: test-cap-01-cav-v2 + steps: + - name: custom-say + type: CustomTenantOperation + - name: ten-op + type: TenantOperation + - name: custom-say + type: CustomTenantOperation +status: + state: Processing + observedGeneration: 1 + conditions: + - type: Ready + status: "False" + reason: StepInitiated + message: "step 1/3 : job default.test-cap-01-provider-custom-say-xyz created" + currentStep: 1 + activeJob: "test-cap-01-provider-custom-say-xyz" +--- +apiVersion: batch/v1 +kind: Job +metadata: + annotations: + sidecar.istio.io/inject: "false" + sme.sap.com/owner-identifier: default.test-cap-01-provider-abcd + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/tenant-operation-step: "1" + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: ce6bb3ae0b5ebbd0116259415ccac00bff0dc431 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + app: test-cap-01 + sme.sap.com/category: "Workload" + sme.sap.com/workload-name: "custom-say" + sme.sap.com/workload-type: "CustomTenantOperation" + generateName: test-cap-01-provider-custom-say- + name: test-cap-01-provider-custom-say-xyz + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenantOperation + name: test-cap-01-provider-abcd +spec: + backoffLimit: 1 + ttlSecondsAfterFinished: 150 + template: + metadata: + annotations: + sidecar.istio.io/inject: "false" + sme.sap.com/owner-identifier: default.test-cap-01-provider-abcd + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/tenant-operation-step: "1" + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: ce6bb3ae0b5ebbd0116259415ccac00bff0dc431 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + app: test-cap-01 + sme.sap.com/category: "Workload" + sme.sap.com/workload-name: "custom-say" + sme.sap.com/workload-type: "CustomTenantOperation" + spec: + containers: + - env: + - name: CAPOP_APP_VERSION + value: 8.9.10 + - name: CAPOP_TENANT_ID + value: tenant-id-for-provider + - name: CAPOP_TENANT_OPERATION + value: upgrade + - name: CAPOP_TENANT_SUBDOMAIN + value: my-provider + - name: close + value: encounter + envFrom: + - secretRef: + name: test-cap-01-provider-abcd-custom-say-gen + optional: true + image: docker/whalesay + command: ["cowsay", "$(CAPOP_TENANT_OPERATION)", "$(CAPOP_TENANT_ID)"] + name: custom-say + imagePullSecrets: + - name: regcred + restartPolicy: Never +status: + activeItems: 0 + conditions: + - type: Complete + status: "True" + Reason: PodCompleted diff --git a/internal/controller/testdata/captenantoperation/ctop-13.expected.yaml b/internal/controller/testdata/captenantoperation/ctop-13.expected.yaml new file mode 100644 index 0000000..f9e16e8 --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-13.expected.yaml @@ -0,0 +1,43 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-provider-abcd + namespace: default + deletionTimestamp: "2022-08-22T13:24:38Z" + generation: 1 + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + sme.sap.com/cav-version: 8.9.10 + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + tenantId: tenant-id-for-provider + subDomain: my-provider + operation: upgrade + capApplicationVersionInstance: test-cap-01-cav-v2 + steps: + - name: custom-say + type: CustomTenantOperation + - name: ten-op + type: TenantOperation + - name: custom-say + type: CustomTenantOperation +status: + state: Deleting + observedGeneration: 1 + conditions: + - type: Ready + status: "True" + observedGeneration: 1 + reason: StepFailed + message: "step 2/3 : job default.test-cap-01-provider-ten-op-xyz failed" diff --git a/internal/controller/testdata/captenantoperation/ctop-13.initial.yaml b/internal/controller/testdata/captenantoperation/ctop-13.initial.yaml new file mode 100644 index 0000000..35a32a3 --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-13.initial.yaml @@ -0,0 +1,142 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-provider-abcd + namespace: default + deletionTimestamp: "2022-08-22T13:24:38Z" + generation: 1 + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + sme.sap.com/cav-version: 8.9.10 + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + tenantId: tenant-id-for-provider + subDomain: my-provider + operation: upgrade + capApplicationVersionInstance: test-cap-01-cav-v2 + steps: + - name: custom-say + type: CustomTenantOperation + - name: ten-op + type: TenantOperation + - name: custom-say + type: CustomTenantOperation +status: + state: Processing + observedGeneration: 1 + conditions: + - type: Ready + status: "False" + reason: StepInitiated + message: "step 2/3 : job default.test-cap-01-provider-ten-op-xyz created" + currentStep: 2 + activeJob: "test-cap-01-provider-ten-op-xyz" +--- +apiVersion: batch/v1 +kind: Job +metadata: + annotations: + sidecar.istio.io/inject: "false" + sme.sap.com/owner-identifier: default.test-cap-01-provider-abcd + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/tenant-operation-step: "2" + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: ce6bb3ae0b5ebbd0116259415ccac00bff0dc431 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + app: test-cap-01 + sme.sap.com/category: "Workload" + sme.sap.com/workload-name: "ten-op" + sme.sap.com/workload-type: "TenantOperation" + generateName: test-cap-01-provider-ten-op- + name: test-cap-01-provider-ten-op-xyz + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenantOperation + name: test-cap-01-provider-abcd +spec: + backoffLimit: 1 + ttlSecondsAfterFinished: 150 + template: + metadata: + annotations: + sidecar.istio.io/inject: "false" + sme.sap.com/owner-identifier: default.test-cap-01-provider-abcd + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/tenant-operation-step: "2" + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: ce6bb3ae0b5ebbd0116259415ccac00bff0dc431 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + app: test-cap-01 + sme.sap.com/category: "Workload" + sme.sap.com/workload-name: "ten-op" + sme.sap.com/workload-type: "TenantOperation" + spec: + containers: + - env: + - name: WAIT_FOR_SIDECAR + value: "false" + - name: XSUAA_INSTANCE_NAME + value: cap-uaa + - name: MTX_SERVICE_URL + value: http://localhost:4004 + - name: MTX_REQUEST_TYPE + value: upgrade + - name: MTX_TENANT_ID + value: tenant-id-for-provider + - name: MTX_REQUEST_PAYLOAD + value: '{"tenants":["tenant-id-for-provider"],"autoUndeploy":true}' + envFrom: + - secretRef: + name: test-cap-01-provider-abcd-ten-op-gen + optional: true + image: ghcr.io/sap/cap-operator/mtx-job + name: trigger + - env: + - name: CAPOP_APP_VERSION + value: 8.9.10 + - name: CAPOP_TENANT_ID + value: tenant-id-for-provider + - name: CAPOP_TENANT_OPERATION + value: upgrade + - name: CAPOP_TENANT_SUBDOMAIN + value: my-provider + - name: flow + value: glow + envFrom: + - secretRef: + name: test-cap-01-provider-abcd-ten-op-gen + optional: true + command: + - "/bin/sh" + - "-c" + args: + - node ./node_modules/@sap/cds/bin/cds run & nc -lv -s localhost -p 8080 + image: docker.image.repo/srv/server:latest + name: ten-op + imagePullSecrets: + - name: regcred + restartPolicy: Never +status: + activeItems: 0 + conditions: + - type: Failed + status: "True" + Reason: PodError diff --git a/internal/controller/testdata/captenantoperation/ctop-14.expected.yaml b/internal/controller/testdata/captenantoperation/ctop-14.expected.yaml new file mode 100644 index 0000000..2a1d054 --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-14.expected.yaml @@ -0,0 +1,42 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-provider-abcd + namespace: default + deletionTimestamp: "2022-08-22T13:24:38Z" + generation: 1 + finalizers: [] + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + sme.sap.com/cav-version: 8.9.10 + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + tenantId: tenant-id-for-provider + subDomain: my-provider + operation: upgrade + capApplicationVersionInstance: test-cap-01-cav-v2 + steps: + - name: custom-say + type: CustomTenantOperation + - name: ten-op + type: TenantOperation + - name: custom-say + type: CustomTenantOperation +status: + state: Deleting + observedGeneration: 1 + conditions: + - type: Ready + status: "True" + observedGeneration: 1 + reason: StepFailed + message: "step 2/3 : job default.test-cap-01-provider-abcd-xyz failed" diff --git a/internal/controller/testdata/captenantoperation/ctop-14.initial.yaml b/internal/controller/testdata/captenantoperation/ctop-14.initial.yaml new file mode 100644 index 0000000..cf71c1a --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-14.initial.yaml @@ -0,0 +1,43 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-provider-abcd + namespace: default + deletionTimestamp: "2022-08-22T13:24:38Z" + generation: 1 + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + sme.sap.com/cav-version: 8.9.10 + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + tenantId: tenant-id-for-provider + subDomain: my-provider + operation: upgrade + capApplicationVersionInstance: test-cap-01-cav-v2 + steps: + - name: custom-say + type: CustomTenantOperation + - name: ten-op + type: TenantOperation + - name: custom-say + type: CustomTenantOperation +status: + state: Deleting + observedGeneration: 1 + conditions: + - type: Ready + status: "True" + observedGeneration: 1 + reason: StepFailed + message: "step 2/3 : job default.test-cap-01-provider-abcd-xyz failed" diff --git a/internal/controller/testdata/captenantoperation/ctop-15.expected.yaml b/internal/controller/testdata/captenantoperation/ctop-15.expected.yaml new file mode 100644 index 0000000..3749af1 --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-15.expected.yaml @@ -0,0 +1,16 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-invalid-tenant-op + namespace: default + generation: 1 +spec: + tenantId: tenant-id-invalid + operation: deprovisioning + capApplicationVersionInstance: test-cap-01-cav-v1 + steps: + - name: mtx + type: TenantOperation +status: + state: Processing + conditions: [] diff --git a/internal/controller/testdata/captenantoperation/ctop-15.initial.yaml b/internal/controller/testdata/captenantoperation/ctop-15.initial.yaml new file mode 100644 index 0000000..8010244 --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-15.initial.yaml @@ -0,0 +1,13 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-invalid-tenant-op + namespace: default + generation: 1 +spec: + tenantId: tenant-id-invalid + operation: deprovisioning + capApplicationVersionInstance: test-cap-01-cav-v1 + steps: + - name: mtx + type: TenantOperation diff --git a/internal/controller/testdata/captenantoperation/ctop-16.expected.yaml b/internal/controller/testdata/captenantoperation/ctop-16.expected.yaml new file mode 100644 index 0000000..cbede7a --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-16.expected.yaml @@ -0,0 +1,129 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-provider-abcd + namespace: default + generation: 1 + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/tenant-operation-type: deprovisioning + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + tenantId: tenant-id-for-provider + subDomain: my-provider + operation: deprovisioning + capApplicationVersionInstance: test-cap-01-cav-v1 + steps: + - name: mtx + type: TenantOperation +status: + state: Processing + observedGeneration: 1 + conditions: + - type: Ready + status: "False" + observedGeneration: 1 + reason: StepInitiated + message: "step 1/1 : job default.test-cap-01-provider-mtx-gen created" + currentStep: 1 + activeJob: "test-cap-01-provider-mtx-gen" +--- +apiVersion: batch/v1 +kind: Job +metadata: + annotations: + sidecar.istio.io/inject: "false" + sme.sap.com/owner-identifier: default.test-cap-01-provider-abcd + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/tenant-operation-step: "1" + sme.sap.com/tenant-operation-type: deprovisioning + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: ce6bb3ae0b5ebbd0116259415ccac00bff0dc431 + app: test-cap-01 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/category: "Workload" + sme.sap.com/workload-name: "mtx" + sme.sap.com/workload-type: "TenantOperation" + generateName: test-cap-01-provider-mtx- + name: test-cap-01-provider-mtx-gen + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenantOperation + name: test-cap-01-provider-abcd +spec: + template: + metadata: + annotations: + sidecar.istio.io/inject: "false" + sme.sap.com/owner-identifier: default.test-cap-01-provider-abcd + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/tenant-operation-step: "1" + sme.sap.com/tenant-operation-type: deprovisioning + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: ce6bb3ae0b5ebbd0116259415ccac00bff0dc431 + app: test-cap-01 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/category: "Workload" + sme.sap.com/workload-name: "mtx" + sme.sap.com/workload-type: "TenantOperation" + spec: + containers: + - env: + - name: WAIT_FOR_SIDECAR + value: "false" + - name: XSUAA_INSTANCE_NAME + value: cap-uaa + - name: MTX_SERVICE_URL + value: http://localhost:4004 + - name: MTX_REQUEST_TYPE + value: deprovisioning + - name: MTX_TENANT_ID + value: tenant-id-for-provider + - name: MTX_REQUEST_PAYLOAD + value: "{}" + envFrom: + - secretRef: + name: test-cap-01-provider-abcd-mtx-gen + optional: true + image: ghcr.io/sap/cap-operator/mtx-job + name: trigger + - env: + - name: CAPOP_APP_VERSION + value: 5.6.7 + - name: CAPOP_TENANT_ID + value: tenant-id-for-provider + - name: CAPOP_TENANT_OPERATION + value: deprovisioning + - name: CAPOP_TENANT_SUBDOMAIN + value: my-provider + - name: "IS_MTXS_ENABLED" + value: "false" + envFrom: + - secretRef: + name: test-cap-01-provider-abcd-mtx-gen + optional: true + command: + - "/bin/sh" + - "-c" + args: + - node ./node_modules/@sap/cds/bin/cds run & nc -lv -s localhost -p 8080 + image: docker.image.repo/srv/server:latest + name: mtx + imagePullSecrets: + - name: regcred + restartPolicy: Never diff --git a/internal/controller/testdata/captenantoperation/ctop-16.initial.yaml b/internal/controller/testdata/captenantoperation/ctop-16.initial.yaml new file mode 100644 index 0000000..afb672c --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-16.initial.yaml @@ -0,0 +1,32 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-provider-abcd + namespace: default + generation: 1 + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/tenant-operation-type: deprovisioning + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + tenantId: tenant-id-for-provider + subDomain: my-provider + operation: deprovisioning + capApplicationVersionInstance: test-cap-01-cav-v1 + steps: + - name: mtx + type: TenantOperation +status: + state: Processing + conditions: [] + currentStep: 1 diff --git a/internal/controller/testdata/captenantoperation/ctop-17.expected.yaml b/internal/controller/testdata/captenantoperation/ctop-17.expected.yaml new file mode 100644 index 0000000..8a5fe00 --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-17.expected.yaml @@ -0,0 +1,129 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-provider-abcd + namespace: default + generation: 1 + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/tenant-operation-type: deprovisioning + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + tenantId: tenant-id-for-provider + subDomain: my-provider + operation: deprovisioning + capApplicationVersionInstance: test-cap-01-cav-v1 + steps: + - name: mtx + type: TenantOperation +status: + state: Processing + observedGeneration: 1 + conditions: + - type: Ready + status: "False" + observedGeneration: 1 + reason: StepProcessing + message: "step 1/1 : waiting for job default.test-cap-01-provider-mtx-gen" + currentStep: 1 + activeJob: "test-cap-01-provider-mtx-gen" +--- +apiVersion: batch/v1 +kind: Job +metadata: + annotations: + sidecar.istio.io/inject: "false" + sme.sap.com/owner-identifier: default.test-cap-01-provider-abcd + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/tenant-operation-step: "1" + sme.sap.com/tenant-operation-type: deprovisioning + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: ce6bb3ae0b5ebbd0116259415ccac00bff0dc431 + app: test-cap-01 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/category: "Workload" + sme.sap.com/workload-name: "mtx" + sme.sap.com/workload-type: "TenantOperation" + generateName: test-cap-01-provider-mtx- + name: test-cap-01-provider-mtx-gen + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenantOperation + name: test-cap-01-provider-abcd +spec: + template: + metadata: + annotations: + sidecar.istio.io/inject: "false" + sme.sap.com/owner-identifier: default.test-cap-01-provider-abcd + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/tenant-operation-step: "1" + sme.sap.com/tenant-operation-type: deprovisioning + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: ce6bb3ae0b5ebbd0116259415ccac00bff0dc431 + app: test-cap-01 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/category: "Workload" + sme.sap.com/workload-name: "mtx" + sme.sap.com/workload-type: "TenantOperation" + spec: + containers: + - env: + - name: WAIT_FOR_SIDECAR + value: "false" + - name: XSUAA_INSTANCE_NAME + value: cap-uaa + - name: MTX_SERVICE_URL + value: http://localhost:4004 + - name: MTX_REQUEST_TYPE + value: deprovisioning + - name: MTX_TENANT_ID + value: tenant-id-for-provider + - name: MTX_REQUEST_PAYLOAD + value: "{}" + envFrom: + - secretRef: + name: test-cap-01-provider-abcd-mtx-gen + optional: true + image: ghcr.io/sap/cap-operator/mtx-job + name: trigger + - env: + - name: CAPOP_APP_VERSION + value: 5.6.7 + - name: CAPOP_TENANT_ID + value: tenant-id-for-provider + - name: CAPOP_TENANT_OPERATION + value: deprovisioning + - name: CAPOP_TENANT_SUBDOMAIN + value: my-provider + envFrom: + - secretRef: + name: test-cap-01-provider-abcd-mtx-gen + optional: true + command: + - "/bin/sh" + - "-c" + args: + - node ./node_modules/@sap/cds/bin/cds run & nc -lv -s localhost -p 8080 + image: docker.image.repo/srv/server:latest + name: mtx + imagePullSecrets: + - name: regcred + restartPolicy: Never +status: + activeItems: 1 diff --git a/internal/controller/testdata/captenantoperation/ctop-17.initial.yaml b/internal/controller/testdata/captenantoperation/ctop-17.initial.yaml new file mode 100644 index 0000000..f64b4d4 --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-17.initial.yaml @@ -0,0 +1,128 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-provider-abcd + namespace: default + generation: 1 + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/tenant-operation-type: deprovisioning + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + tenantId: tenant-id-for-provider + subDomain: my-provider + operation: deprovisioning + capApplicationVersionInstance: test-cap-01-cav-v1 + steps: + - name: mtx + type: TenantOperation +status: + state: Processing + observedGeneration: 1 + conditions: + - type: Ready + status: "False" + reason: StepInitiated + message: "step 1/1 : job default.test-cap-01-provider-mtx-gen created" + currentStep: 1 + activeJob: "test-cap-01-provider-mtx-gen" +--- +apiVersion: batch/v1 +kind: Job +metadata: + annotations: + sidecar.istio.io/inject: "false" + sme.sap.com/owner-identifier: default.test-cap-01-provider-abcd + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/tenant-operation-step: "1" + sme.sap.com/tenant-operation-type: deprovisioning + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: ce6bb3ae0b5ebbd0116259415ccac00bff0dc431 + app: test-cap-01 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/category: "Workload" + sme.sap.com/workload-name: "mtx" + sme.sap.com/workload-type: "TenantOperation" + generateName: test-cap-01-provider-mtx- + name: test-cap-01-provider-mtx-gen + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenantOperation + name: test-cap-01-provider-abcd +spec: + template: + metadata: + annotations: + sidecar.istio.io/inject: "false" + sme.sap.com/owner-identifier: default.test-cap-01-provider-abcd + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/tenant-operation-step: "1" + sme.sap.com/tenant-operation-type: deprovisioning + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: ce6bb3ae0b5ebbd0116259415ccac00bff0dc431 + app: test-cap-01 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/category: "Workload" + sme.sap.com/workload-name: "mtx" + sme.sap.com/workload-type: "TenantOperation" + spec: + containers: + - env: + - name: WAIT_FOR_SIDECAR + value: "false" + - name: XSUAA_INSTANCE_NAME + value: cap-uaa + - name: MTX_SERVICE_URL + value: http://localhost:4004 + - name: MTX_REQUEST_TYPE + value: deprovisioning + - name: MTX_TENANT_ID + value: tenant-id-for-provider + - name: MTX_REQUEST_PAYLOAD + value: "{}" + envFrom: + - secretRef: + name: test-cap-01-provider-abcd-mtx-gen + optional: true + image: ghcr.io/sap/cap-operator/mtx-job + name: trigger + - env: + - name: CAPOP_APP_VERSION + value: 5.6.7 + - name: CAPOP_TENANT_ID + value: tenant-id-for-provider + - name: CAPOP_TENANT_OPERATION + value: deprovisioning + - name: CAPOP_TENANT_SUBDOMAIN + value: my-provider + envFrom: + - secretRef: + name: test-cap-01-provider-abcd-mtx-gen + optional: true + command: + - "/bin/sh" + - "-c" + args: + - node ./node_modules/@sap/cds/bin/cds run & nc -lv -s localhost -p 8080 + image: docker.image.repo/srv/server:latest + name: mtx + imagePullSecrets: + - name: regcred + restartPolicy: Never +status: + activeItems: 1 diff --git a/internal/controller/testdata/captenantoperation/ctop-18.expected.yaml b/internal/controller/testdata/captenantoperation/ctop-18.expected.yaml new file mode 100644 index 0000000..b6637f7 --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-18.expected.yaml @@ -0,0 +1,110 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-provider-abcd + namespace: default + generation: 1 + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/tenant-operation-type: provisioning + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + tenantId: tenant-id-for-provider + subDomain: my-provider + operation: provisioning + capApplicationVersionInstance: test-cap-01-cav-v1 + steps: + - name: mtx + type: TenantOperation +status: + state: Processing + observedGeneration: 1 + conditions: + - type: Ready + status: "False" + observedGeneration: 1 + reason: StepInitiated + message: "step 1/1 : job default.test-cap-01-provider-mtx-gen created" + currentStep: 1 + activeJob: "test-cap-01-provider-mtx-gen" +--- +apiVersion: batch/v1 +kind: Job +metadata: + annotations: + sidecar.istio.io/inject: "false" + sme.sap.com/owner-identifier: default.test-cap-01-provider-abcd + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/tenant-operation-step: "1" + sme.sap.com/tenant-operation-type: provisioning + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: ce6bb3ae0b5ebbd0116259415ccac00bff0dc431 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + app: test-cap-01 + sme.sap.com/category: "Workload" + sme.sap.com/workload-name: "mtx" + sme.sap.com/workload-type: "TenantOperation" + generateName: test-cap-01-provider-mtx- + name: test-cap-01-provider-mtx-gen + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenantOperation + name: test-cap-01-provider-abcd +spec: + template: + metadata: + annotations: + sidecar.istio.io/inject: "false" + sme.sap.com/owner-identifier: default.test-cap-01-provider-abcd + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/tenant-operation-step: "1" + sme.sap.com/tenant-operation-type: provisioning + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: ce6bb3ae0b5ebbd0116259415ccac00bff0dc431 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + app: test-cap-01 + sme.sap.com/category: "Workload" + sme.sap.com/workload-name: "mtx" + sme.sap.com/workload-type: "TenantOperation" + spec: + containers: + - env: + - name: CAPOP_APP_VERSION + value: 5.6.7 + - name: CAPOP_TENANT_ID + value: tenant-id-for-provider + - name: CAPOP_TENANT_OPERATION + value: provisioning + - name: CAPOP_TENANT_SUBDOMAIN + value: my-provider + envFrom: + - secretRef: + name: test-cap-01-provider-abcd-mtx-gen + optional: true + command: + - node + - ./node_modules/@sap/cds-mtxs/bin/cds-mtx + - subscribe + - tenant-id-for-provider + image: docker.image.repo/srv/server:latest + imagePullPolicy: Always + name: mtx + imagePullSecrets: + - name: regcred + restartPolicy: Never + diff --git a/internal/controller/testdata/captenantoperation/ctop-18.initial.yaml b/internal/controller/testdata/captenantoperation/ctop-18.initial.yaml new file mode 100644 index 0000000..8086d46 --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-18.initial.yaml @@ -0,0 +1,32 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-provider-abcd + namespace: default + generation: 1 + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/tenant-operation-type: provisioning + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + tenantId: tenant-id-for-provider + subDomain: my-provider + operation: provisioning + capApplicationVersionInstance: test-cap-01-cav-v1 + steps: + - name: mtx + type: TenantOperation +status: + state: Processing + conditions: [] + currentStep: 1 diff --git a/internal/controller/testdata/captenantoperation/ctop-19.expected.yaml b/internal/controller/testdata/captenantoperation/ctop-19.expected.yaml new file mode 100644 index 0000000..0253daa --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-19.expected.yaml @@ -0,0 +1,109 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-provider-abcd + namespace: default + generation: 1 + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/tenant-operation-type: deprovisioning + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + tenantId: tenant-id-for-provider + subDomain: my-provider + operation: deprovisioning + capApplicationVersionInstance: test-cap-01-cav-v1 + steps: + - name: mtx + type: TenantOperation +status: + state: Processing + observedGeneration: 1 + conditions: + - type: Ready + status: "False" + observedGeneration: 1 + reason: StepInitiated + message: "step 1/1 : job default.test-cap-01-provider-mtx-gen created" + currentStep: 1 + activeJob: "test-cap-01-provider-mtx-gen" +--- +apiVersion: batch/v1 +kind: Job +metadata: + annotations: + sidecar.istio.io/inject: "false" + sme.sap.com/owner-identifier: default.test-cap-01-provider-abcd + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/tenant-operation-step: "1" + sme.sap.com/tenant-operation-type: deprovisioning + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: ce6bb3ae0b5ebbd0116259415ccac00bff0dc431 + app: test-cap-01 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/category: "Workload" + sme.sap.com/workload-name: "mtx" + sme.sap.com/workload-type: "TenantOperation" + generateName: test-cap-01-provider-mtx- + name: test-cap-01-provider-mtx-gen + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenantOperation + name: test-cap-01-provider-abcd +spec: + template: + metadata: + annotations: + sidecar.istio.io/inject: "false" + sme.sap.com/owner-identifier: default.test-cap-01-provider-abcd + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/tenant-operation-step: "1" + sme.sap.com/tenant-operation-type: deprovisioning + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: ce6bb3ae0b5ebbd0116259415ccac00bff0dc431 + app: test-cap-01 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/category: "Workload" + sme.sap.com/workload-name: "mtx" + sme.sap.com/workload-type: "TenantOperation" + spec: + containers: + - env: + - name: CAPOP_APP_VERSION + value: 5.6.7 + - name: CAPOP_TENANT_ID + value: tenant-id-for-provider + - name: CAPOP_TENANT_OPERATION + value: deprovisioning + - name: CAPOP_TENANT_SUBDOMAIN + value: my-provider + envFrom: + - secretRef: + name: test-cap-01-provider-abcd-mtx-gen + optional: true + command: + - node + - ./node_modules/@sap/cds-mtxs/bin/cds-mtx + - unsubscribe + - tenant-id-for-provider + image: docker.image.repo/srv/server:latest + imagePullPolicy: Always + name: mtx + imagePullSecrets: + - name: regcred + restartPolicy: Never diff --git a/internal/controller/testdata/captenantoperation/ctop-19.initial.yaml b/internal/controller/testdata/captenantoperation/ctop-19.initial.yaml new file mode 100644 index 0000000..afb672c --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-19.initial.yaml @@ -0,0 +1,32 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-provider-abcd + namespace: default + generation: 1 + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/tenant-operation-type: deprovisioning + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + tenantId: tenant-id-for-provider + subDomain: my-provider + operation: deprovisioning + capApplicationVersionInstance: test-cap-01-cav-v1 + steps: + - name: mtx + type: TenantOperation +status: + state: Processing + conditions: [] + currentStep: 1 diff --git a/internal/controller/testdata/captenantoperation/ctop-20.expected.yaml b/internal/controller/testdata/captenantoperation/ctop-20.expected.yaml new file mode 100644 index 0000000..3d21519 --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-20.expected.yaml @@ -0,0 +1,109 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-provider-abcd + namespace: default + generation: 1 + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + tenantId: tenant-id-for-provider + subDomain: my-provider + operation: upgrade + capApplicationVersionInstance: test-cap-01-cav-v1 + steps: + - name: mtx + type: TenantOperation +status: + state: Processing + observedGeneration: 1 + conditions: + - type: Ready + status: "False" + observedGeneration: 1 + reason: StepInitiated + message: "step 1/1 : job default.test-cap-01-provider-mtx-gen created" + currentStep: 1 + activeJob: "test-cap-01-provider-mtx-gen" +--- +apiVersion: batch/v1 +kind: Job +metadata: + annotations: + sidecar.istio.io/inject: "false" + sme.sap.com/owner-identifier: default.test-cap-01-provider-abcd + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/tenant-operation-step: "1" + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: ce6bb3ae0b5ebbd0116259415ccac00bff0dc431 + app: test-cap-01 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/category: "Workload" + sme.sap.com/workload-name: "mtx" + sme.sap.com/workload-type: "TenantOperation" + generateName: test-cap-01-provider-mtx- + name: test-cap-01-provider-mtx-gen + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenantOperation + name: test-cap-01-provider-abcd +spec: + template: + metadata: + annotations: + sidecar.istio.io/inject: "false" + sme.sap.com/owner-identifier: default.test-cap-01-provider-abcd + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/tenant-operation-step: "1" + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: ce6bb3ae0b5ebbd0116259415ccac00bff0dc431 + app: test-cap-01 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/category: "Workload" + sme.sap.com/workload-name: "mtx" + sme.sap.com/workload-type: "TenantOperation" + spec: + containers: + - env: + - name: CAPOP_APP_VERSION + value: 5.6.7 + - name: CAPOP_TENANT_ID + value: tenant-id-for-provider + - name: CAPOP_TENANT_OPERATION + value: upgrade + - name: CAPOP_TENANT_SUBDOMAIN + value: my-provider + envFrom: + - secretRef: + name: test-cap-01-provider-abcd-mtx-gen + optional: true + command: + - node + - ./node_modules/@sap/cds-mtxs/bin/cds-mtx + - upgrade + - tenant-id-for-provider + image: docker.image.repo/srv/server:latest + imagePullPolicy: Always + name: mtx + imagePullSecrets: + - name: regcred + restartPolicy: Never diff --git a/internal/controller/testdata/captenantoperation/ctop-20.initial.yaml b/internal/controller/testdata/captenantoperation/ctop-20.initial.yaml new file mode 100644 index 0000000..5b23137 --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-20.initial.yaml @@ -0,0 +1,32 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-provider-abcd + namespace: default + generation: 1 + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + tenantId: tenant-id-for-provider + subDomain: my-provider + operation: upgrade + capApplicationVersionInstance: test-cap-01-cav-v1 + steps: + - name: mtx + type: TenantOperation +status: + state: Processing + conditions: [] + currentStep: 1 diff --git a/internal/controller/testdata/captenantoperation/ctop-21.expected.yaml b/internal/controller/testdata/captenantoperation/ctop-21.expected.yaml new file mode 100644 index 0000000..5a2d547 --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-21.expected.yaml @@ -0,0 +1,115 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-provider-abcd + namespace: default + generation: 1 + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/tenant-operation-type: provisioning + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + tenantId: tenant-id-for-provider + subDomain: my-provider + operation: provisioning + capApplicationVersionInstance: test-cap-01-cav-v1 + steps: + - name: mtx + type: TenantOperation +status: + state: Processing + observedGeneration: 1 + conditions: + - type: Ready + status: "False" + observedGeneration: 1 + reason: StepInitiated + message: "step 1/1 : job default.test-cap-01-provider-mtx-gen created" + currentStep: 1 + activeJob: "test-cap-01-provider-mtx-gen" +--- +apiVersion: batch/v1 +kind: Job +metadata: + annotations: + sidecar.istio.io/inject: "false" + sme.sap.com/owner-identifier: default.test-cap-01-provider-abcd + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/tenant-operation-step: "1" + sme.sap.com/tenant-operation-type: provisioning + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: ce6bb3ae0b5ebbd0116259415ccac00bff0dc431 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + app: test-cap-01 + sme.sap.com/category: "Workload" + sme.sap.com/workload-name: "mtx" + sme.sap.com/workload-type: "TenantOperation" + generateName: test-cap-01-provider-mtx- + name: test-cap-01-provider-mtx-gen + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenantOperation + name: test-cap-01-provider-abcd +spec: + template: + metadata: + annotations: + sidecar.istio.io/inject: "false" + sme.sap.com/owner-identifier: default.test-cap-01-provider-abcd + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/tenant-operation-step: "1" + sme.sap.com/tenant-operation-type: provisioning + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: ce6bb3ae0b5ebbd0116259415ccac00bff0dc431 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + app: test-cap-01 + sme.sap.com/category: "Workload" + sme.sap.com/workload-name: "mtx" + sme.sap.com/workload-type: "TenantOperation" + spec: + containers: + - env: + - name: CAPOP_APP_VERSION + value: 5.6.7 + - name: CAPOP_TENANT_ID + value: tenant-id-for-provider + - name: CAPOP_TENANT_OPERATION + value: provisioning + - name: CAPOP_TENANT_SUBDOMAIN + value: my-provider + envFrom: + - secretRef: + name: test-cap-01-provider-abcd-mtx-gen + optional: true + command: + - node + - ./node_modules/@sap/cds-mtxs/bin/cds-mtx + - subscribe + - tenant-id-for-provider + resources: + limits: + cpu: 200m + memory: 200Mi + requests: + cpu: 20m + memory: 20Mi + image: docker.image.repo/srv/server:latest + name: mtx + imagePullSecrets: + - name: regcred + restartPolicy: Never diff --git a/internal/controller/testdata/captenantoperation/ctop-21.initial.yaml b/internal/controller/testdata/captenantoperation/ctop-21.initial.yaml new file mode 100644 index 0000000..8086d46 --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-21.initial.yaml @@ -0,0 +1,32 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-provider-abcd + namespace: default + generation: 1 + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/tenant-operation-type: provisioning + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + tenantId: tenant-id-for-provider + subDomain: my-provider + operation: provisioning + capApplicationVersionInstance: test-cap-01-cav-v1 + steps: + - name: mtx + type: TenantOperation +status: + state: Processing + conditions: [] + currentStep: 1 diff --git a/internal/controller/testdata/captenantoperation/ctop-22.expected.yaml b/internal/controller/testdata/captenantoperation/ctop-22.expected.yaml new file mode 100644 index 0000000..2889ca1 --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-22.expected.yaml @@ -0,0 +1,118 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-provider-abcd + namespace: default + generation: 1 + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/tenant-operation-type: provisioning + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + tenantId: tenant-id-for-provider + subDomain: my-provider + operation: provisioning + capApplicationVersionInstance: test-cap-01-cav-v1 + steps: + - name: mtx + type: TenantOperation +status: + state: Processing + observedGeneration: 1 + conditions: + - type: Ready + status: "False" + observedGeneration: 1 + reason: StepInitiated + message: "step 1/1 : job default.test-cap-01-provider-mtx-gen created" + currentStep: 1 + activeJob: "test-cap-01-provider-mtx-gen" +--- +apiVersion: batch/v1 +kind: Job +metadata: + annotations: + sidecar.istio.io/inject: "false" + sme.sap.com/owner-identifier: default.test-cap-01-provider-abcd + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/tenant-operation-step: "1" + sme.sap.com/tenant-operation-type: provisioning + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: ce6bb3ae0b5ebbd0116259415ccac00bff0dc431 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + app: test-cap-01 + sme.sap.com/category: "Workload" + sme.sap.com/workload-name: "mtx" + sme.sap.com/workload-type: "TenantOperation" + generateName: test-cap-01-provider-mtx- + name: test-cap-01-provider-mtx-gen + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenantOperation + name: test-cap-01-provider-abcd +spec: + template: + metadata: + annotations: + sidecar.istio.io/inject: "false" + sme.sap.com/owner-identifier: default.test-cap-01-provider-abcd + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/tenant-operation-step: "1" + sme.sap.com/tenant-operation-type: provisioning + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: ce6bb3ae0b5ebbd0116259415ccac00bff0dc431 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + app: test-cap-01 + sme.sap.com/category: "Workload" + sme.sap.com/workload-name: "mtx" + sme.sap.com/workload-type: "TenantOperation" + spec: + containers: + - env: + - name: CAPOP_APP_VERSION + value: 5.6.7 + - name: CAPOP_TENANT_ID + value: tenant-id-for-provider + - name: CAPOP_TENANT_OPERATION + value: provisioning + - name: CAPOP_TENANT_SUBDOMAIN + value: my-provider + envFrom: + - secretRef: + name: test-cap-01-provider-abcd-mtx-gen + optional: true + command: + - node + - ./node_modules/@sap/cds-mtxs/bin/cds-mtx + - subscribe + - tenant-id-for-provider + resources: + limits: + cpu: 200m + memory: 200Mi + requests: + cpu: 20m + memory: 20Mi + securityContext: + runAsUser: 1000 + runAsGroup: 2000 + image: docker.image.repo/srv/server:latest + name: mtx + imagePullSecrets: + - name: regcred + restartPolicy: Never diff --git a/internal/controller/testdata/captenantoperation/ctop-22.initial.yaml b/internal/controller/testdata/captenantoperation/ctop-22.initial.yaml new file mode 100644 index 0000000..8086d46 --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-22.initial.yaml @@ -0,0 +1,32 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-provider-abcd + namespace: default + generation: 1 + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/tenant-operation-type: provisioning + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + tenantId: tenant-id-for-provider + subDomain: my-provider + operation: provisioning + capApplicationVersionInstance: test-cap-01-cav-v1 + steps: + - name: mtx + type: TenantOperation +status: + state: Processing + conditions: [] + currentStep: 1 diff --git a/internal/controller/testdata/captenantoperation/ctop-23.expected.yaml b/internal/controller/testdata/captenantoperation/ctop-23.expected.yaml new file mode 100644 index 0000000..b7fd73f --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-23.expected.yaml @@ -0,0 +1,119 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-provider-abcd + namespace: default + generation: 1 + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + sme.sap.com/cav-version: 8.9.10 + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + tenantId: tenant-id-for-provider + subDomain: my-provider + operation: upgrade + capApplicationVersionInstance: test-cap-01-cav-v2 + steps: + - name: custom-say + type: CustomTenantOperation + - name: ten-op + type: TenantOperation + - name: custom-say + type: CustomTenantOperation +status: + state: Processing + observedGeneration: 1 + conditions: + - type: Ready + status: "False" + observedGeneration: 1 + reason: StepInitiated + message: "step 1/3 : job default.test-cap-01-provider-custom-say-gen created" + currentStep: 1 + activeJob: "test-cap-01-provider-custom-say-gen" +--- +apiVersion: batch/v1 +kind: Job +metadata: + annotations: + sidecar.istio.io/inject: "false" + sme.sap.com/owner-identifier: default.test-cap-01-provider-abcd + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/tenant-operation-step: "1" + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: ce6bb3ae0b5ebbd0116259415ccac00bff0dc431 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + app: test-cap-01 + sme.sap.com/category: "Workload" + sme.sap.com/workload-name: "custom-say" + sme.sap.com/workload-type: "CustomTenantOperation" + generateName: test-cap-01-provider-custom-say- + name: test-cap-01-provider-custom-say-gen + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenantOperation + name: test-cap-01-provider-abcd +spec: + backoffLimit: 1 + ttlSecondsAfterFinished: 150 + template: + metadata: + annotations: + sidecar.istio.io/inject: "false" + sme.sap.com/owner-identifier: default.test-cap-01-provider-abcd + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/tenant-operation-step: "1" + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: ce6bb3ae0b5ebbd0116259415ccac00bff0dc431 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + app: test-cap-01 + sme.sap.com/category: "Workload" + sme.sap.com/workload-name: "custom-say" + sme.sap.com/workload-type: "CustomTenantOperation" + spec: + containers: + - env: + - name: CAPOP_APP_VERSION + value: 8.9.10 + - name: CAPOP_TENANT_ID + value: tenant-id-for-provider + - name: CAPOP_TENANT_OPERATION + value: upgrade + - name: CAPOP_TENANT_SUBDOMAIN + value: my-provider + - name: close + value: encounter + envFrom: + - secretRef: + name: test-cap-01-provider-abcd-custom-say-gen + optional: true + securityContext: + runAsUser: 1000 + runAsGroup: 2000 + image: docker/whalesay + command: ["cowsay", "$(CAPOP_TENANT_OPERATION)", "$(CAPOP_TENANT_ID)"] + name: custom-say + imagePullSecrets: + - name: regcred + securityContext: + runAsUser: 2000 + runAsGroup: 2000 + restartPolicy: Never diff --git a/internal/controller/testdata/captenantoperation/ctop-23.initial.yaml b/internal/controller/testdata/captenantoperation/ctop-23.initial.yaml new file mode 100644 index 0000000..d95fb1e --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-23.initial.yaml @@ -0,0 +1,37 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-provider-abcd + namespace: default + generation: 1 + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: "default.test-cap-01-provider" + labels: + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + sme.sap.com/cav-version: "8.9.10" + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + tenantId: tenant-id-for-provider + subDomain: my-provider + operation: upgrade + capApplicationVersionInstance: test-cap-01-cav-v2 + steps: + - name: custom-say + type: CustomTenantOperation + - name: ten-op + type: TenantOperation + - name: custom-say + type: CustomTenantOperation +status: + state: Processing + conditions: [] + currentStep: 1 diff --git a/internal/controller/testdata/captenantoperation/ctop-24.expected.yaml b/internal/controller/testdata/captenantoperation/ctop-24.expected.yaml new file mode 100644 index 0000000..cbe9d63 --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-24.expected.yaml @@ -0,0 +1,110 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-provider-abcd + namespace: default + generation: 1 + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/tenant-operation-type: provisioning + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + tenantId: tenant-id-for-provider + subDomain: my-provider + operation: provisioning + capApplicationVersionInstance: test-cap-01-cav-v1 + steps: + - name: mtx + type: TenantOperation +status: + state: Processing + observedGeneration: 1 + conditions: + - type: Ready + status: "False" + observedGeneration: 1 + reason: StepInitiated + message: "step 1/1 : job default.test-cap-01-provider-mtx-gen created" + currentStep: 1 + activeJob: "test-cap-01-provider-mtx-gen" +--- +apiVersion: batch/v1 +kind: Job +metadata: + annotations: + foo: "bar" + sme.sap.com/my-custom-operation-identifier: "some value for the annotation: `sme.sap.com/my-custom-operation-identifier`" + sidecar.istio.io/inject: "false" + sme.sap.com/owner-identifier: default.test-cap-01-provider-abcd + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/tenant-operation-step: "1" + sme.sap.com/tenant-operation-type: provisioning + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: ce6bb3ae0b5ebbd0116259415ccac00bff0dc431 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + app: test-cap-01 + sme.sap.com/category: "Workload" + sme.sap.com/workload-name: "mtx" + sme.sap.com/workload-type: "TenantOperation" + generateName: test-cap-01-provider-mtx- + name: test-cap-01-provider-mtx-gen + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenantOperation + name: test-cap-01-provider-abcd +spec: + template: + metadata: + annotations: + foo: "bar" + sme.sap.com/my-custom-operation-identifier: "some value for the annotation: `sme.sap.com/my-custom-operation-identifier`" + sidecar.istio.io/inject: "false" + sme.sap.com/owner-identifier: default.test-cap-01-provider-abcd + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/tenant-operation-step: "1" + sme.sap.com/tenant-operation-type: provisioning + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: ce6bb3ae0b5ebbd0116259415ccac00bff0dc431 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + app: test-cap-01 + sme.sap.com/category: "Workload" + sme.sap.com/workload-name: "mtx" + sme.sap.com/workload-type: "TenantOperation" + spec: + containers: + - env: + - name: CAPOP_APP_VERSION + value: 5.6.7 + - name: CAPOP_TENANT_ID + value: tenant-id-for-provider + - name: CAPOP_TENANT_OPERATION + value: provisioning + - name: CAPOP_TENANT_SUBDOMAIN + value: my-provider + envFrom: + - secretRef: + name: test-cap-01-provider-abcd-mtx-gen + optional: true + command: + - node custom-command + image: docker.image.repo/srv/server:latest + name: mtx + imagePullSecrets: + - name: regcred + restartPolicy: Never + diff --git a/internal/controller/testdata/captenantoperation/ctop-24.initial.yaml b/internal/controller/testdata/captenantoperation/ctop-24.initial.yaml new file mode 100644 index 0000000..8086d46 --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-24.initial.yaml @@ -0,0 +1,32 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-provider-abcd + namespace: default + generation: 1 + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/tenant-operation-type: provisioning + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + tenantId: tenant-id-for-provider + subDomain: my-provider + operation: provisioning + capApplicationVersionInstance: test-cap-01-cav-v1 + steps: + - name: mtx + type: TenantOperation +status: + state: Processing + conditions: [] + currentStep: 1 diff --git a/internal/controller/testdata/captenantoperation/ctop-25.expected.yaml b/internal/controller/testdata/captenantoperation/ctop-25.expected.yaml new file mode 100644 index 0000000..0d3078b --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-25.expected.yaml @@ -0,0 +1,123 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-provider-abcd + namespace: default + generation: 1 + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + sme.sap.com/cav-version: 8.9.10 + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + tenantId: tenant-id-for-provider + subDomain: my-provider + operation: upgrade + capApplicationVersionInstance: test-cap-01-cav-v2 + steps: + - name: custom-say + type: CustomTenantOperation + - name: ten-op + type: TenantOperation + - name: custom-say + type: CustomTenantOperation +status: + state: Processing + observedGeneration: 1 + conditions: + - type: Ready + status: "False" + observedGeneration: 1 + reason: StepInitiated + message: "step 1/3 : job default.test-cap-01-provider-custom-say-gen created" + currentStep: 1 + activeJob: "test-cap-01-provider-custom-say-gen" +--- +apiVersion: batch/v1 +kind: Job +metadata: + annotations: + foo: "bar" + sme.sap.com/my-custom-app-identifier: "some value for the annotation: `sme.sap.com/my-custom-app-identifier`" + sidecar.istio.io/inject: "false" + sme.sap.com/owner-identifier: default.test-cap-01-provider-abcd + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/tenant-operation-step: "1" + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: ce6bb3ae0b5ebbd0116259415ccac00bff0dc431 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + app: test-cap-01 + sme.sap.com/category: "Workload" + sme.sap.com/workload-name: "custom-say" + sme.sap.com/workload-type: "CustomTenantOperation" + generateName: test-cap-01-provider-custom-say- + name: test-cap-01-provider-custom-say-gen + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenantOperation + name: test-cap-01-provider-abcd +spec: + backoffLimit: 1 + ttlSecondsAfterFinished: 150 + template: + metadata: + annotations: + foo: "bar" + sme.sap.com/my-custom-app-identifier: "some value for the annotation: `sme.sap.com/my-custom-app-identifier`" + sidecar.istio.io/inject: "false" + sme.sap.com/owner-identifier: default.test-cap-01-provider-abcd + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/tenant-operation-step: "1" + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: ce6bb3ae0b5ebbd0116259415ccac00bff0dc431 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + app: test-cap-01 + sme.sap.com/category: "Workload" + sme.sap.com/workload-name: "custom-say" + sme.sap.com/workload-type: "CustomTenantOperation" + spec: + containers: + - env: + - name: CAPOP_APP_VERSION + value: 8.9.10 + - name: CAPOP_TENANT_ID + value: tenant-id-for-provider + - name: CAPOP_TENANT_OPERATION + value: upgrade + - name: CAPOP_TENANT_SUBDOMAIN + value: my-provider + - name: close + value: encounter + envFrom: + - secretRef: + name: test-cap-01-provider-abcd-custom-say-gen + optional: true + securityContext: + runAsUser: 1000 + runAsGroup: 2000 + image: docker/whalesay + command: ["cowsay", "$(CAPOP_TENANT_OPERATION)", "$(CAPOP_TENANT_ID)"] + name: custom-say + imagePullSecrets: + - name: regcred + securityContext: + runAsUser: 2000 + runAsGroup: 2000 + restartPolicy: Never diff --git a/internal/controller/testdata/captenantoperation/ctop-25.initial.yaml b/internal/controller/testdata/captenantoperation/ctop-25.initial.yaml new file mode 100644 index 0000000..d95fb1e --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-25.initial.yaml @@ -0,0 +1,37 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-provider-abcd + namespace: default + generation: 1 + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: "default.test-cap-01-provider" + labels: + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + sme.sap.com/cav-version: "8.9.10" + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + tenantId: tenant-id-for-provider + subDomain: my-provider + operation: upgrade + capApplicationVersionInstance: test-cap-01-cav-v2 + steps: + - name: custom-say + type: CustomTenantOperation + - name: ten-op + type: TenantOperation + - name: custom-say + type: CustomTenantOperation +status: + state: Processing + conditions: [] + currentStep: 1 diff --git a/internal/controller/testdata/captenantoperation/ctop-26.expected.yaml b/internal/controller/testdata/captenantoperation/ctop-26.expected.yaml new file mode 100644 index 0000000..1ccc475 --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-26.expected.yaml @@ -0,0 +1,134 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-provider-abcd + namespace: default + generation: 1 + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: default.test-cap-01-provider + labels: + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + sme.sap.com/cav-version: 8.9.10 + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + tenantId: tenant-id-for-provider + subDomain: my-provider + operation: upgrade + capApplicationVersionInstance: test-cap-01-cav-v2 + steps: + - name: ten-op + type: TenantOperation +status: + state: Processing + observedGeneration: 1 + conditions: + - type: Ready + status: "False" + observedGeneration: 1 + reason: StepInitiated + message: "step 1/1 : job default.test-cap-01-provider-ten-op-gen created" + currentStep: 1 + activeJob: "test-cap-01-provider-ten-op-gen" +--- +apiVersion: batch/v1 +kind: Job +metadata: + annotations: + sidecar.istio.io/inject: "false" + sme.sap.com/owner-identifier: default.test-cap-01-provider-abcd + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/tenant-operation-step: "1" + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: ce6bb3ae0b5ebbd0116259415ccac00bff0dc431 + app: test-cap-01 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/category: "Workload" + sme.sap.com/workload-name: "ten-op" + sme.sap.com/workload-type: "TenantOperation" + generateName: test-cap-01-provider-ten-op- + name: test-cap-01-provider-ten-op-gen + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenantOperation + name: test-cap-01-provider-abcd +spec: + backoffLimit: 1 + ttlSecondsAfterFinished: 150 + template: + metadata: + annotations: + sidecar.istio.io/inject: "false" + sme.sap.com/owner-identifier: default.test-cap-01-provider-abcd + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + labels: + sme.sap.com/tenant-operation-step: "1" + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: ce6bb3ae0b5ebbd0116259415ccac00bff0dc431 + app: test-cap-01 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/category: "Workload" + sme.sap.com/workload-name: "ten-op" + sme.sap.com/workload-type: "TenantOperation" + spec: + containers: + - env: + - name: WAIT_FOR_SIDECAR + value: "false" + - name: XSUAA_INSTANCE_NAME + value: cap-uaa2 + - name: MTX_SERVICE_URL + value: http://localhost:4004 + - name: MTX_REQUEST_TYPE + value: upgrade + - name: MTX_TENANT_ID + value: tenant-id-for-provider + - name: MTX_REQUEST_PAYLOAD + value: '{"tenants":["tenant-id-for-provider"],"autoUndeploy":true}' + envFrom: + - secretRef: + name: test-cap-01-provider-abcd-ten-op-gen + optional: true + image: ghcr.io/sap/cap-operator/mtx-job + name: trigger + - env: + - name: CAPOP_APP_VERSION + value: 8.9.10 + - name: CAPOP_TENANT_ID + value: tenant-id-for-provider + - name: CAPOP_TENANT_OPERATION + value: upgrade + - name: CAPOP_TENANT_SUBDOMAIN + value: my-provider + - name: flow + value: glow + - name: "IS_MTXS_ENABLED" + value: "false" + envFrom: + - secretRef: + name: test-cap-01-provider-abcd-ten-op-gen + optional: true + command: + - "/bin/sh" + - "-c" + args: + - node ./node_modules/@sap/cds/bin/cds run & nc -lv -s localhost -p 8080 + image: docker.image.repo/srv/server:latest + name: ten-op + imagePullSecrets: + - name: regcred + restartPolicy: Never diff --git a/internal/controller/testdata/captenantoperation/ctop-26.initial.yaml b/internal/controller/testdata/captenantoperation/ctop-26.initial.yaml new file mode 100644 index 0000000..159fd11 --- /dev/null +++ b/internal/controller/testdata/captenantoperation/ctop-26.initial.yaml @@ -0,0 +1,33 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: test-cap-01-provider-abcd + namespace: default + generation: 1 + finalizers: + - sme.sap.com/captenantoperation + annotations: + sme.sap.com/owner-identifier: "default.test-cap-01-provider" + labels: + sme.sap.com/tenant-operation-type: upgrade + sme.sap.com/owner-generation: "0" + sme.sap.com/owner-identifier-hash: db1f1fd7eaeb0e6407c741b7e4b2540044bcc4ec + sme.sap.com/cav-version: "8.9.10" + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPTenant + name: test-cap-01-provider +spec: + tenantId: tenant-id-for-provider + subDomain: my-provider + operation: upgrade + capApplicationVersionInstance: test-cap-01-cav-v2 + steps: + - name: ten-op + type: TenantOperation +status: + state: Processing + conditions: [] + currentStep: 1 diff --git a/internal/controller/testdata/common/capapplication-multi-xsuaa.yaml b/internal/controller/testdata/common/capapplication-multi-xsuaa.yaml new file mode 100644 index 0000000..16d4b33 --- /dev/null +++ b/internal/controller/testdata/common/capapplication-multi-xsuaa.yaml @@ -0,0 +1,41 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + annotations: + "sme.sap.com/primary-xsuaa": "cap-uaa2" + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: xsuaa + name: cap-uaa2 + secret: cap-cap-01-uaa2-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider diff --git a/internal/controller/testdata/common/capapplication.yaml b/internal/controller/testdata/common/capapplication.yaml new file mode 100644 index 0000000..097045c --- /dev/null +++ b/internal/controller/testdata/common/capapplication.yaml @@ -0,0 +1,39 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: xsuaa + name: cap-uaa2 + secret: cap-cap-01-uaa2-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider diff --git a/internal/controller/testdata/common/capapplicationversion-invalid-env.yaml b/internal/controller/testdata/common/capapplicationversion-invalid-env.yaml new file mode 100644 index 0000000..f63156a --- /dev/null +++ b/internal/controller/testdata/common/capapplicationversion-invalid-env.yaml @@ -0,0 +1,78 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-03-18T22:14:33Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 5.6.7 + workloads: + - name: cap-backend + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + type: CAP + env: + - name: foo + value: bar + image: docker.image.repo/srv/server:latest + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + type: Content + image: docker.image.repo/content/cap-content:latest + - name: mtx + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + type: TenantOperation + image: docker.image.repo/srv/server:latest + env: + - name: foo + value: bar + - name: VCAP_SERVICES + value: "{'foo': [{'test': 'bar'}]}" + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + type: Router + env: + - name: foo + value: bar + image: docker.image.repo/approuter/approuter:latest +status: + conditions: + - lastTransitionTime: "2022-03-18T23:07:47Z" + lastUpdateTime: "2022-03-18T23:07:47Z" + reason: CreatedDeployments + status: "True" + type: Ready + observedGeneration: 1 + state: Ready diff --git a/internal/controller/testdata/common/capapplicationversion-v1-mtxs-custom-cmd.yaml b/internal/controller/testdata/common/capapplicationversion-v1-mtxs-custom-cmd.yaml new file mode 100644 index 0000000..cf634fd --- /dev/null +++ b/internal/controller/testdata/common/capapplicationversion-v1-mtxs-custom-cmd.yaml @@ -0,0 +1,72 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-03-18T22:14:33Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 5.6.7 + workloads: + - name: cap-backend + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + type: CAP + image: docker.image.repo/srv/server:latest + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + type: Content + image: docker.image.repo/content/cap-content:latest + - name: mtx + annotations: + foo: "bar" + sme.sap.com/my-custom-operation-identifier: "some value for the annotation: `sme.sap.com/my-custom-operation-identifier`" + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + type: "TenantOperation" + image: docker.image.repo/srv/server:latest + command: + - node custom-command + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + type: Router + image: docker.image.repo/approuter/approuter:latest +status: + conditions: + - lastTransitionTime: "2022-03-18T23:07:47Z" + lastUpdateTime: "2022-03-18T23:07:47Z" + reason: CreatedDeployments + status: "True" + type: Ready + observedGeneration: 1 + state: Ready diff --git a/internal/controller/testdata/common/capapplicationversion-v1-mtxs.yaml b/internal/controller/testdata/common/capapplicationversion-v1-mtxs.yaml new file mode 100644 index 0000000..3ffa992 --- /dev/null +++ b/internal/controller/testdata/common/capapplicationversion-v1-mtxs.yaml @@ -0,0 +1,68 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-03-18T22:14:33Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 5.6.7 + workloads: + - name: cap-backend + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + type: CAP + image: docker.image.repo/srv/server:latest + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + type: Content + image: docker.image.repo/content/cap-content:latest + - name: mtx + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + type: "TenantOperation" + image: docker.image.repo/srv/server:latest + imagePullPolicy: Always + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + type: Router + image: docker.image.repo/approuter/approuter:latest +status: + conditions: + - lastTransitionTime: "2022-03-18T23:07:47Z" + lastUpdateTime: "2022-03-18T23:07:47Z" + reason: CreatedDeployments + status: "True" + type: Ready + observedGeneration: 1 + state: Ready diff --git a/internal/controller/testdata/common/capapplicationversion-v1-not-ready.yaml b/internal/controller/testdata/common/capapplicationversion-v1-not-ready.yaml new file mode 100644 index 0000000..5f4c664 --- /dev/null +++ b/internal/controller/testdata/common/capapplicationversion-v1-not-ready.yaml @@ -0,0 +1,65 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-03-18T22:14:33Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + version: 5.6.7 + registrySecrets: + - regcred + workloads: + - name: cap-backend + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + type: CAP + image: docker.image.repo/srv/server:latest + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + type: Content + image: docker.image.repo/content/cap-content:latest + - name: mtx + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + type: TenantOperation + image: docker.image.repo/srv/server:latest + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + type: Router + image: docker.image.repo/approuter/approuter:latest +status: + conditions: + - reason: ReadyForProcessing + status: "False" + type: Ready + observedGeneration: 1 + state: Processing diff --git a/internal/controller/testdata/common/capapplicationversion-v1-resources.yaml b/internal/controller/testdata/common/capapplicationversion-v1-resources.yaml new file mode 100644 index 0000000..dd7e669 --- /dev/null +++ b/internal/controller/testdata/common/capapplicationversion-v1-resources.yaml @@ -0,0 +1,74 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-03-18T22:14:33Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 5.6.7 + workloads: + - name: cap-backend + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + type: CAP + image: docker.image.repo/srv/server:latest + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + type: Content + image: docker.image.repo/content/cap-content:latest + - name: mtx + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + type: "TenantOperation" + resources: + limits: + cpu: 200m + memory: 200Mi + requests: + cpu: 20m + memory: 20Mi + image: docker.image.repo/srv/server:latest + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + type: Router + image: docker.image.repo/approuter/approuter:latest +status: + conditions: + - lastTransitionTime: "2022-03-18T23:07:47Z" + lastUpdateTime: "2022-03-18T23:07:47Z" + reason: CreatedDeployments + status: "True" + type: Ready + observedGeneration: 1 + state: Ready diff --git a/internal/controller/testdata/common/capapplicationversion-v1-security-context.yaml b/internal/controller/testdata/common/capapplicationversion-v1-security-context.yaml new file mode 100644 index 0000000..7a1ff17 --- /dev/null +++ b/internal/controller/testdata/common/capapplicationversion-v1-security-context.yaml @@ -0,0 +1,77 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-03-18T22:14:33Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 5.6.7 + workloads: + - name: cap-backend + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + type: CAP + image: docker.image.repo/srv/server:latest + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + type: Content + image: docker.image.repo/content/cap-content:latest + - name: mtx + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + type: "TenantOperation" + resources: + limits: + cpu: 200m + memory: 200Mi + requests: + cpu: 20m + memory: 20Mi + securityContext: + runAsUser: 1000 + runAsGroup: 2000 + image: docker.image.repo/srv/server:latest + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + type: Router + image: docker.image.repo/approuter/approuter:latest +status: + conditions: + - lastTransitionTime: "2022-03-18T23:07:47Z" + lastUpdateTime: "2022-03-18T23:07:47Z" + reason: CreatedDeployments + status: "True" + type: Ready + observedGeneration: 1 + state: Ready diff --git a/internal/controller/testdata/common/capapplicationversion-v1.yaml b/internal/controller/testdata/common/capapplicationversion-v1.yaml new file mode 100644 index 0000000..edb876e --- /dev/null +++ b/internal/controller/testdata/common/capapplicationversion-v1.yaml @@ -0,0 +1,70 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-03-18T22:14:33Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 5.6.7 + workloads: + - name: cap-backend + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + type: CAP + image: docker.image.repo/srv/server:latest + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + type: Content + image: docker.image.repo/content/cap-content:latest + - name: mtx + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + type: "TenantOperation" + image: docker.image.repo/srv/server:latest + env: + - name: IS_MTXS_ENABLED + value: "false" + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + type: Router + image: docker.image.repo/approuter/approuter:latest +status: + conditions: + - lastTransitionTime: "2022-03-18T23:07:47Z" + lastUpdateTime: "2022-03-18T23:07:47Z" + reason: CreatedDeployments + status: "True" + type: Ready + observedGeneration: 1 + state: Ready diff --git a/internal/controller/testdata/common/capapplicationversion-v2-annotations.yaml b/internal/controller/testdata/common/capapplicationversion-v2-annotations.yaml new file mode 100644 index 0000000..5194bdf --- /dev/null +++ b/internal/controller/testdata/common/capapplicationversion-v2-annotations.yaml @@ -0,0 +1,104 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v2 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + version: 8.9.10 + registrySecrets: + - regcred + workloads: + - name: cap-backend + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + type: CAP + image: docker.image.repo/srv/server:v2 + env: + - name: foo + value: bar + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + type: Content + image: docker.image.repo/content/cap-content:v2 + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + type: Router + image: docker.image.repo/approuter/approuter:v2 + - name: ten-op + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + type: "TenantOperation" + backoffLimit: 1 + ttlSecondsAfterFinished: 150 + image: docker.image.repo/srv/server:latest + env: + - name: flow + value: glow + - name: custom-say + annotations: + foo: "bar" + sme.sap.com/my-custom-app-identifier: "some value for the annotation: `sme.sap.com/my-custom-app-identifier`" + consumedBTPServices: + - cap-uaa + jobDefinition: + type: "CustomTenantOperation" + image: docker/whalesay + command: ["cowsay", "$(CAPOP_TENANT_OPERATION)", "$(CAPOP_TENANT_ID)"] + backoffLimit: 1 + ttlSecondsAfterFinished: 150 + securityContext: + runAsUser: 1000 + runAsGroup: 2000 + podSecurityContext: + runAsUser: 2000 + runAsGroup: 2000 + env: + - name: close + value: encounter + tenantOperations: + provisioning: + - workloadName: custom-say + - workloadName: content-job # this will be ignored + - workloadName: ten-op + upgrade: + - workloadName: custom-say + - workloadName: ten-op + - workloadName: custom-say +status: + conditions: + - lastTransitionTime: "2022-03-18T23:07:47Z" + lastUpdateTime: "2022-03-18T23:07:47Z" + reason: CreatedDeployments + status: "True" + type: Ready + observedGeneration: 1 + state: Ready diff --git a/internal/controller/testdata/common/capapplicationversion-v2-multi-xsuaa.yaml b/internal/controller/testdata/common/capapplicationversion-v2-multi-xsuaa.yaml new file mode 100644 index 0000000..7d4816c --- /dev/null +++ b/internal/controller/testdata/common/capapplicationversion-v2-multi-xsuaa.yaml @@ -0,0 +1,78 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v2 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + version: 8.9.10 + registrySecrets: + - regcred + workloads: + - name: cap-backend + consumedBTPServices: + - cap-uaa + - cap-uaa2 + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + type: CAP + image: docker.image.repo/srv/server:v2 + env: + - name: foo + value: bar + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + type: Content + image: docker.image.repo/content/cap-content:v2 + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + type: Router + image: docker.image.repo/approuter/approuter:v2 + - name: ten-op + consumedBTPServices: + - cap-uaa + - cap-uaa2 + - cap-service-manager + - cap-saas-registry + jobDefinition: + type: "TenantOperation" + backoffLimit: 1 + ttlSecondsAfterFinished: 150 + image: docker.image.repo/srv/server:latest + env: + - name: flow + value: glow + - name: "IS_MTXS_ENABLED" + value: "false" +status: + conditions: + - lastTransitionTime: "2022-03-18T23:07:47Z" + lastUpdateTime: "2022-03-18T23:07:47Z" + reason: CreatedDeployments + status: "True" + type: Ready + observedGeneration: 1 + state: Ready diff --git a/internal/controller/testdata/common/capapplicationversion-v2-multiple-tenant-ops.yaml b/internal/controller/testdata/common/capapplicationversion-v2-multiple-tenant-ops.yaml new file mode 100644 index 0000000..dd5f061 --- /dev/null +++ b/internal/controller/testdata/common/capapplicationversion-v2-multiple-tenant-ops.yaml @@ -0,0 +1,98 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v2 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + version: 8.9.10 + registrySecrets: + - regcred + workloads: + - name: cap-backend + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + type: CAP + image: docker.image.repo/srv/server:v2 + env: + - name: foo + value: bar + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + type: Content + image: docker.image.repo/content/cap-content:v2 + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + type: Router + image: docker.image.repo/approuter/approuter:v2 + - name: ten-op + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + type: "TenantOperation" + backoffLimit: 1 + ttlSecondsAfterFinished: 150 + image: docker.image.repo/srv/server:latest + env: + - name: flow + value: glow + - name: "IS_MTXS_ENABLED" + value: "false" + - name: custom-say + consumedBTPServices: + - cap-uaa + jobDefinition: + type: "CustomTenantOperation" + image: docker/whalesay + imagePullPolicy: IfNotPresent + command: ["cowsay", "$(CAPOP_TENANT_OPERATION)", "$(CAPOP_TENANT_ID)"] + backoffLimit: 1 + ttlSecondsAfterFinished: 150 + env: + - name: close + value: encounter + tenantOperations: + provisioning: + - workloadName: custom-say + - workloadName: content-job # this will be ignored + - workloadName: ten-op + upgrade: + - workloadName: custom-say + - workloadName: ten-op + - workloadName: custom-say +status: + conditions: + - lastTransitionTime: "2022-03-18T23:07:47Z" + lastUpdateTime: "2022-03-18T23:07:47Z" + reason: CreatedDeployments + status: "True" + type: Ready + observedGeneration: 1 + state: Ready diff --git a/internal/controller/testdata/common/capapplicationversion-v2-no-mtx-workload.yaml b/internal/controller/testdata/common/capapplicationversion-v2-no-mtx-workload.yaml new file mode 100644 index 0000000..24d5425 --- /dev/null +++ b/internal/controller/testdata/common/capapplicationversion-v2-no-mtx-workload.yaml @@ -0,0 +1,63 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v2 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + version: 8.9.10 + registrySecrets: + - regcred + workloads: + - name: cap-backend + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + type: CAP + image: docker.image.repo/srv/server:v2 + env: + - name: foo + value: bar + - name: "IS_MTXS_ENABLED" + value: "false" + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + type: Content + image: docker.image.repo/content/cap-content:v2 + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + type: Router + image: docker.image.repo/approuter/approuter:v2 +status: + conditions: + - lastTransitionTime: "2022-03-18T23:07:47Z" + lastUpdateTime: "2022-03-18T23:07:47Z" + reason: CreatedDeployments + status: "True" + type: Ready + observedGeneration: 1 + state: Ready diff --git a/internal/controller/testdata/common/capapplicationversion-v2-pod-security-context.yaml b/internal/controller/testdata/common/capapplicationversion-v2-pod-security-context.yaml new file mode 100644 index 0000000..2212d68 --- /dev/null +++ b/internal/controller/testdata/common/capapplicationversion-v2-pod-security-context.yaml @@ -0,0 +1,101 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v2 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + version: 8.9.10 + registrySecrets: + - regcred + workloads: + - name: cap-backend + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + type: CAP + image: docker.image.repo/srv/server:v2 + env: + - name: foo + value: bar + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + type: Content + image: docker.image.repo/content/cap-content:v2 + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + type: Router + image: docker.image.repo/approuter/approuter:v2 + - name: ten-op + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + type: "TenantOperation" + backoffLimit: 1 + ttlSecondsAfterFinished: 150 + image: docker.image.repo/srv/server:latest + env: + - name: flow + value: glow + - name: custom-say + consumedBTPServices: + - cap-uaa + jobDefinition: + type: "CustomTenantOperation" + image: docker/whalesay + command: ["cowsay", "$(CAPOP_TENANT_OPERATION)", "$(CAPOP_TENANT_ID)"] + backoffLimit: 1 + ttlSecondsAfterFinished: 150 + securityContext: + runAsUser: 1000 + runAsGroup: 2000 + podSecurityContext: + runAsUser: 2000 + runAsGroup: 2000 + env: + - name: close + value: encounter + tenantOperations: + provisioning: + - workloadName: custom-say + - workloadName: content-job # this will be ignored + - workloadName: ten-op + upgrade: + - workloadName: custom-say + - workloadName: ten-op + - workloadName: custom-say +status: + conditions: + - lastTransitionTime: "2022-03-18T23:07:47Z" + lastUpdateTime: "2022-03-18T23:07:47Z" + reason: CreatedDeployments + status: "True" + type: Ready + observedGeneration: 1 + state: Ready diff --git a/internal/controller/testdata/common/capapplicationversion-v2.yaml b/internal/controller/testdata/common/capapplicationversion-v2.yaml new file mode 100644 index 0000000..c809b91 --- /dev/null +++ b/internal/controller/testdata/common/capapplicationversion-v2.yaml @@ -0,0 +1,66 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v2 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + version: 8.9.10 + registrySecrets: + - regcred + workloads: + - name: cap-backend + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + type: CAP + image: docker.image.repo/srv/server:v2 + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + type: Content + image: docker.image.repo/content/cap-content:v2 + - name: mtx + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + type: "TenantOperation" + image: docker.image.repo/srv/server:v2 + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + type: Router + image: docker.image.repo/approuter/approuter:v2 +status: + conditions: + - lastTransitionTime: "2022-03-18T23:07:47Z" + lastUpdateTime: "2022-03-18T23:07:47Z" + reason: CreatedDeployments + status: "True" + type: Ready + observedGeneration: 1 + state: Ready diff --git a/internal/controller/testdata/common/capapplicationversion-v3.yaml b/internal/controller/testdata/common/capapplicationversion-v3.yaml new file mode 100644 index 0000000..62c0e67 --- /dev/null +++ b/internal/controller/testdata/common/capapplicationversion-v3.yaml @@ -0,0 +1,66 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v3 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "113715468" + uid: 5e64489b-1234-4984-8617-e8c37338b3d8 +spec: + capApplicationInstance: test-cap-01 + version: 11.12.13 + registrySecrets: + - regcred + workloads: + - name: cap-backend + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + type: CAP + image: docker.image.repo/srv/server:v3 + - name: content-job + consumedBTPServices: + - cap-uaa + jobDefinition: + type: Content + image: docker.image.repo/content/cap-content:v3 + - name: mtx + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + type: "TenantOperation" + image: docker.image.repo/srv/server:v3 + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + type: Router + image: docker.image.repo/approuter/approuter:v3 +status: + conditions: + - lastTransitionTime: "2022-03-18T23:07:47Z" + lastUpdateTime: "2022-03-18T23:07:47Z" + reason: CreatedDeployments + status: "True" + type: Ready + observedGeneration: 1 + state: Ready diff --git a/internal/controller/testdata/common/captenant-provider-ready.yaml b/internal/controller/testdata/common/captenant-provider-ready.yaml new file mode 100644 index 0000000..5969035 --- /dev/null +++ b/internal/controller/testdata/common/captenant-provider-ready.yaml @@ -0,0 +1,37 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + finalizers: + - sme.sap.com/captenant + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-provider + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: provider + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-provider + tenantId: tenant-id-for-provider + version: 5.6.7 + versionUpgradeStrategy: always +status: + conditions: + - message: "CAPTenantOperation default.test-cap-01-provider-s6f4l successfully completed" + reason: ProvisioningCompleted + status: "True" + type: Ready + state: Ready + currentCAPApplicationVersionInstance: test-cap-01-cav-v1 diff --git a/internal/controller/testdata/common/captenant-provider-upgraded-ready.yaml b/internal/controller/testdata/common/captenant-provider-upgraded-ready.yaml new file mode 100644 index 0000000..60ad219 --- /dev/null +++ b/internal/controller/testdata/common/captenant-provider-upgraded-ready.yaml @@ -0,0 +1,37 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + finalizers: + - sme.sap.com/captenant + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-provider + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: provider + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-provider + tenantId: tenant-id-for-provider + version: 8.9.10 + versionUpgradeStrategy: always +status: + conditions: + - message: "CAPTenantOperation default.test-cap-01-provider-s6f4l successfully completed" + reason: UpgradeCompleted + status: "True" + type: Ready + state: Ready + currentCAPApplicationVersionInstance: test-cap-01-cav-v2 diff --git a/internal/controller/testdata/common/credential-secrets.yaml b/internal/controller/testdata/common/credential-secrets.yaml new file mode 100644 index 0000000..f258f50 --- /dev/null +++ b/internal/controller/testdata/common/credential-secrets.yaml @@ -0,0 +1,35 @@ +apiVersion: v1 +kind: Secret +metadata: + name: cap-cap-01-uaa-bind-cf + namespace: default +type: Opaque +data: + credentials: ewogICJ1YWFkb21haW4iOiAiYXV0aC5zZXJ2aWNlLmxvY2FsIiwKICAieHNhcHBuYW1lIjogInRlc3QtY2FwLTAxIWIxNCIsCiAgInVybCI6ICJodHRwczovL2FwcC1kb21haW4uYXV0aC5zZXJ2aWNlLmxvY2FsIiwKICAiY3JlZGVudGlhbC10eXBlIjogImluc3RhbmNlLXNlY3JldCIKfQo= +--- +apiVersion: v1 +kind: Secret +metadata: + name: cap-cap-01-uaa2-bind-cf + namespace: default +type: Opaque +data: + credentials: eyJ1YWFkb21haW4iOiJhdXRoMi5zZXJ2aWNlLmxvY2FsIiwieHNhcHBuYW1lIjoidGVzdC1jYXAtMDIhYjIxIiwidXJsIjoiaHR0cHM6Ly9hcHAtZG9tYWluMi5hdXRoLnNlcnZpY2UubG9jYWwiLCJjcmVkZW50aWFsLXR5cGUiOiJpbnN0YW5jZS1zZWNyZXQifQ== +--- +apiVersion: v1 +kind: Secret +metadata: + name: cap-cap-01-saas-bind-cf + namespace: default +type: Opaque +data: + credentials: ewogICJzYWFzX3JlZ2lzdHJ5X3VybCI6ICJodHRwczovL3NtLnNlcnZpY2UubG9jYWwiLAogICJjbGllbnRpZCI6ICJjbGllbnRpZCIsCiAgImNsaWVudHNlY3JldCI6ICJjbGllbnRzZWNyZXQiLAogICJ1YWFkb21haW4iOiAiYXV0aC5zZXJ2aWNlLmxvY2FsIgp9Cg== +--- +apiVersion: v1 +kind: Secret +metadata: + name: cap-cap-01-svc-man-bind-cf + namespace: default +type: Opaque +data: + credentials: ewogICJzbV91cmwiOiAiaHR0cHM6Ly9zbS5zZXJ2aWNlLmxvY2FsIiwKICAiY2xpZW50aWQiOiAiY2xpZW50aWQiLAogICJjbGllbnRzZWNyZXQiOiAiY2xpZW50c2VjcmV0IiwKICAidWFhZG9tYWluIjogImF1dGguc2VydmljZS5sb2NhbCIKfQo= diff --git a/internal/controller/testdata/common/istio-ingress.yaml b/internal/controller/testdata/common/istio-ingress.yaml new file mode 100644 index 0000000..f4bb51d --- /dev/null +++ b/internal/controller/testdata/common/istio-ingress.yaml @@ -0,0 +1,46 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: istio-system +status: + phase: Active +--- +apiVersion: v1 +kind: Pod +metadata: + labels: + app: istio-ingressgateway + istio: ingressgateway + name: istio-ingressgateway-5dbdc4cdbb-pvgtp + namespace: istio-system +spec: + containers: + - image: istio.ingress.image.local:1.12.2 + imagePullPolicy: IfNotPresent + name: istio-proxy +--- +apiVersion: v1 +kind: Service +metadata: + annotations: + dns.gardener.cloud/class: garden + dns.gardener.cloud/dnsnames: public-ingress.operator.testing.local + dns.gardener.cloud/ttl: "600" + creationTimestamp: "2022-03-01T15:14:59Z" + finalizers: + - garden.dns.gardener.cloud/service-dns + name: istio-ingressgateway + namespace: istio-system + resourceVersion: "4876" + uid: ee535038-2f0f-4d9a-adbd-1ae05ba1e864 +spec: + ports: + - name: https + port: 443 + protocol: TCP + targetPort: 8443 + selector: + app: istio-ingressgateway + istio: ingressgateway + sessionAffinity: None + type: LoadBalancer diff --git a/internal/controller/testdata/common/operator-gateway.yaml b/internal/controller/testdata/common/operator-gateway.yaml new file mode 100644 index 0000000..5b60fc3 --- /dev/null +++ b/internal/controller/testdata/common/operator-gateway.yaml @@ -0,0 +1,26 @@ +apiVersion: networking.istio.io/v1beta1 +kind: Gateway +metadata: + generateName: cap-operator-domains- + generation: 1 + annotations: + sme.sap.com/owner-identifier: CAPOperator.OperatorDomains + labels: + sme.sap.com/owner-identifier-hash: 933c63ba93a11feed88e28ae21e68591bf13256b + sme.sap.com/relevant-dns-target-hash: 2b2bdb40343ac1ec9a7d18bce136a564c026e7d2 + name: cap-operator-domains-gen + namespace: istio-system +spec: + selector: + app: istio-ingressgateway + istio: ingressgateway + servers: + - hosts: + - "*.foo.bar.local" + port: + name: foo.bar.local + number: 443 + protocol: HTTPS + tls: + credentialName: test.local + mode: SIMPLE diff --git a/internal/controller/utils.go b/internal/controller/utils.go new file mode 100644 index 0000000..2ee04bd --- /dev/null +++ b/internal/controller/utils.go @@ -0,0 +1,222 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +package controller + +import ( + "crypto/sha1" + "crypto/sha256" + "fmt" + "os" + "strconv" + "strings" + + "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + "golang.org/x/exp/slices" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/klog/v2" +) + +const ( + certManagerGardener = "gardener" + certManagerCertManagerIO = "cert-manager.io" + dnsManagerGardener = "gardener" + dnsManagerKubernetes = "kubernetes" +) + +const ( + certManagerEnv = "CERT_MANAGER" + dnsManagerEnv = "DNS_MANAGER" +) + +type ownerInfo struct { + ownerNamespace string + ownerName string + ownerGeneration int64 +} +type appMetadataIdentifiers struct { + globalAccountId string + appName string + ownerInfo *ownerInfo +} + +func getOwnerByKind(owners []metav1.OwnerReference, kind string) (*metav1.OwnerReference, bool) { + for _, o := range owners { + if o.APIVersion == v1alpha1.SchemeGroupVersion.String() && o.Kind == kind && *o.Controller { + return &o, true + } + } + return nil, false +} + +func getOwnerFromObjectMetadata(objectMeta metav1.Object, dependentKind string) (NamespacedResourceKey, bool) { + var ownerKey NamespacedResourceKey + + annotations := objectMeta.GetAnnotations() + + if annotations[AnnotationOwnerIdentifier] != "" { + identifier := strings.Split(annotations[AnnotationOwnerIdentifier], ".") + if len(identifier) == 3 && identifier[0] == dependentKind { + ownerKey.Namespace = identifier[1] + ownerKey.Name = identifier[2] + return ownerKey, true + } + } + + return ownerKey, false +} + +func convertToLocalObjectReferences(entries []string) []corev1.LocalObjectReference { + localObjects := make([]corev1.LocalObjectReference, 0) + for _, entry := range entries { + localObjects = append(localObjects, corev1.LocalObjectReference{Name: entry}) + } + return localObjects +} + +func getResourceKindFromKey(key int) string { + kind, ok := KindMap[key] + if !ok { + kind = "unknown" + } + return kind +} + +/* +check whether the status of a custom resource (sme.sap.com) is Ready (based on metav1.Condition) +*/ +func isCROConditionReady(status v1alpha1.GenericStatus) bool { + if status.Conditions == nil { + return false + } + + return slices.ContainsFunc(status.Conditions, func(condition metav1.Condition) bool { + return condition.Type == string(v1alpha1.ConditionTypeReady) && condition.Status == metav1.ConditionTrue + }) +} + +func addFinalizer(finalizers *[]string, finalizerType string) bool { + finalizerExists := slices.ContainsFunc(*finalizers, func(f string) bool { + return f == finalizerType + }) + + if !finalizerExists { + *finalizers = append(*finalizers, finalizerType) + return true + } + return false +} + +func removeFinalizer(finalizers *[]string, finalizerType string) bool { + finalizerExists := false + adjusted := make([]string, 0) + for _, f := range *finalizers { + if f != finalizerType { + adjusted = append(adjusted, f) + } else { + finalizerExists = true + } + } + + if finalizerExists { + *finalizers = adjusted + return true + } + return false +} + +func certificateManager() string { + mgr := certManagerGardener + env := os.Getenv(certManagerEnv) + if env != "" { + if env == certManagerGardener || env == certManagerCertManagerIO { + mgr = env + } else { + klog.Error("Error parsing certificate manager environment variable: invalid value") + } + } + return mgr +} + +func dnsManager() string { + mgr := dnsManagerGardener + env := os.Getenv(dnsManagerEnv) + if env != "" { + if env == dnsManagerGardener || env == dnsManagerKubernetes { + mgr = env + } else { + klog.Error("Error parsing DNS manager environment variable: invalid value") + } + } + return mgr +} + +func updateResourceAnnotation(object *metav1.ObjectMeta, hash string) { + if object.Annotations == nil { + object.Annotations = map[string]string{} + } + // Update Annotation hash + object.Annotations[AnnotationResourceHash] = hash +} + +// Returns an sha256 checksum for a given source string +func sha256Sum(source ...string) string { + sum := sha256.Sum256([]byte(strings.Join(source, ""))) + return fmt.Sprintf("%x", sum) +} + +// Returns an sha1 checksum for a given source string +func sha1Sum(source ...string) string { + sum := sha1.Sum([]byte(strings.Join(source, ""))) + return fmt.Sprintf("%x", sum) +} + +func amendObjectMetadata(object *metav1.ObjectMeta, annotatedOldLabel string, hashLabel string, oldValue string, hashedValue string) (updated bool) { + // Check if old label exists, if so remove it + if _, ok := object.Labels[annotatedOldLabel]; ok { + // Should never happen + klog.Infof("Unexpected label %s=%s found for resource %s.%s", annotatedOldLabel, oldValue, object.Namespace, object.Name) + delete(object.Labels, annotatedOldLabel) + updated = true + } + // Add hashed label as the new label with the hashed identifier value + if _, ok := object.Labels[hashLabel]; !ok { + object.Labels[hashLabel] = hashedValue + updated = true + } + // Add old label as an annotation with the old value + if _, ok := object.Annotations[annotatedOldLabel]; !ok { + object.Annotations[annotatedOldLabel] = oldValue + updated = true + } + // return if something was updated + return updated +} + +func updateLabelAnnotationMetadata(object *metav1.ObjectMeta, appMetadata *appMetadataIdentifiers) (updated bool) { + if object.Labels == nil { + object.Labels = make(map[string]string) + } + if object.Annotations == nil { + object.Annotations = map[string]string{} + } + + // Update BTP Application Identifier + if appMetadata.globalAccountId != "" && amendObjectMetadata(object, AnnotationBTPApplicationIdentifier, LabelBTPApplicationIdentifierHash, strings.Join([]string{appMetadata.globalAccountId, appMetadata.appName}, "."), sha1Sum(appMetadata.globalAccountId, appMetadata.appName)) { + updated = true + } + + // Update OwnerInfo if owner details exists + if appMetadata.ownerInfo != nil { + if amendObjectMetadata(object, AnnotationOwnerIdentifier, LabelOwnerIdentifierHash, strings.Join([]string{appMetadata.ownerInfo.ownerNamespace, appMetadata.ownerInfo.ownerName}, "."), sha1Sum(appMetadata.ownerInfo.ownerNamespace, appMetadata.ownerInfo.ownerName)) { + updated = true + } + if _, ok := object.Labels[LabelOwnerGeneration]; !ok { + object.Labels[LabelOwnerGeneration] = strconv.FormatInt(appMetadata.ownerInfo.ownerGeneration, 10) + } + } + + return updated +} diff --git a/internal/util/config.go b/internal/util/config.go new file mode 100644 index 0000000..db7c70b --- /dev/null +++ b/internal/util/config.go @@ -0,0 +1,59 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +package util + +import ( + "io/ioutil" + "os" + "path" + "strings" + + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/klog/v2" +) + +func GetConfig() *rest.Config { + // Try to load config from within cluster + config, err := rest.InClusterConfig() + if err != nil { + klog.Warning("Could not load config from cluster; will attempt to load from file") + + // Load config from local/home directory + loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() + pwd, err := os.Getwd() + if err != nil { + klog.Fatal("Could not determine working directory") + } + loadingRules.Precedence = append(loadingRules.Precedence, path.Join(pwd, ".kubeconfig")) + clientConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, &clientcmd.ConfigOverrides{}) + config, err = clientConfig.ClientConfig() + if err != nil { + klog.Fatal("Error: ", err) + return nil + } else { + klog.Info("Found config file in: ", clientConfig.ConfigAccess().GetDefaultFilename()) + } + } else { + klog.Info("Found config in cluster") + } + + return config +} + +func GetNamespace() string { + if ns := os.Getenv("POD_NAMESPACE"); ns != "" { + return ns + } + + // Fall back to the namespace associated with the service account token, if available + if data, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace"); err == nil { + if ns := strings.TrimSpace(string(data)); len(ns) > 0 { + return ns + } + } + + return "default" +} diff --git a/internal/util/types.go b/internal/util/types.go new file mode 100644 index 0000000..f9e85b1 --- /dev/null +++ b/internal/util/types.go @@ -0,0 +1,42 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +package util + +type VCAPServiceInstance struct { + Name string `json:"name"` // this attribute holds the binding name if it exists, otherwise, instance name + BindingGUID string `json:"binding_guid,omitempty"` + BindingName string `json:"binding_name,omitempty"` + InstanceGUID string `json:"instance_guid,omitempty"` + InstanceName string `json:"instance_name,omitempty"` + Label string `json:"label"` + Plan string `json:"plan,omitempty"` + Credentials interface{} `json:"credentials"` + Tags []string `json:"tags,omitempty"` +} + +type CredentialData struct { + CredentialType string `json:"credential-type"` + ClientId string `json:"clientid"` + ClientSecret string `json:"clientsecret"` + AuthUrl string `json:"url"` + UAADomain string `json:"uaadomain"` + ServiceBrokerUrl string `json:"sburl"` + CertificateUrl string `json:"certurl"` + Certificate string `json:"certificate"` + CertificateKey string `json:"key"` + VerificationKey string `json:"verificationkey"` + CallbackTimeoutMillis string `json:"callbackTimeoutMillis"` +} + +type SaasRegistryCredentials struct { + CredentialData `json:",inline"` + SaasManagerUrl string `json:"saas_registry_url"` +} + +type XSUAACredentials struct { + CredentialData `json:",inline"` + XSAppName string `json:"xsappname"` + TrusterClientIDSuffix string `json:"trustedclientidsuffix"` +} diff --git a/internal/util/uaa.go b/internal/util/uaa.go new file mode 100644 index 0000000..ec341f5 --- /dev/null +++ b/internal/util/uaa.go @@ -0,0 +1,40 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +package util + +import ( + "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + "golang.org/x/exp/slices" +) + +const ( + AnnotationPrimaryXSUAA = "sme.sap.com/primary-xsuaa" +) + +func GetXSUAAInfo(consumedServiceInfos []v1alpha1.ServiceInfo, ca *v1alpha1.CAPApplication) *v1alpha1.ServiceInfo { + // Get primary xsuaa service instance name + primaryXSUAA := ca.Annotations[AnnotationPrimaryXSUAA] + + serviceIndex := -1 + + // Check if matching service with annotated xsuaa name exists + if primaryXSUAA != "" { + serviceIndex = slices.IndexFunc(consumedServiceInfos, func(consumedServiceInfo v1alpha1.ServiceInfo) bool { + return consumedServiceInfo.Name == primaryXSUAA + }) + } + + // Fallback to using 1st matching "xsuaa" class in the list of consumed services + if serviceIndex == -1 { + serviceIndex = slices.IndexFunc(consumedServiceInfos, func(consumedServiceInfo v1alpha1.ServiceInfo) bool { return consumedServiceInfo.Class == "xsuaa" }) + } + + // Return matching service info if any + if serviceIndex > -1 { + return &consumedServiceInfos[serviceIndex] + } + + return nil +} diff --git a/internal/util/uaa_test.go b/internal/util/uaa_test.go new file mode 100644 index 0000000..3e391e6 --- /dev/null +++ b/internal/util/uaa_test.go @@ -0,0 +1,97 @@ +// This file is needed just to show some coverage as go tests report coverage package wise. The usage in controller and server already cover most of the code. +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +package util + +import ( + "strings" + "testing" + + "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var serviceInfos = []v1alpha1.ServiceInfo{ + { + Class: "xsuaa", + Name: "test-xsuaa", + Secret: "test-xsuaa-sec", + }, + { + Class: "xsuaa", + Name: "test-xsuaa2", + Secret: "test-xsuaa-sec2", + }, + { + Class: "saas-registry", + Name: "test-saas", + Secret: "test-saas-sec", + }, + { + Class: "service-manager", + Name: "test-sm", + Secret: "test-sm-sec", + }, + { + Class: "destination", + Name: "test-dest", + Secret: "test-dest-sec", + }, + { + Class: "html5-apps-repo", + Name: "test-html-host", + Secret: "test-html-host-sec", + }, + { + Class: "html5-apps-repo", + Name: "test-html-rt", + Secret: "test-html-rt-sec", + }, +} + +func execTestsWithBLI(t *testing.T, name string, backlogItems []string, test func(t *testing.T)) { + t.Run(name+", BLIs: "+strings.Join(backlogItems, ", "), test) +} + +func TestGetXSUAAInfoMissingService(t *testing.T) { + execTestsWithBLI(t, "Check that no uaa info is returned when no uaa service is present", []string{"ERP4SMEPREPWORKAPPPLAT-3773"}, func(t *testing.T) { + res := GetXSUAAInfo([]v1alpha1.ServiceInfo{}, &v1alpha1.CAPApplication{}) + + if res != nil { + t.Error("unexpected uaa info") + } + }) +} +func TestGetXSUAAInfoWithoutAnnotation(t *testing.T) { + execTestsWithBLI(t, "Check that the 1st uaa info is returned with CA with no annotation is present", []string{"ERP4SMEPREPWORKAPPPLAT-3773"}, func(t *testing.T) { + // CA without "sme.sap.com/primary-xsuaa" annotation + ca := v1alpha1.CAPApplication{} + + res := GetXSUAAInfo(serviceInfos, &ca) + + if res.Name != "test-xsuaa" { + t.Error("incorrect uaa info") + } + }) +} + +func TestGetXSUAAInfoWithAnnotation(t *testing.T) { + execTestsWithBLI(t, "Check that the right uaa info is returned for CA with annotation present", []string{"ERP4SMEPREPWORKAPPPLAT-3773"}, func(t *testing.T) { + // CA without "sme.sap.com/primary-xsuaa" annotation + ca := v1alpha1.CAPApplication{ + ObjectMeta: v1.ObjectMeta{ + Annotations: map[string]string{ + AnnotationPrimaryXSUAA: "test-xsuaa2", + }, + }, + } + + res := GetXSUAAInfo(serviceInfos, &ca) + + if res.Name != "test-xsuaa2" { + t.Error("incorrect uaa info") + } + }) +} diff --git a/internal/util/vcap-credentials.go b/internal/util/vcap-credentials.go new file mode 100644 index 0000000..92b9b4a --- /dev/null +++ b/internal/util/vcap-credentials.go @@ -0,0 +1,153 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +package util + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +// See Kubernetes-Service-Bindings/doc +type SecretMetadata struct { + MetadataProperties []MetadataPropertyDescriptor `json:"metaDataProperties"` + CredentialProperties []MetadataPropertyDescriptor `json:"credentialProperties"` +} + +type MetadataPropertyDescriptor struct { + Name string `json:"name"` + SourceName string `json:"sourceName"` + Format PropertyFormat `json:"format"` + Container bool `json:"container"` +} + +func (d MetadataPropertyDescriptor) getKey() (key string) { + key = d.SourceName + if key == "" { + key = d.Name + } + return +} + +func (d MetadataPropertyDescriptor) move(source map[string][]byte, target map[string]any) (map[string]any, error) { + key := d.getKey() + switch d.Format { + case PropertyFormatText: + target[d.Name] = string(source[key]) // when property is a container, it cannot have text format + case PropertyFormatJSON: + var ( + v *any + err error + ) + if v, err = ParseJSON[any](source[key]); err != nil { + return nil, err + } + if d.Container { + return (*v).(map[string]any), nil + } + target[d.Name] = v + } + return target, nil +} + +type PropertyFormat string + +const ( + PropertyFormatText PropertyFormat = "text" + PropertyFormatJSON PropertyFormat = "json" +) + +func ReadServiceCredentialsFromSecret[T any](serviceInfo *v1alpha1.ServiceInfo, ns string, kubeClient kubernetes.Interface) (*T, error) { + entry, err := CreateVCAPEntryFromSecret(serviceInfo, ns, kubeClient) + if err != nil { + return nil, err + } + b, err := json.Marshal(entry["credentials"]) + if err != nil { + return nil, fmt.Errorf("could not serialize credentials for service %s: %s", serviceInfo.Name, err) + } + return ParseJSON[T](b) +} + +func CreateVCAPEntryFromSecret(serviceInfo *v1alpha1.ServiceInfo, ns string, kubeClient kubernetes.Interface) (entry map[string]any, err error) { + // Get secret + secret, err := kubeClient.CoreV1().Secrets(ns).Get(context.TODO(), serviceInfo.Secret, metav1.GetOptions{}) + if err != nil { + return nil, err + } + + return createVCAPEntry(secret.Data, serviceInfo) +} + +func createVCAPEntry(data map[string][]byte, si *v1alpha1.ServiceInfo) (entry map[string]any, err error) { + if metaBytes, ok := data[".metadata"]; ok { // metadata available from new service binding specification + var meta SecretMetadata + if err = json.Unmarshal(metaBytes, &meta); err != nil { + return nil, errorCredentialParse("metadata", si.Secret, err) + } + return createVCAPEntryWithMetadata(data, &meta, si) + } else { // fallback to reading secret-key "credentials" + credBytes, ok := data["credentials"] + if !ok { + return nil, fmt.Errorf("could not find credentials for secret %s", si.Secret) + } + // Parse JSON value for the given service secret + cred, err := ParseJSON[map[string]any](credBytes) + if err != nil { + return nil, errorCredentialParse("credentials", si.Secret, err) + } + entry = map[string]any{ + "credentials": cred, + "label": si.Class, + "name": si.Name, + "instance_name": si.Name, + "tags": []string{si.Class}, // app-router looks for xsuaa in tags alone! So add class as a tag! + } + } + return +} + +func createVCAPEntryWithMetadata(data map[string][]byte, meta *SecretMetadata, si *v1alpha1.ServiceInfo) (entry map[string]any, err error) { + entry = map[string]any{"credentials": map[string]any{}} + for i := range meta.MetadataProperties { + if entry, err = meta.MetadataProperties[i].move(data, entry); err != nil { + return nil, errorCredentialParse("metadata", si.Secret, err) + } + } + for i := range meta.CredentialProperties { + if entry["credentials"], err = meta.CredentialProperties[i].move(data, entry["credentials"].(map[string]any)); err != nil { + return nil, errorCredentialParse("credentials", si.Secret, err) + } + if meta.CredentialProperties[i].Container { + break + } + } + if _, ok := entry["label"]; !ok { + entry["label"] = si.Class // ensure label is provided + } + // ensure name is set + if instanceName, ok := entry["instance_name"]; ok { + entry["name"] = instanceName // conform to VCAP_SERVICES specification (https://docs.cloudfoundry.org/devguide/deploy-apps/environment-variable.html#VCAP-SERVICES) + } else { + entry["name"] = si.Name // source from service info as a fallback + } + return +} + +func errorCredentialParse(key string, secret string, err error) error { + return fmt.Errorf("could not parse %s from secret %s: %w", key, secret, err) +} + +func ParseJSON[T any](b []byte) (*T, error) { + var v T + if err := json.Unmarshal(b, &v); err != nil { + return nil, err // ensure nil pointer is returned in case of error + } + return &v, nil +} diff --git a/internal/util/vcap-credentials_test.go b/internal/util/vcap-credentials_test.go new file mode 100644 index 0000000..bdee273 --- /dev/null +++ b/internal/util/vcap-credentials_test.go @@ -0,0 +1,215 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +package util + +import ( + "reflect" + "runtime" + "strings" + "testing" + + "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + k8sruntime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/fake" +) + +func createTestClient() (*fake.Clientset, error) { + secs := []k8sruntime.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "no-meta-credential-key", Namespace: "default"}, + Data: map[string][]byte{ + "credentials": []byte(`{ + "user": "a-user", + "password": "some-pass" + }`), + }, + }, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "metadata-with-credential-key", Namespace: "default"}, + Data: map[string][]byte{ + ".metadata": []byte(`{ + "metadataProperties": [ + {"name": "instance_name", "sourceName": "service_instance", "format": "text"}, + {"name": "plan", "format": "text"}, + {"name": "type", "format": "text"} + ], + "credentialProperties": [ + {"name": "credentials", "sourceName": "secret-data", "format": "json", "container": true} + ] + }`), + "secret-data": []byte(`{ + "user": "a-user", + "password": "some-pass" + }`), + "service_instance": []byte(`service-a`), + "plan": []byte(`default`), + "type": []byte(`xyz`), + }, + }, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "malformed-metadata", Namespace: "default"}, + Data: map[string][]byte{ + ".metadata": []byte(`{ + "metadataProperties": [ + {"name": "instance_name", "sourceName": "service_instance", "format": "text"}, + {"name": "plan", "format": "text"}, + {"name": "type", "format": "text"} + ], + "MALFORMED": [[[ // + {"name": "credentials", "sourceName": "secret-data", "format": "json", "container": true} + ] + }`), + "secret-data": []byte(`{ + "user": "a-user", + "password": "some-pass" + }`), + "service_instance": []byte(`service-a`), + "plan": []byte(`default`), + "type": []byte(`xyz`), + }, + }, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "metadata-with-credential-properties", Namespace: "default"}, + Data: map[string][]byte{ + ".metadata": []byte(`{ + "metadataProperties": [ + {"name": "instance_name", "sourceName": "service_instance", "format": "text"}, + {"name": "plan", "format": "text"}, + {"name": "type", "format": "text"}, + {"name": "tags", "format": "json"} + ], + "credentialProperties": [ + {"name": "api-keys", "sourceName": "keys", "format": "json"}, + {"name": "host", "format": "text"} + ] + }`), + "secret-data": []byte(`{ + "user": "a-user", + "password": "some-pass" + }`), + "service_instance": []byte(`service-a`), + "plan": []byte(`default`), + "type": []byte(`xyz`), + "host": []byte(`abc.url.local`), + "keys": []byte(`["one", "two"]`), + "tags": []byte(`["xyz", "lmnop"]`), + }, + }, + } + return fake.NewSimpleClientset(secs...), nil +} + +func testCreateVCAPEntryFromSecret(t *testing.T) { + type testCase struct { + name string + serviceInfo *v1alpha1.ServiceInfo + namespace string + expectError bool + errorMsg string + expectedLabel string + expectedInstanceName string + expectTags bool + } + cases := []testCase{ + { + name: "valid credential secret without metadata", + namespace: "default", + serviceInfo: &v1alpha1.ServiceInfo{Name: "service-a", Secret: "no-meta-credential-key", Class: "xzy"}, + expectedInstanceName: "service-a", + expectedLabel: "xyz", + }, + { + name: "secret not found", + namespace: "another", + serviceInfo: &v1alpha1.ServiceInfo{Name: "service-a", Secret: "no-meta-credential-key", Class: "xzy"}, + expectError: true, + errorMsg: "secrets \"no-meta-credential-key\" not found", + }, + { + name: "valid credentials (container) with metadata", + namespace: "default", + serviceInfo: &v1alpha1.ServiceInfo{Name: "service-a", Secret: "metadata-with-credential-key", Class: "xzy"}, + expectedInstanceName: "service-a", + expectedLabel: "xyz", + }, + { + name: "malformed metadata", + namespace: "default", + serviceInfo: &v1alpha1.ServiceInfo{Name: "service-a", Secret: "malformed-metadata", Class: "xzy"}, + expectError: true, + errorMsg: "could not parse metadata from secret malformed-metadata: invalid character '/' looking for beginning of value", + }, + { + name: "valid credentials (multiple properties) with metadata", + namespace: "default", + serviceInfo: &v1alpha1.ServiceInfo{Name: "service-a", Secret: "metadata-with-credential-properties", Class: "xzy"}, + expectedInstanceName: "service-a", + expectedLabel: "xyz", + expectTags: true, + }, + } + c, _ := createTestClient() + for i := range cases { + t.Run(cases[i].name, func(t *testing.T) { + config := &cases[i] + entry, err := CreateVCAPEntryFromSecret(config.serviceInfo, config.namespace, c) + if err != nil { + if !config.expectError { + t.Errorf("unexpected error in test case: %s", config.name) + } + if config.errorMsg != "" && err.Error() != config.errorMsg { + t.Errorf("error differs from expected for test case: %s", config.name) + } + return + } else { + if config.expectError { + t.Errorf("expected error in test case: %s", config.name) + } + } + if config.expectedInstanceName != "" && config.expectedInstanceName != entry["instance_name"].(string) { + t.Errorf("instance name differs from expected for test case: %s", config.name) + } + if tags, ok := entry["tags"]; config.expectTags && (!ok || tags == nil) { + t.Errorf("expected tags for test case: %s", config.name) + } + }) + } +} + +func testReadServiceCredentialsFromSecret(t *testing.T) { + c, _ := createTestClient() + + // test successful read + secretName := "metadata-with-credential-key" + credentials, err := ReadServiceCredentialsFromSecret[map[string]string](&v1alpha1.ServiceInfo{Name: "service-a", Class: "xyz", Secret: secretName}, "default", c) + if err != nil { + t.Errorf("could not read credentials from secret %s", secretName) + } + if (*credentials)["user"] != "a-user" || (*credentials)["password"] != "some-pass" { + t.Errorf("credential attributes from secret %s not as expected", secretName) + } + + // test with type mismatch + _, err = ReadServiceCredentialsFromSecret[[]string](&v1alpha1.ServiceInfo{Name: "service-a", Class: "xyz", Secret: secretName}, "default", c) + if err == nil { + t.Errorf("expected error when reading credentials as array from secret %s", secretName) + } +} + +func TestVCAPCredentials(t *testing.T) { + catalog := &[]struct { + test func(t *testing.T) + backlogItems []string + }{ + {test: testCreateVCAPEntryFromSecret, backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2611"}}, + {test: testReadServiceCredentialsFromSecret, backlogItems: []string{"ERP4SMEPREPWORKAPPPLAT-2611"}}, + } + for _, tc := range *catalog { + nameParts := []string{runtime.FuncForPC(reflect.ValueOf(tc.test).Pointer()).Name()} + t.Run(strings.Join(append(nameParts, tc.backlogItems...), " "), tc.test) + } +} diff --git a/pkg/apis/sme.sap.com/v1alpha1/doc.go b/pkg/apis/sme.sap.com/v1alpha1/doc.go new file mode 100644 index 0000000..3bd2a8b --- /dev/null +++ b/pkg/apis/sme.sap.com/v1alpha1/doc.go @@ -0,0 +1,8 @@ +// +k8s:deepcopy-gen=package +// +groupName=sme.sap.com + +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +package v1alpha1 diff --git a/pkg/apis/sme.sap.com/v1alpha1/register.go b/pkg/apis/sme.sap.com/v1alpha1/register.go new file mode 100644 index 0000000..ac873b9 --- /dev/null +++ b/pkg/apis/sme.sap.com/v1alpha1/register.go @@ -0,0 +1,57 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +package v1alpha1 + +import ( + metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// SchemeGroupVersion is group version used to register these objects +var SchemeGroupVersion = schema.GroupVersion{Group: Group, Version: Version} + +var ( + // SchemeBuilder initializes a scheme builder + SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) + // AddToScheme is a global function that registers this API group & version to a scheme + AddToScheme = SchemeBuilder.AddToScheme +) + +// Kind takes an unqualified kind and returns back a Group qualified GroupKind +func Kind(kind string) schema.GroupKind { + return SchemeGroupVersion.WithKind(kind).GroupKind() +} + +// Resource takes an unqualified resource and returns a Group qualified GroupResource +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} + +// Adds the list of known types to the given scheme +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes( + SchemeGroupVersion, + &CAPApplication{}, + &CAPApplicationList{}, + ) + scheme.AddKnownTypes( + SchemeGroupVersion, + &CAPApplicationVersion{}, + &CAPApplicationVersionList{}, + ) + scheme.AddKnownTypes( + SchemeGroupVersion, + &CAPTenant{}, + &CAPTenantList{}, + ) + scheme.AddKnownTypes( + SchemeGroupVersion, + &CAPTenantOperation{}, + &CAPTenantOperationList{}, + ) + metaV1.AddToGroupVersion(scheme, SchemeGroupVersion) + return nil +} diff --git a/pkg/apis/sme.sap.com/v1alpha1/types.go b/pkg/apis/sme.sap.com/v1alpha1/types.go new file mode 100644 index 0000000..ab6e3fd --- /dev/null +++ b/pkg/apis/sme.sap.com/v1alpha1/types.go @@ -0,0 +1,475 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +package v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + Group = "sme.sap.com" + Version = "v1alpha1" + CAPApplicationKind = "CAPApplication" + CAPApplicationResource = "capapplications" + CAPApplicationVersionKind = "CAPApplicationVersion" + CAPApplicationVersionResource = "capapplicationversions" + CAPTenantKind = "CAPTenant" + CAPTenantResource = "captenants" + CAPTenantOperationKind = "CAPTenantOperation" + CAPTenantOperationResource = "captenantoperations" +) + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// CAPApplication is the schema for capapplications API +type CAPApplication struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata"` + // CAPApplication spec + Spec CAPApplicationSpec `json:"spec"` + // CAPApplication status + Status CAPApplicationStatus `json:"status"` +} + +type CAPApplicationStatus struct { + GenericStatus `json:",inline"` + // State of CAPApplication + State CAPApplicationState `json:"state"` + // Hash representing last known application domains + DomainSpecHash string `json:"domainSpecHash,omitempty"` + // The last time a full reconciliation was completed + LastFullReconciliationTime metav1.Time `json:"lastFullReconciliationTime,omitempty"` +} + +type CAPApplicationState string + +const ( + // CAPApplication is being reconciled + CAPApplicationStateProcessing CAPApplicationState = "Processing" + // An error occurred during reconciliation + CAPApplicationStateError CAPApplicationState = "Error" + // Deletion has been triggered + CAPApplicationStateDeleting CAPApplicationState = "Deleting" + // CAPApplication has been reconciled and is now consistent + CAPApplicationStateConsistent CAPApplicationState = "Consistent" +) + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// CAPApplicationList contains a list of CAPApplication +type CAPApplicationList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata"` + Items []CAPApplication `json:"items"` +} + +// CAPApplicationSpec defines the desired state of CAPApplication +type CAPApplicationSpec struct { + // Domains used by the application + Domains ApplicationDomains `json:"domains"` + // SAP BTP Global Account Identifier where services are entitles for the current application + GlobalAccountId string `json:"globalAccountId"` + // Short name for the application (similar to BTP XSAPPNAME) + BTPAppName string `json:"btpAppName"` + // Provider subaccount where application services are created + Provider BTPTenantIdentification `json:"provider"` + // SAP BTP Services consumed by the application + BTP BTP `json:"btp"` +} + +// Application domains +type ApplicationDomains struct { + // Primary application domain will be used to generate a wildcard TLS certificate. In SAP Gardener managed clusters this is (usually) a subdomain of the cluster domain + Primary string `json:"primary"` + // Customer specific domains to serve application endpoints (optional) + Secondary []string `json:"secondary,omitempty"` + // Public ingress URL for the cluster Load Balancer + DnsTarget string `json:"dnsTarget,omitempty"` + // Labels used to identify the istio ingress-gateway component and its corresponding namespace. Usually {"app":"istio-ingressgateway","istio":"ingressgateway"} + IstioIngressGatewayLabels []NameValue `json:"istioIngressGatewayLabels"` +} + +// Generic Name/Value configuration +type NameValue struct { + Name string `json:"name"` + Value string `json:"value"` +} + +// Identifies an SAP BTP subaccount (tenant) +type BTPTenantIdentification struct { + // BTP subaccount subdomain + SubDomain string `json:"subDomain"` + // BTP subaccount Tenant ID + TenantId string `json:"tenantId"` +} + +type BTP struct { + // Details of BTP Services + Services []ServiceInfo `json:"services"` +} + +// Service information +type ServiceInfo struct { + // Name of service instance + Name string `json:"name"` + // Secret containing service access credentials + Secret string `json:"secret"` + // Type of service + Class string `json:"class"` + // TODO: enhance this with params and other options --> Needed if/when we want to create the services via BTP/CF Operator +} + +// Custom resource status +type GenericStatus struct { + // Observed generation of the resource where this status was identified + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + // State expressed as conditions + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +type CAPApplicationStatusConditionType string + +const ( + ConditionTypeAllTenantsReady CAPApplicationStatusConditionType = "AllTenantsReady" + ConditionTypeLatestVersionReady CAPApplicationStatusConditionType = "LatestVersionReady" +) + +type StatusConditionType string + +const ( + ConditionTypeReady StatusConditionType = "Ready" +) + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// CAPApplicationVersion defines the schema for capapplicationversions API +type CAPApplicationVersion struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata"` + // CAPApplicationVersion spec + Spec CAPApplicationVersionSpec `json:"spec"` + // CAPApplicationVersion status + Status CAPApplicationVersionStatus `json:"status"` +} + +type CAPApplicationVersionStatus struct { + GenericStatus `json:",inline"` + // State of CAPApplicationVersion + State CAPApplicationVersionState `json:"state"` + // List of finished Content Jobs + FinishedJobs []string `json:"finishedJobs,omitempty"` +} + +type CAPApplicationVersionState string + +const ( + // CAPApplicationVersion is being processed + CAPApplicationVersionStateProcessing CAPApplicationVersionState = "Processing" + // An error occurred during reconciliation + CAPApplicationVersionStateError CAPApplicationVersionState = "Error" + // Deletion has been triggered + CAPApplicationVersionStateDeleting CAPApplicationVersionState = "Deleting" + // CAPApplicationVersion is now ready for use (dependent resources have been created) + CAPApplicationVersionStateReady CAPApplicationVersionState = "Ready" +) + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// CAPApplicationVersionList contains a list of CAPApplicationVersion +type CAPApplicationVersionList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata"` + Items []CAPApplicationVersion `json:"items"` +} + +// CAPApplicationVersionSpec specifies the desired state of CAPApplicationVersion +type CAPApplicationVersionSpec struct { + // Denotes to which CAPApplication the current version belongs + CAPApplicationInstance string `json:"capApplicationInstance"` + // Semantic version + Version string `json:"version"` + // Registry secrets used to pull images of the application components + RegistrySecrets []string `json:"registrySecrets"` + // Information about the Workloads + Workloads []WorkloadDetails `json:"workloads"` + // Tenant Operations may be used to specify how jobs are sequenced for the different tenant operations + TenantOperations *TenantOperations `json:"tenantOperations,omitempty"` +} + +// WorkloadDetails specifies the details of the Workload +type WorkloadDetails struct { + // Name of the workload + Name string `json:"name"` + // List of BTP services consumed by the current application component workload. These services must be defined in the corresponding CAPApplication. + ConsumedBTPServices []string `json:"consumedBTPServices"` + // Custom labels for the current workload + Labels map[string]string `json:"labels"` + // Annotations for the current workload, in case of `Deployments` this also get copied over to any `Service` that may be created + Annotations map[string]string `json:"annotations"` + // Definition of a deployment + DeploymentDefinition *DeploymentDetails `json:"deploymentDefinition,omitempty"` + // Definition of a job + JobDefinition *JobDetails `json:"jobDefinition,omitempty"` +} + +// DeploymentDetails specifies the details of the Deployment +type DeploymentDetails struct { + ContainerDetails `json:",inline"` + // Type of the Deployment + Type DeploymentType `json:"type"` + // Number of replicas + Replicas *int32 `json:"replicas,omitempty"` + // Port configuration + Ports []Ports `json:"ports,omitempty"` + // Liveness probe + LivenessProbe *corev1.Probe `json:"livenessProbe,omitempty"` + // Readiness probe + ReadinessProbe *corev1.Probe `json:"readinessProbe,omitempty"` +} + +// Type of deployment +type DeploymentType string + +const ( + // CAP backend server deployment type + DeploymentCAP DeploymentType = "CAP" + // Application router deployment type + DeploymentRouter DeploymentType = "Router" + // Additional deployment type + DeploymentAdditional DeploymentType = "Additional" +) + +// JobDetails specifies the details of the Job +type JobDetails struct { + ContainerDetails `json:",inline"` + // Type of Job + Type JobType `json:"type"` + // Specifies the number of retries before marking this job failed. + BackoffLimit *int32 `json:"backoffLimit,omitempty"` + // Specifies the time after which the job may be cleaned up. + TTLSecondsAfterFinished *int32 `json:"ttlSecondsAfterFinished,omitempty"` +} + +// Type of Job +type JobType string + +const ( + // job for deploying content or configuration to (BTP) services + JobContent JobType = "Content" + // job for tenant operation e.g. deploying relevant data to a tenant + JobTenantOperation JobType = "TenantOperation" + // job for custom tenant operation e.g. pre/post hooks for a tenant operation + JobCustomTenantOperation JobType = "CustomTenantOperation" +) + +// ContainerDetails specifies the details of the Container +type ContainerDetails struct { + // Image info for the container + Image string `json:"image"` + // Pull policy for the container image + ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy,omitempty"` + // Entrypoint array for the container + Command []string `json:"command,omitempty"` + // Environment Config for the Container + Env []corev1.EnvVar `json:"env,omitempty"` + // Resources + Resources corev1.ResourceRequirements `json:"resources,omitempty"` + // SecurityContext for the Container + SecurityContext *corev1.SecurityContext `json:"securityContext,omitempty"` + // SecurityContext for the Pod + PodSecurityContext *corev1.PodSecurityContext `json:"podSecurityContext,omitempty"` +} + +// Configuration of Service Ports for the deployment +type Ports struct { + // App protocol used by the service port + AppProtocol *string `json:"appProtocol,omitempty"` + // Name of the service port + Name string `json:"name"` + // Network Policy of the service port + NetworkPolicy PortNetworkPolicyType `json:"networkPolicy,omitempty"` + // The port number used for container and the corresponding service (if any) + Port int32 `json:"port"` + // Destination name which may be used by the Router deployment to reach this backend service + RouterDestinationName string `json:"routerDestinationName,omitempty"` +} + +// Type of NetworkPolicy for the port +type PortNetworkPolicyType string + +const ( + // Expose the port for the current application versions pod(s) scope + PortNetworkPolicyTypeApplication PortNetworkPolicyType = "Application" + // Expose the port for any pod(s) in the overall cluster scope + PortNetworkPolicyTypeCluster PortNetworkPolicyType = "Cluster" +) + +// Configuration used to sequence tenant related jobs for a given tenant operation +type TenantOperations struct { + // Tenant provisioning steps + Provisioning []TenantOperationWorkloadReference `json:"provisioning,omitempty"` + // Tenant upgrade steps + Upgrade []TenantOperationWorkloadReference `json:"upgrade,omitempty"` + // Tenant deprovisioning steps + Deprovisioning []TenantOperationWorkloadReference `json:"deprovisioning,omitempty"` +} + +type TenantOperationWorkloadReference struct { + // Reference to a specified workload of type 'TenantOperation' or 'CustomTenantOperation' + WorkloadName string `json:"workloadName"` + // Indicates whether to proceed with remaining operation steps in case of failure. Relevant only for 'CustomTenantOperation' + ContinueOnFailure bool `json:"continueOnFailure,omitempty"` +} + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// CAPTenant defines the schema for captenants API +type CAPTenant struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata"` + // CAPTenant spec + Spec CAPTenantSpec `json:"spec"` + // CAPTenant status + Status CAPTenantStatus `json:"status"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// CAPTenantList contains a list of CAPTenant +type CAPTenantList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata"` + Items []CAPTenant `json:"items"` +} + +type CAPTenantStatus struct { + GenericStatus `json:",inline"` + // State of CAPTenant + State CAPTenantState `json:"state"` + // Specifies the current version of the tenant after provisioning or upgrade + CurrentCAPApplicationVersionInstance string `json:"currentCAPApplicationVersionInstance,omitempty"` + // Previous versions of the tenant (first to last) + PreviousCAPApplicationVersions []string `json:"previousCAPApplicationVersions,omitempty"` + // The last time a full reconciliation was completed + LastFullReconciliationTime metav1.Time `json:"lastFullReconciliationTime,omitempty"` +} + +type CAPTenantState string + +const ( + // Tenant is being provisioned + CAPTenantStateProvisioning CAPTenantState = "Provisioning" + // Tenant provisioning ended in error + CAPTenantStateProvisioningError CAPTenantState = "ProvisioningError" + // Tenant is being upgraded + CAPTenantStateUpgrading CAPTenantState = "Upgrading" + // Tenant upgrade failed + CAPTenantStateUpgradeError CAPTenantState = "UpgradeError" + // Deletion has been triggered + CAPTenantStateDeleting CAPTenantState = "Deleting" + // Tenant has been provisioned/upgraded and is now ready for use + CAPTenantStateReady CAPTenantState = "Ready" +) + +// CAPTenantSpec defines the desired state of the CAPTenant +type CAPTenantSpec struct { + // Denotes to which CAPApplication the current tenant belongs + CAPApplicationInstance string `json:"capApplicationInstance"` + // Details of consumer sub-account subscribing to the application + BTPTenantIdentification `json:",inline"` + // Semver that is used to determine the relevant CAPApplicationVersion that a CAPTenant can be upgraded to (i.e. if it is not already on that version) + Version string `json:"version,omitempty"` + // Denotes whether a CAPTenant can be upgraded. One of ('always', 'never') + VersionUpgradeStrategy VersionUpgradeStrategyType `json:"versionUpgradeStrategy,omitempty"` +} + +type VersionUpgradeStrategyType string + +const ( + // Always (default) + VersionUpgradeStrategyTypeAlways VersionUpgradeStrategyType = "always" + // Never + VersionUpgradeStrategyTypeNever VersionUpgradeStrategyType = "never" +) + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// CAPTenantOperation defines the schema for captenantoperations API +type CAPTenantOperation struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata"` + // CAPTenantOperation spec + Spec CAPTenantOperationSpec `json:"spec"` + // CAPTenantOperation status + Status CAPTenantOperationStatus `json:"status"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// CAPTenantOperationList contains a list of CAPTenantOperation +type CAPTenantOperationList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata"` + Items []CAPTenantOperation `json:"items"` +} + +type CAPTenantOperationSpec struct { + // Scope of the tenant lifecycle operation. One of 'provisioning', 'deprovisioning' or 'upgrade' + Operation CAPTenantOperationType `json:"operation"` + // BTP sub-account (tenant) for which request is created + BTPTenantIdentification `json:",inline"` + // Reference to CAPApplicationVersion for executing the operation + CAPApplicationVersionInstance string `json:"capApplicationVersionInstance"` + // Steps (jobs) to be executed for the operation to complete + Steps []CAPTenantOperationStep `json:"steps"` +} + +type CAPTenantOperationStep struct { + // Name of the workload from the referenced CAPApplicationVersion + Name string `json:"name"` + // Type of job. One of 'TenantOperation' or 'CustomTenantOperation' + Type JobType `json:"type"` + // Indicates whether the operation can continue in case of step failure. Relevant only for type 'CustomTenantOperation' + ContinueOnFailure bool `json:"continueOnFailure,omitempty"` +} + +type CAPTenantOperationStatus struct { + GenericStatus `json:",inline"` + // State of CAPTenantOperation + State CAPTenantOperationState `json:"state"` + // Current step being processed from the sequence of specified steps + CurrentStep *uint32 `json:"currentStep,omitempty"` + // Name of the job being executed for the current step + ActiveJob *string `json:"activeJob,omitempty"` +} + +type CAPTenantOperationState string + +const ( + // CAPTenantOperation is being processed + CAPTenantOperationStateProcessing CAPTenantOperationState = "Processing" + // CAPTenantOperation steps have failed + CAPTenantOperationStateFailed CAPTenantOperationState = "Failed" + // CAPTenantOperation steps completed + CAPTenantOperationStateCompleted CAPTenantOperationState = "Completed" + // CAPTenantOperation deletion has been triggered + CAPTenantOperationStateDeleting CAPTenantOperationState = "Deleting" +) + +type CAPTenantOperationType string + +const ( + // Provision tenant + CAPTenantOperationTypeProvisioning CAPTenantOperationType = "provisioning" + // Deprovision tenant + CAPTenantOperationTypeDeprovisioning CAPTenantOperationType = "deprovisioning" + // Upgrade tenant + CAPTenantOperationTypeUpgrade CAPTenantOperationType = "upgrade" +) diff --git a/pkg/apis/sme.sap.com/v1alpha1/utils.go b/pkg/apis/sme.sap.com/v1alpha1/utils.go new file mode 100644 index 0000000..b4ad331 --- /dev/null +++ b/pkg/apis/sme.sap.com/v1alpha1/utils.go @@ -0,0 +1,93 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +package v1alpha1 + +import ( + "os" + "strconv" + + "golang.org/x/exp/slices" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + readyType = string(ConditionTypeReady) + EnvMaxTenantVersionHistory = "MAX_TENANT_VERSION_HISTORY" + defaultMaxTenantVersionHistory = 10 + minTenantVersionHistory = 3 +) + +func (ca *CAPApplication) SetStatusWithReadyCondition(state CAPApplicationState, readyStatus metav1.ConditionStatus, reason string, message string) { + ca.Status.State = state + ca.SetStatusCondition(readyType, readyStatus, reason, message) +} + +func (ca *CAPApplication) SetStatusDomainSpecHash(hash string) { + ca.Status.DomainSpecHash = hash +} + +// SetStatusCondition updates/sets the conditions in the Status of the resource. +func (ca *CAPApplication) SetStatusCondition(conditionType string, readyStatus metav1.ConditionStatus, reason string, message string) { + ca.Status.SetStatusCondition(metav1.Condition{Type: conditionType, Status: readyStatus, Reason: reason, Message: message, ObservedGeneration: ca.Generation}) +} + +func (cav *CAPApplicationVersion) SetStatusWithReadyCondition(state CAPApplicationVersionState, readyStatus metav1.ConditionStatus, reason string, message string) { + cav.Status.State = state + cav.Status.SetStatusCondition(metav1.Condition{Type: readyType, Status: readyStatus, Reason: reason, Message: message, ObservedGeneration: cav.Generation}) +} + +func (cav *CAPApplicationVersion) SetStatusFinishedJobs(finishedJob string) { + if !cav.CheckFinishedJobs(finishedJob) { + cav.Status.FinishedJobs = append(cav.Status.FinishedJobs, finishedJob) + } +} + +func (cav *CAPApplicationVersion) CheckFinishedJobs(finishedJob string) bool { + return slices.ContainsFunc(cav.Status.FinishedJobs, func(job string) bool { + return job == finishedJob + }) +} + +func (cat *CAPTenant) SetStatusWithReadyCondition(state CAPTenantState, readyStatus metav1.ConditionStatus, reason string, message string) { + cat.Status.State = state + cat.Status.SetStatusCondition(metav1.Condition{Type: readyType, Status: readyStatus, Reason: reason, Message: message, ObservedGeneration: cat.Generation}) +} + +func (cat *CAPTenant) SetStatusCAPApplicationVersion(cavName string) { + if cat.Status.CurrentCAPApplicationVersionInstance != "" { + if cat.Status.PreviousCAPApplicationVersions == nil { + cat.Status.PreviousCAPApplicationVersions = []string{} + } + cat.Status.PreviousCAPApplicationVersions = append(cat.Status.PreviousCAPApplicationVersions, cat.Status.CurrentCAPApplicationVersionInstance) + if len(cat.Status.PreviousCAPApplicationVersions) > minTenantVersionHistory { // clean up history only if it exceeds minimum + max := defaultMaxTenantVersionHistory + if sval, ok := os.LookupEnv(EnvMaxTenantVersionHistory); ok { + if i, err := strconv.ParseInt(sval, 10, 0); err == nil { + max = int(i) + } + } + if len(cat.Status.PreviousCAPApplicationVersions) > max { + cat.Status.PreviousCAPApplicationVersions = cat.Status.PreviousCAPApplicationVersions[1:] // remove one entry + } + } + } + cat.Status.CurrentCAPApplicationVersionInstance = cavName +} + +func (ctop *CAPTenantOperation) SetStatusWithReadyCondition(state CAPTenantOperationState, readyStatus metav1.ConditionStatus, reason string, message string) { + ctop.Status.State = state + ctop.Status.SetStatusCondition(metav1.Condition{Type: readyType, Status: readyStatus, Reason: reason, Message: message, ObservedGeneration: ctop.Generation}) +} + +func (ctop *CAPTenantOperation) SetStatusCurrentStep(step *uint32, job *string) { + ctop.Status.CurrentStep = step + ctop.Status.ActiveJob = job +} + +func (status *GenericStatus) SetStatusCondition(condition metav1.Condition) { + status.ObservedGeneration = condition.ObservedGeneration + meta.SetStatusCondition(&status.Conditions, condition) +} diff --git a/pkg/apis/sme.sap.com/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/sme.sap.com/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 0000000..aee5d8b --- /dev/null +++ b/pkg/apis/sme.sap.com/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,793 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by deepcopy-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ApplicationDomains) DeepCopyInto(out *ApplicationDomains) { + *out = *in + if in.Secondary != nil { + in, out := &in.Secondary, &out.Secondary + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.IstioIngressGatewayLabels != nil { + in, out := &in.IstioIngressGatewayLabels, &out.IstioIngressGatewayLabels + *out = make([]NameValue, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationDomains. +func (in *ApplicationDomains) DeepCopy() *ApplicationDomains { + if in == nil { + return nil + } + out := new(ApplicationDomains) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BTP) DeepCopyInto(out *BTP) { + *out = *in + if in.Services != nil { + in, out := &in.Services, &out.Services + *out = make([]ServiceInfo, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BTP. +func (in *BTP) DeepCopy() *BTP { + if in == nil { + return nil + } + out := new(BTP) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BTPTenantIdentification) DeepCopyInto(out *BTPTenantIdentification) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BTPTenantIdentification. +func (in *BTPTenantIdentification) DeepCopy() *BTPTenantIdentification { + if in == nil { + return nil + } + out := new(BTPTenantIdentification) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CAPApplication) DeepCopyInto(out *CAPApplication) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CAPApplication. +func (in *CAPApplication) DeepCopy() *CAPApplication { + if in == nil { + return nil + } + out := new(CAPApplication) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CAPApplication) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CAPApplicationList) DeepCopyInto(out *CAPApplicationList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]CAPApplication, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CAPApplicationList. +func (in *CAPApplicationList) DeepCopy() *CAPApplicationList { + if in == nil { + return nil + } + out := new(CAPApplicationList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CAPApplicationList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CAPApplicationSpec) DeepCopyInto(out *CAPApplicationSpec) { + *out = *in + in.Domains.DeepCopyInto(&out.Domains) + out.Provider = in.Provider + in.BTP.DeepCopyInto(&out.BTP) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CAPApplicationSpec. +func (in *CAPApplicationSpec) DeepCopy() *CAPApplicationSpec { + if in == nil { + return nil + } + out := new(CAPApplicationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CAPApplicationStatus) DeepCopyInto(out *CAPApplicationStatus) { + *out = *in + in.GenericStatus.DeepCopyInto(&out.GenericStatus) + in.LastFullReconciliationTime.DeepCopyInto(&out.LastFullReconciliationTime) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CAPApplicationStatus. +func (in *CAPApplicationStatus) DeepCopy() *CAPApplicationStatus { + if in == nil { + return nil + } + out := new(CAPApplicationStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CAPApplicationVersion) DeepCopyInto(out *CAPApplicationVersion) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CAPApplicationVersion. +func (in *CAPApplicationVersion) DeepCopy() *CAPApplicationVersion { + if in == nil { + return nil + } + out := new(CAPApplicationVersion) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CAPApplicationVersion) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CAPApplicationVersionList) DeepCopyInto(out *CAPApplicationVersionList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]CAPApplicationVersion, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CAPApplicationVersionList. +func (in *CAPApplicationVersionList) DeepCopy() *CAPApplicationVersionList { + if in == nil { + return nil + } + out := new(CAPApplicationVersionList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CAPApplicationVersionList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CAPApplicationVersionSpec) DeepCopyInto(out *CAPApplicationVersionSpec) { + *out = *in + if in.RegistrySecrets != nil { + in, out := &in.RegistrySecrets, &out.RegistrySecrets + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Workloads != nil { + in, out := &in.Workloads, &out.Workloads + *out = make([]WorkloadDetails, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.TenantOperations != nil { + in, out := &in.TenantOperations, &out.TenantOperations + *out = new(TenantOperations) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CAPApplicationVersionSpec. +func (in *CAPApplicationVersionSpec) DeepCopy() *CAPApplicationVersionSpec { + if in == nil { + return nil + } + out := new(CAPApplicationVersionSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CAPApplicationVersionStatus) DeepCopyInto(out *CAPApplicationVersionStatus) { + *out = *in + in.GenericStatus.DeepCopyInto(&out.GenericStatus) + if in.FinishedJobs != nil { + in, out := &in.FinishedJobs, &out.FinishedJobs + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CAPApplicationVersionStatus. +func (in *CAPApplicationVersionStatus) DeepCopy() *CAPApplicationVersionStatus { + if in == nil { + return nil + } + out := new(CAPApplicationVersionStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CAPTenant) DeepCopyInto(out *CAPTenant) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CAPTenant. +func (in *CAPTenant) DeepCopy() *CAPTenant { + if in == nil { + return nil + } + out := new(CAPTenant) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CAPTenant) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CAPTenantList) DeepCopyInto(out *CAPTenantList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]CAPTenant, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CAPTenantList. +func (in *CAPTenantList) DeepCopy() *CAPTenantList { + if in == nil { + return nil + } + out := new(CAPTenantList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CAPTenantList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CAPTenantOperation) DeepCopyInto(out *CAPTenantOperation) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CAPTenantOperation. +func (in *CAPTenantOperation) DeepCopy() *CAPTenantOperation { + if in == nil { + return nil + } + out := new(CAPTenantOperation) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CAPTenantOperation) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CAPTenantOperationList) DeepCopyInto(out *CAPTenantOperationList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]CAPTenantOperation, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CAPTenantOperationList. +func (in *CAPTenantOperationList) DeepCopy() *CAPTenantOperationList { + if in == nil { + return nil + } + out := new(CAPTenantOperationList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CAPTenantOperationList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CAPTenantOperationSpec) DeepCopyInto(out *CAPTenantOperationSpec) { + *out = *in + out.BTPTenantIdentification = in.BTPTenantIdentification + if in.Steps != nil { + in, out := &in.Steps, &out.Steps + *out = make([]CAPTenantOperationStep, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CAPTenantOperationSpec. +func (in *CAPTenantOperationSpec) DeepCopy() *CAPTenantOperationSpec { + if in == nil { + return nil + } + out := new(CAPTenantOperationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CAPTenantOperationStatus) DeepCopyInto(out *CAPTenantOperationStatus) { + *out = *in + in.GenericStatus.DeepCopyInto(&out.GenericStatus) + if in.CurrentStep != nil { + in, out := &in.CurrentStep, &out.CurrentStep + *out = new(uint32) + **out = **in + } + if in.ActiveJob != nil { + in, out := &in.ActiveJob, &out.ActiveJob + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CAPTenantOperationStatus. +func (in *CAPTenantOperationStatus) DeepCopy() *CAPTenantOperationStatus { + if in == nil { + return nil + } + out := new(CAPTenantOperationStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CAPTenantOperationStep) DeepCopyInto(out *CAPTenantOperationStep) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CAPTenantOperationStep. +func (in *CAPTenantOperationStep) DeepCopy() *CAPTenantOperationStep { + if in == nil { + return nil + } + out := new(CAPTenantOperationStep) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CAPTenantSpec) DeepCopyInto(out *CAPTenantSpec) { + *out = *in + out.BTPTenantIdentification = in.BTPTenantIdentification + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CAPTenantSpec. +func (in *CAPTenantSpec) DeepCopy() *CAPTenantSpec { + if in == nil { + return nil + } + out := new(CAPTenantSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CAPTenantStatus) DeepCopyInto(out *CAPTenantStatus) { + *out = *in + in.GenericStatus.DeepCopyInto(&out.GenericStatus) + if in.PreviousCAPApplicationVersions != nil { + in, out := &in.PreviousCAPApplicationVersions, &out.PreviousCAPApplicationVersions + *out = make([]string, len(*in)) + copy(*out, *in) + } + in.LastFullReconciliationTime.DeepCopyInto(&out.LastFullReconciliationTime) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CAPTenantStatus. +func (in *CAPTenantStatus) DeepCopy() *CAPTenantStatus { + if in == nil { + return nil + } + out := new(CAPTenantStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ContainerDetails) DeepCopyInto(out *ContainerDetails) { + *out = *in + if in.Command != nil { + in, out := &in.Command, &out.Command + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Env != nil { + in, out := &in.Env, &out.Env + *out = make([]v1.EnvVar, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + in.Resources.DeepCopyInto(&out.Resources) + if in.SecurityContext != nil { + in, out := &in.SecurityContext, &out.SecurityContext + *out = new(v1.SecurityContext) + (*in).DeepCopyInto(*out) + } + if in.PodSecurityContext != nil { + in, out := &in.PodSecurityContext, &out.PodSecurityContext + *out = new(v1.PodSecurityContext) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ContainerDetails. +func (in *ContainerDetails) DeepCopy() *ContainerDetails { + if in == nil { + return nil + } + out := new(ContainerDetails) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeploymentDetails) DeepCopyInto(out *DeploymentDetails) { + *out = *in + in.ContainerDetails.DeepCopyInto(&out.ContainerDetails) + if in.Replicas != nil { + in, out := &in.Replicas, &out.Replicas + *out = new(int32) + **out = **in + } + if in.Ports != nil { + in, out := &in.Ports, &out.Ports + *out = make([]Ports, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.LivenessProbe != nil { + in, out := &in.LivenessProbe, &out.LivenessProbe + *out = new(v1.Probe) + (*in).DeepCopyInto(*out) + } + if in.ReadinessProbe != nil { + in, out := &in.ReadinessProbe, &out.ReadinessProbe + *out = new(v1.Probe) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeploymentDetails. +func (in *DeploymentDetails) DeepCopy() *DeploymentDetails { + if in == nil { + return nil + } + out := new(DeploymentDetails) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GenericStatus) DeepCopyInto(out *GenericStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GenericStatus. +func (in *GenericStatus) DeepCopy() *GenericStatus { + if in == nil { + return nil + } + out := new(GenericStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *JobDetails) DeepCopyInto(out *JobDetails) { + *out = *in + in.ContainerDetails.DeepCopyInto(&out.ContainerDetails) + if in.BackoffLimit != nil { + in, out := &in.BackoffLimit, &out.BackoffLimit + *out = new(int32) + **out = **in + } + if in.TTLSecondsAfterFinished != nil { + in, out := &in.TTLSecondsAfterFinished, &out.TTLSecondsAfterFinished + *out = new(int32) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JobDetails. +func (in *JobDetails) DeepCopy() *JobDetails { + if in == nil { + return nil + } + out := new(JobDetails) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NameValue) DeepCopyInto(out *NameValue) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NameValue. +func (in *NameValue) DeepCopy() *NameValue { + if in == nil { + return nil + } + out := new(NameValue) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Ports) DeepCopyInto(out *Ports) { + *out = *in + if in.AppProtocol != nil { + in, out := &in.AppProtocol, &out.AppProtocol + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Ports. +func (in *Ports) DeepCopy() *Ports { + if in == nil { + return nil + } + out := new(Ports) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceInfo) DeepCopyInto(out *ServiceInfo) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceInfo. +func (in *ServiceInfo) DeepCopy() *ServiceInfo { + if in == nil { + return nil + } + out := new(ServiceInfo) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TenantOperationWorkloadReference) DeepCopyInto(out *TenantOperationWorkloadReference) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantOperationWorkloadReference. +func (in *TenantOperationWorkloadReference) DeepCopy() *TenantOperationWorkloadReference { + if in == nil { + return nil + } + out := new(TenantOperationWorkloadReference) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TenantOperations) DeepCopyInto(out *TenantOperations) { + *out = *in + if in.Provisioning != nil { + in, out := &in.Provisioning, &out.Provisioning + *out = make([]TenantOperationWorkloadReference, len(*in)) + copy(*out, *in) + } + if in.Upgrade != nil { + in, out := &in.Upgrade, &out.Upgrade + *out = make([]TenantOperationWorkloadReference, len(*in)) + copy(*out, *in) + } + if in.Deprovisioning != nil { + in, out := &in.Deprovisioning, &out.Deprovisioning + *out = make([]TenantOperationWorkloadReference, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantOperations. +func (in *TenantOperations) DeepCopy() *TenantOperations { + if in == nil { + return nil + } + out := new(TenantOperations) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WorkloadDetails) DeepCopyInto(out *WorkloadDetails) { + *out = *in + if in.ConsumedBTPServices != nil { + in, out := &in.ConsumedBTPServices, &out.ConsumedBTPServices + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.DeploymentDefinition != nil { + in, out := &in.DeploymentDefinition, &out.DeploymentDefinition + *out = new(DeploymentDetails) + (*in).DeepCopyInto(*out) + } + if in.JobDefinition != nil { + in, out := &in.JobDefinition, &out.JobDefinition + *out = new(JobDetails) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkloadDetails. +func (in *WorkloadDetails) DeepCopy() *WorkloadDetails { + if in == nil { + return nil + } + out := new(WorkloadDetails) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/client/applyconfiguration/internal/internal.go b/pkg/client/applyconfiguration/internal/internal.go new file mode 100644 index 0000000..b2ce045 --- /dev/null +++ b/pkg/client/applyconfiguration/internal/internal.go @@ -0,0 +1,50 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package internal + +import ( + "fmt" + "sync" + + typed "sigs.k8s.io/structured-merge-diff/v4/typed" +) + +func Parser() *typed.Parser { + parserOnce.Do(func() { + var err error + parser, err = typed.NewParser(schemaYAML) + if err != nil { + panic(fmt.Sprintf("Failed to parse schema: %v", err)) + } + }) + return parser +} + +var parserOnce sync.Once +var parser *typed.Parser +var schemaYAML = typed.YAMLObject(`types: +- name: __untyped_atomic_ + scalar: untyped + list: + elementType: + namedType: __untyped_atomic_ + elementRelationship: atomic + map: + elementType: + namedType: __untyped_atomic_ + elementRelationship: atomic +- name: __untyped_deduced_ + scalar: untyped + list: + elementType: + namedType: __untyped_atomic_ + elementRelationship: atomic + map: + elementType: + namedType: __untyped_deduced_ + elementRelationship: separable +`) diff --git a/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/applicationdomains.go b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/applicationdomains.go new file mode 100644 index 0000000..25d83b1 --- /dev/null +++ b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/applicationdomains.go @@ -0,0 +1,61 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +// ApplicationDomainsApplyConfiguration represents an declarative configuration of the ApplicationDomains type for use +// with apply. +type ApplicationDomainsApplyConfiguration struct { + Primary *string `json:"primary,omitempty"` + Secondary []string `json:"secondary,omitempty"` + DnsTarget *string `json:"dnsTarget,omitempty"` + IstioIngressGatewayLabels []NameValueApplyConfiguration `json:"istioIngressGatewayLabels,omitempty"` +} + +// ApplicationDomainsApplyConfiguration constructs an declarative configuration of the ApplicationDomains type for use with +// apply. +func ApplicationDomains() *ApplicationDomainsApplyConfiguration { + return &ApplicationDomainsApplyConfiguration{} +} + +// WithPrimary sets the Primary field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Primary field is set to the value of the last call. +func (b *ApplicationDomainsApplyConfiguration) WithPrimary(value string) *ApplicationDomainsApplyConfiguration { + b.Primary = &value + return b +} + +// WithSecondary adds the given value to the Secondary field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Secondary field. +func (b *ApplicationDomainsApplyConfiguration) WithSecondary(values ...string) *ApplicationDomainsApplyConfiguration { + for i := range values { + b.Secondary = append(b.Secondary, values[i]) + } + return b +} + +// WithDnsTarget sets the DnsTarget field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DnsTarget field is set to the value of the last call. +func (b *ApplicationDomainsApplyConfiguration) WithDnsTarget(value string) *ApplicationDomainsApplyConfiguration { + b.DnsTarget = &value + return b +} + +// WithIstioIngressGatewayLabels adds the given value to the IstioIngressGatewayLabels field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the IstioIngressGatewayLabels field. +func (b *ApplicationDomainsApplyConfiguration) WithIstioIngressGatewayLabels(values ...*NameValueApplyConfiguration) *ApplicationDomainsApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithIstioIngressGatewayLabels") + } + b.IstioIngressGatewayLabels = append(b.IstioIngressGatewayLabels, *values[i]) + } + return b +} diff --git a/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/btp.go b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/btp.go new file mode 100644 index 0000000..e8ccad7 --- /dev/null +++ b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/btp.go @@ -0,0 +1,32 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +// BTPApplyConfiguration represents an declarative configuration of the BTP type for use +// with apply. +type BTPApplyConfiguration struct { + Services []ServiceInfoApplyConfiguration `json:"services,omitempty"` +} + +// BTPApplyConfiguration constructs an declarative configuration of the BTP type for use with +// apply. +func BTP() *BTPApplyConfiguration { + return &BTPApplyConfiguration{} +} + +// WithServices adds the given value to the Services field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Services field. +func (b *BTPApplyConfiguration) WithServices(values ...*ServiceInfoApplyConfiguration) *BTPApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithServices") + } + b.Services = append(b.Services, *values[i]) + } + return b +} diff --git a/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/btptenantidentification.go b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/btptenantidentification.go new file mode 100644 index 0000000..911a0a8 --- /dev/null +++ b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/btptenantidentification.go @@ -0,0 +1,36 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +// BTPTenantIdentificationApplyConfiguration represents an declarative configuration of the BTPTenantIdentification type for use +// with apply. +type BTPTenantIdentificationApplyConfiguration struct { + SubDomain *string `json:"subDomain,omitempty"` + TenantId *string `json:"tenantId,omitempty"` +} + +// BTPTenantIdentificationApplyConfiguration constructs an declarative configuration of the BTPTenantIdentification type for use with +// apply. +func BTPTenantIdentification() *BTPTenantIdentificationApplyConfiguration { + return &BTPTenantIdentificationApplyConfiguration{} +} + +// WithSubDomain sets the SubDomain field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the SubDomain field is set to the value of the last call. +func (b *BTPTenantIdentificationApplyConfiguration) WithSubDomain(value string) *BTPTenantIdentificationApplyConfiguration { + b.SubDomain = &value + return b +} + +// WithTenantId sets the TenantId field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the TenantId field is set to the value of the last call. +func (b *BTPTenantIdentificationApplyConfiguration) WithTenantId(value string) *BTPTenantIdentificationApplyConfiguration { + b.TenantId = &value + return b +} diff --git a/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/capapplication.go b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/capapplication.go new file mode 100644 index 0000000..a4146c5 --- /dev/null +++ b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/capapplication.go @@ -0,0 +1,207 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + v1 "k8s.io/client-go/applyconfigurations/meta/v1" +) + +// CAPApplicationApplyConfiguration represents an declarative configuration of the CAPApplication type for use +// with apply. +type CAPApplicationApplyConfiguration struct { + v1.TypeMetaApplyConfiguration `json:",inline"` + *v1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"` + Spec *CAPApplicationSpecApplyConfiguration `json:"spec,omitempty"` + Status *CAPApplicationStatusApplyConfiguration `json:"status,omitempty"` +} + +// CAPApplication constructs an declarative configuration of the CAPApplication type for use with +// apply. +func CAPApplication(name, namespace string) *CAPApplicationApplyConfiguration { + b := &CAPApplicationApplyConfiguration{} + b.WithName(name) + b.WithNamespace(namespace) + b.WithKind("CAPApplication") + b.WithAPIVersion("sme.sap.com/v1alpha1") + return b +} + +// WithKind sets the Kind field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Kind field is set to the value of the last call. +func (b *CAPApplicationApplyConfiguration) WithKind(value string) *CAPApplicationApplyConfiguration { + b.Kind = &value + return b +} + +// WithAPIVersion sets the APIVersion field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the APIVersion field is set to the value of the last call. +func (b *CAPApplicationApplyConfiguration) WithAPIVersion(value string) *CAPApplicationApplyConfiguration { + b.APIVersion = &value + return b +} + +// WithName sets the Name field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Name field is set to the value of the last call. +func (b *CAPApplicationApplyConfiguration) WithName(value string) *CAPApplicationApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.Name = &value + return b +} + +// WithGenerateName sets the GenerateName field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the GenerateName field is set to the value of the last call. +func (b *CAPApplicationApplyConfiguration) WithGenerateName(value string) *CAPApplicationApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.GenerateName = &value + return b +} + +// WithNamespace sets the Namespace field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Namespace field is set to the value of the last call. +func (b *CAPApplicationApplyConfiguration) WithNamespace(value string) *CAPApplicationApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.Namespace = &value + return b +} + +// WithUID sets the UID field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the UID field is set to the value of the last call. +func (b *CAPApplicationApplyConfiguration) WithUID(value types.UID) *CAPApplicationApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.UID = &value + return b +} + +// WithResourceVersion sets the ResourceVersion field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ResourceVersion field is set to the value of the last call. +func (b *CAPApplicationApplyConfiguration) WithResourceVersion(value string) *CAPApplicationApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ResourceVersion = &value + return b +} + +// WithGeneration sets the Generation field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Generation field is set to the value of the last call. +func (b *CAPApplicationApplyConfiguration) WithGeneration(value int64) *CAPApplicationApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.Generation = &value + return b +} + +// WithCreationTimestamp sets the CreationTimestamp field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the CreationTimestamp field is set to the value of the last call. +func (b *CAPApplicationApplyConfiguration) WithCreationTimestamp(value metav1.Time) *CAPApplicationApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.CreationTimestamp = &value + return b +} + +// WithDeletionTimestamp sets the DeletionTimestamp field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DeletionTimestamp field is set to the value of the last call. +func (b *CAPApplicationApplyConfiguration) WithDeletionTimestamp(value metav1.Time) *CAPApplicationApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.DeletionTimestamp = &value + return b +} + +// WithDeletionGracePeriodSeconds sets the DeletionGracePeriodSeconds field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DeletionGracePeriodSeconds field is set to the value of the last call. +func (b *CAPApplicationApplyConfiguration) WithDeletionGracePeriodSeconds(value int64) *CAPApplicationApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.DeletionGracePeriodSeconds = &value + return b +} + +// WithLabels puts the entries into the Labels field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Labels field, +// overwriting an existing map entries in Labels field with the same key. +func (b *CAPApplicationApplyConfiguration) WithLabels(entries map[string]string) *CAPApplicationApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + if b.Labels == nil && len(entries) > 0 { + b.Labels = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.Labels[k] = v + } + return b +} + +// WithAnnotations puts the entries into the Annotations field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Annotations field, +// overwriting an existing map entries in Annotations field with the same key. +func (b *CAPApplicationApplyConfiguration) WithAnnotations(entries map[string]string) *CAPApplicationApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + if b.Annotations == nil && len(entries) > 0 { + b.Annotations = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.Annotations[k] = v + } + return b +} + +// WithOwnerReferences adds the given value to the OwnerReferences field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the OwnerReferences field. +func (b *CAPApplicationApplyConfiguration) WithOwnerReferences(values ...*v1.OwnerReferenceApplyConfiguration) *CAPApplicationApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + if values[i] == nil { + panic("nil value passed to WithOwnerReferences") + } + b.OwnerReferences = append(b.OwnerReferences, *values[i]) + } + return b +} + +// WithFinalizers adds the given value to the Finalizers field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Finalizers field. +func (b *CAPApplicationApplyConfiguration) WithFinalizers(values ...string) *CAPApplicationApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + b.Finalizers = append(b.Finalizers, values[i]) + } + return b +} + +func (b *CAPApplicationApplyConfiguration) ensureObjectMetaApplyConfigurationExists() { + if b.ObjectMetaApplyConfiguration == nil { + b.ObjectMetaApplyConfiguration = &v1.ObjectMetaApplyConfiguration{} + } +} + +// WithSpec sets the Spec field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Spec field is set to the value of the last call. +func (b *CAPApplicationApplyConfiguration) WithSpec(value *CAPApplicationSpecApplyConfiguration) *CAPApplicationApplyConfiguration { + b.Spec = value + return b +} + +// WithStatus sets the Status field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Status field is set to the value of the last call. +func (b *CAPApplicationApplyConfiguration) WithStatus(value *CAPApplicationStatusApplyConfiguration) *CAPApplicationApplyConfiguration { + b.Status = value + return b +} diff --git a/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/capapplicationspec.go b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/capapplicationspec.go new file mode 100644 index 0000000..6c4fc60 --- /dev/null +++ b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/capapplicationspec.go @@ -0,0 +1,63 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +// CAPApplicationSpecApplyConfiguration represents an declarative configuration of the CAPApplicationSpec type for use +// with apply. +type CAPApplicationSpecApplyConfiguration struct { + Domains *ApplicationDomainsApplyConfiguration `json:"domains,omitempty"` + GlobalAccountId *string `json:"globalAccountId,omitempty"` + BTPAppName *string `json:"btpAppName,omitempty"` + Provider *BTPTenantIdentificationApplyConfiguration `json:"provider,omitempty"` + BTP *BTPApplyConfiguration `json:"btp,omitempty"` +} + +// CAPApplicationSpecApplyConfiguration constructs an declarative configuration of the CAPApplicationSpec type for use with +// apply. +func CAPApplicationSpec() *CAPApplicationSpecApplyConfiguration { + return &CAPApplicationSpecApplyConfiguration{} +} + +// WithDomains sets the Domains field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Domains field is set to the value of the last call. +func (b *CAPApplicationSpecApplyConfiguration) WithDomains(value *ApplicationDomainsApplyConfiguration) *CAPApplicationSpecApplyConfiguration { + b.Domains = value + return b +} + +// WithGlobalAccountId sets the GlobalAccountId field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the GlobalAccountId field is set to the value of the last call. +func (b *CAPApplicationSpecApplyConfiguration) WithGlobalAccountId(value string) *CAPApplicationSpecApplyConfiguration { + b.GlobalAccountId = &value + return b +} + +// WithBTPAppName sets the BTPAppName field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the BTPAppName field is set to the value of the last call. +func (b *CAPApplicationSpecApplyConfiguration) WithBTPAppName(value string) *CAPApplicationSpecApplyConfiguration { + b.BTPAppName = &value + return b +} + +// WithProvider sets the Provider field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Provider field is set to the value of the last call. +func (b *CAPApplicationSpecApplyConfiguration) WithProvider(value *BTPTenantIdentificationApplyConfiguration) *CAPApplicationSpecApplyConfiguration { + b.Provider = value + return b +} + +// WithBTP sets the BTP field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the BTP field is set to the value of the last call. +func (b *CAPApplicationSpecApplyConfiguration) WithBTP(value *BTPApplyConfiguration) *CAPApplicationSpecApplyConfiguration { + b.BTP = value + return b +} diff --git a/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/capapplicationstatus.go b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/capapplicationstatus.go new file mode 100644 index 0000000..1548543 --- /dev/null +++ b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/capapplicationstatus.go @@ -0,0 +1,69 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + smesapcomv1alpha1 "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// CAPApplicationStatusApplyConfiguration represents an declarative configuration of the CAPApplicationStatus type for use +// with apply. +type CAPApplicationStatusApplyConfiguration struct { + GenericStatusApplyConfiguration `json:",inline"` + State *smesapcomv1alpha1.CAPApplicationState `json:"state,omitempty"` + DomainSpecHash *string `json:"domainSpecHash,omitempty"` + LastFullReconciliationTime *v1.Time `json:"lastFullReconciliationTime,omitempty"` +} + +// CAPApplicationStatusApplyConfiguration constructs an declarative configuration of the CAPApplicationStatus type for use with +// apply. +func CAPApplicationStatus() *CAPApplicationStatusApplyConfiguration { + return &CAPApplicationStatusApplyConfiguration{} +} + +// WithObservedGeneration sets the ObservedGeneration field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ObservedGeneration field is set to the value of the last call. +func (b *CAPApplicationStatusApplyConfiguration) WithObservedGeneration(value int64) *CAPApplicationStatusApplyConfiguration { + b.ObservedGeneration = &value + return b +} + +// WithConditions adds the given value to the Conditions field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Conditions field. +func (b *CAPApplicationStatusApplyConfiguration) WithConditions(values ...v1.Condition) *CAPApplicationStatusApplyConfiguration { + for i := range values { + b.Conditions = append(b.Conditions, values[i]) + } + return b +} + +// WithState sets the State field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the State field is set to the value of the last call. +func (b *CAPApplicationStatusApplyConfiguration) WithState(value smesapcomv1alpha1.CAPApplicationState) *CAPApplicationStatusApplyConfiguration { + b.State = &value + return b +} + +// WithDomainSpecHash sets the DomainSpecHash field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DomainSpecHash field is set to the value of the last call. +func (b *CAPApplicationStatusApplyConfiguration) WithDomainSpecHash(value string) *CAPApplicationStatusApplyConfiguration { + b.DomainSpecHash = &value + return b +} + +// WithLastFullReconciliationTime sets the LastFullReconciliationTime field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the LastFullReconciliationTime field is set to the value of the last call. +func (b *CAPApplicationStatusApplyConfiguration) WithLastFullReconciliationTime(value v1.Time) *CAPApplicationStatusApplyConfiguration { + b.LastFullReconciliationTime = &value + return b +} diff --git a/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/capapplicationversion.go b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/capapplicationversion.go new file mode 100644 index 0000000..b71be0f --- /dev/null +++ b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/capapplicationversion.go @@ -0,0 +1,207 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + v1 "k8s.io/client-go/applyconfigurations/meta/v1" +) + +// CAPApplicationVersionApplyConfiguration represents an declarative configuration of the CAPApplicationVersion type for use +// with apply. +type CAPApplicationVersionApplyConfiguration struct { + v1.TypeMetaApplyConfiguration `json:",inline"` + *v1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"` + Spec *CAPApplicationVersionSpecApplyConfiguration `json:"spec,omitempty"` + Status *CAPApplicationVersionStatusApplyConfiguration `json:"status,omitempty"` +} + +// CAPApplicationVersion constructs an declarative configuration of the CAPApplicationVersion type for use with +// apply. +func CAPApplicationVersion(name, namespace string) *CAPApplicationVersionApplyConfiguration { + b := &CAPApplicationVersionApplyConfiguration{} + b.WithName(name) + b.WithNamespace(namespace) + b.WithKind("CAPApplicationVersion") + b.WithAPIVersion("sme.sap.com/v1alpha1") + return b +} + +// WithKind sets the Kind field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Kind field is set to the value of the last call. +func (b *CAPApplicationVersionApplyConfiguration) WithKind(value string) *CAPApplicationVersionApplyConfiguration { + b.Kind = &value + return b +} + +// WithAPIVersion sets the APIVersion field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the APIVersion field is set to the value of the last call. +func (b *CAPApplicationVersionApplyConfiguration) WithAPIVersion(value string) *CAPApplicationVersionApplyConfiguration { + b.APIVersion = &value + return b +} + +// WithName sets the Name field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Name field is set to the value of the last call. +func (b *CAPApplicationVersionApplyConfiguration) WithName(value string) *CAPApplicationVersionApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.Name = &value + return b +} + +// WithGenerateName sets the GenerateName field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the GenerateName field is set to the value of the last call. +func (b *CAPApplicationVersionApplyConfiguration) WithGenerateName(value string) *CAPApplicationVersionApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.GenerateName = &value + return b +} + +// WithNamespace sets the Namespace field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Namespace field is set to the value of the last call. +func (b *CAPApplicationVersionApplyConfiguration) WithNamespace(value string) *CAPApplicationVersionApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.Namespace = &value + return b +} + +// WithUID sets the UID field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the UID field is set to the value of the last call. +func (b *CAPApplicationVersionApplyConfiguration) WithUID(value types.UID) *CAPApplicationVersionApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.UID = &value + return b +} + +// WithResourceVersion sets the ResourceVersion field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ResourceVersion field is set to the value of the last call. +func (b *CAPApplicationVersionApplyConfiguration) WithResourceVersion(value string) *CAPApplicationVersionApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ResourceVersion = &value + return b +} + +// WithGeneration sets the Generation field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Generation field is set to the value of the last call. +func (b *CAPApplicationVersionApplyConfiguration) WithGeneration(value int64) *CAPApplicationVersionApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.Generation = &value + return b +} + +// WithCreationTimestamp sets the CreationTimestamp field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the CreationTimestamp field is set to the value of the last call. +func (b *CAPApplicationVersionApplyConfiguration) WithCreationTimestamp(value metav1.Time) *CAPApplicationVersionApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.CreationTimestamp = &value + return b +} + +// WithDeletionTimestamp sets the DeletionTimestamp field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DeletionTimestamp field is set to the value of the last call. +func (b *CAPApplicationVersionApplyConfiguration) WithDeletionTimestamp(value metav1.Time) *CAPApplicationVersionApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.DeletionTimestamp = &value + return b +} + +// WithDeletionGracePeriodSeconds sets the DeletionGracePeriodSeconds field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DeletionGracePeriodSeconds field is set to the value of the last call. +func (b *CAPApplicationVersionApplyConfiguration) WithDeletionGracePeriodSeconds(value int64) *CAPApplicationVersionApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.DeletionGracePeriodSeconds = &value + return b +} + +// WithLabels puts the entries into the Labels field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Labels field, +// overwriting an existing map entries in Labels field with the same key. +func (b *CAPApplicationVersionApplyConfiguration) WithLabels(entries map[string]string) *CAPApplicationVersionApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + if b.Labels == nil && len(entries) > 0 { + b.Labels = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.Labels[k] = v + } + return b +} + +// WithAnnotations puts the entries into the Annotations field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Annotations field, +// overwriting an existing map entries in Annotations field with the same key. +func (b *CAPApplicationVersionApplyConfiguration) WithAnnotations(entries map[string]string) *CAPApplicationVersionApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + if b.Annotations == nil && len(entries) > 0 { + b.Annotations = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.Annotations[k] = v + } + return b +} + +// WithOwnerReferences adds the given value to the OwnerReferences field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the OwnerReferences field. +func (b *CAPApplicationVersionApplyConfiguration) WithOwnerReferences(values ...*v1.OwnerReferenceApplyConfiguration) *CAPApplicationVersionApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + if values[i] == nil { + panic("nil value passed to WithOwnerReferences") + } + b.OwnerReferences = append(b.OwnerReferences, *values[i]) + } + return b +} + +// WithFinalizers adds the given value to the Finalizers field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Finalizers field. +func (b *CAPApplicationVersionApplyConfiguration) WithFinalizers(values ...string) *CAPApplicationVersionApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + b.Finalizers = append(b.Finalizers, values[i]) + } + return b +} + +func (b *CAPApplicationVersionApplyConfiguration) ensureObjectMetaApplyConfigurationExists() { + if b.ObjectMetaApplyConfiguration == nil { + b.ObjectMetaApplyConfiguration = &v1.ObjectMetaApplyConfiguration{} + } +} + +// WithSpec sets the Spec field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Spec field is set to the value of the last call. +func (b *CAPApplicationVersionApplyConfiguration) WithSpec(value *CAPApplicationVersionSpecApplyConfiguration) *CAPApplicationVersionApplyConfiguration { + b.Spec = value + return b +} + +// WithStatus sets the Status field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Status field is set to the value of the last call. +func (b *CAPApplicationVersionApplyConfiguration) WithStatus(value *CAPApplicationVersionStatusApplyConfiguration) *CAPApplicationVersionApplyConfiguration { + b.Status = value + return b +} diff --git a/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/capapplicationversionspec.go b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/capapplicationversionspec.go new file mode 100644 index 0000000..0a30c32 --- /dev/null +++ b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/capapplicationversionspec.go @@ -0,0 +1,70 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +// CAPApplicationVersionSpecApplyConfiguration represents an declarative configuration of the CAPApplicationVersionSpec type for use +// with apply. +type CAPApplicationVersionSpecApplyConfiguration struct { + CAPApplicationInstance *string `json:"capApplicationInstance,omitempty"` + Version *string `json:"version,omitempty"` + RegistrySecrets []string `json:"registrySecrets,omitempty"` + Workloads []WorkloadDetailsApplyConfiguration `json:"workloads,omitempty"` + TenantOperations *TenantOperationsApplyConfiguration `json:"tenantOperations,omitempty"` +} + +// CAPApplicationVersionSpecApplyConfiguration constructs an declarative configuration of the CAPApplicationVersionSpec type for use with +// apply. +func CAPApplicationVersionSpec() *CAPApplicationVersionSpecApplyConfiguration { + return &CAPApplicationVersionSpecApplyConfiguration{} +} + +// WithCAPApplicationInstance sets the CAPApplicationInstance field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the CAPApplicationInstance field is set to the value of the last call. +func (b *CAPApplicationVersionSpecApplyConfiguration) WithCAPApplicationInstance(value string) *CAPApplicationVersionSpecApplyConfiguration { + b.CAPApplicationInstance = &value + return b +} + +// WithVersion sets the Version field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Version field is set to the value of the last call. +func (b *CAPApplicationVersionSpecApplyConfiguration) WithVersion(value string) *CAPApplicationVersionSpecApplyConfiguration { + b.Version = &value + return b +} + +// WithRegistrySecrets adds the given value to the RegistrySecrets field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the RegistrySecrets field. +func (b *CAPApplicationVersionSpecApplyConfiguration) WithRegistrySecrets(values ...string) *CAPApplicationVersionSpecApplyConfiguration { + for i := range values { + b.RegistrySecrets = append(b.RegistrySecrets, values[i]) + } + return b +} + +// WithWorkloads adds the given value to the Workloads field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Workloads field. +func (b *CAPApplicationVersionSpecApplyConfiguration) WithWorkloads(values ...*WorkloadDetailsApplyConfiguration) *CAPApplicationVersionSpecApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithWorkloads") + } + b.Workloads = append(b.Workloads, *values[i]) + } + return b +} + +// WithTenantOperations sets the TenantOperations field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the TenantOperations field is set to the value of the last call. +func (b *CAPApplicationVersionSpecApplyConfiguration) WithTenantOperations(value *TenantOperationsApplyConfiguration) *CAPApplicationVersionSpecApplyConfiguration { + b.TenantOperations = value + return b +} diff --git a/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/capapplicationversionstatus.go b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/capapplicationversionstatus.go new file mode 100644 index 0000000..05f3fa5 --- /dev/null +++ b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/capapplicationversionstatus.go @@ -0,0 +1,62 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + smesapcomv1alpha1 "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// CAPApplicationVersionStatusApplyConfiguration represents an declarative configuration of the CAPApplicationVersionStatus type for use +// with apply. +type CAPApplicationVersionStatusApplyConfiguration struct { + GenericStatusApplyConfiguration `json:",inline"` + State *smesapcomv1alpha1.CAPApplicationVersionState `json:"state,omitempty"` + FinishedJobs []string `json:"finishedJobs,omitempty"` +} + +// CAPApplicationVersionStatusApplyConfiguration constructs an declarative configuration of the CAPApplicationVersionStatus type for use with +// apply. +func CAPApplicationVersionStatus() *CAPApplicationVersionStatusApplyConfiguration { + return &CAPApplicationVersionStatusApplyConfiguration{} +} + +// WithObservedGeneration sets the ObservedGeneration field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ObservedGeneration field is set to the value of the last call. +func (b *CAPApplicationVersionStatusApplyConfiguration) WithObservedGeneration(value int64) *CAPApplicationVersionStatusApplyConfiguration { + b.ObservedGeneration = &value + return b +} + +// WithConditions adds the given value to the Conditions field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Conditions field. +func (b *CAPApplicationVersionStatusApplyConfiguration) WithConditions(values ...v1.Condition) *CAPApplicationVersionStatusApplyConfiguration { + for i := range values { + b.Conditions = append(b.Conditions, values[i]) + } + return b +} + +// WithState sets the State field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the State field is set to the value of the last call. +func (b *CAPApplicationVersionStatusApplyConfiguration) WithState(value smesapcomv1alpha1.CAPApplicationVersionState) *CAPApplicationVersionStatusApplyConfiguration { + b.State = &value + return b +} + +// WithFinishedJobs adds the given value to the FinishedJobs field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the FinishedJobs field. +func (b *CAPApplicationVersionStatusApplyConfiguration) WithFinishedJobs(values ...string) *CAPApplicationVersionStatusApplyConfiguration { + for i := range values { + b.FinishedJobs = append(b.FinishedJobs, values[i]) + } + return b +} diff --git a/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/captenant.go b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/captenant.go new file mode 100644 index 0000000..010dca8 --- /dev/null +++ b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/captenant.go @@ -0,0 +1,207 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + v1 "k8s.io/client-go/applyconfigurations/meta/v1" +) + +// CAPTenantApplyConfiguration represents an declarative configuration of the CAPTenant type for use +// with apply. +type CAPTenantApplyConfiguration struct { + v1.TypeMetaApplyConfiguration `json:",inline"` + *v1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"` + Spec *CAPTenantSpecApplyConfiguration `json:"spec,omitempty"` + Status *CAPTenantStatusApplyConfiguration `json:"status,omitempty"` +} + +// CAPTenant constructs an declarative configuration of the CAPTenant type for use with +// apply. +func CAPTenant(name, namespace string) *CAPTenantApplyConfiguration { + b := &CAPTenantApplyConfiguration{} + b.WithName(name) + b.WithNamespace(namespace) + b.WithKind("CAPTenant") + b.WithAPIVersion("sme.sap.com/v1alpha1") + return b +} + +// WithKind sets the Kind field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Kind field is set to the value of the last call. +func (b *CAPTenantApplyConfiguration) WithKind(value string) *CAPTenantApplyConfiguration { + b.Kind = &value + return b +} + +// WithAPIVersion sets the APIVersion field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the APIVersion field is set to the value of the last call. +func (b *CAPTenantApplyConfiguration) WithAPIVersion(value string) *CAPTenantApplyConfiguration { + b.APIVersion = &value + return b +} + +// WithName sets the Name field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Name field is set to the value of the last call. +func (b *CAPTenantApplyConfiguration) WithName(value string) *CAPTenantApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.Name = &value + return b +} + +// WithGenerateName sets the GenerateName field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the GenerateName field is set to the value of the last call. +func (b *CAPTenantApplyConfiguration) WithGenerateName(value string) *CAPTenantApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.GenerateName = &value + return b +} + +// WithNamespace sets the Namespace field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Namespace field is set to the value of the last call. +func (b *CAPTenantApplyConfiguration) WithNamespace(value string) *CAPTenantApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.Namespace = &value + return b +} + +// WithUID sets the UID field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the UID field is set to the value of the last call. +func (b *CAPTenantApplyConfiguration) WithUID(value types.UID) *CAPTenantApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.UID = &value + return b +} + +// WithResourceVersion sets the ResourceVersion field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ResourceVersion field is set to the value of the last call. +func (b *CAPTenantApplyConfiguration) WithResourceVersion(value string) *CAPTenantApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ResourceVersion = &value + return b +} + +// WithGeneration sets the Generation field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Generation field is set to the value of the last call. +func (b *CAPTenantApplyConfiguration) WithGeneration(value int64) *CAPTenantApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.Generation = &value + return b +} + +// WithCreationTimestamp sets the CreationTimestamp field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the CreationTimestamp field is set to the value of the last call. +func (b *CAPTenantApplyConfiguration) WithCreationTimestamp(value metav1.Time) *CAPTenantApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.CreationTimestamp = &value + return b +} + +// WithDeletionTimestamp sets the DeletionTimestamp field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DeletionTimestamp field is set to the value of the last call. +func (b *CAPTenantApplyConfiguration) WithDeletionTimestamp(value metav1.Time) *CAPTenantApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.DeletionTimestamp = &value + return b +} + +// WithDeletionGracePeriodSeconds sets the DeletionGracePeriodSeconds field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DeletionGracePeriodSeconds field is set to the value of the last call. +func (b *CAPTenantApplyConfiguration) WithDeletionGracePeriodSeconds(value int64) *CAPTenantApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.DeletionGracePeriodSeconds = &value + return b +} + +// WithLabels puts the entries into the Labels field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Labels field, +// overwriting an existing map entries in Labels field with the same key. +func (b *CAPTenantApplyConfiguration) WithLabels(entries map[string]string) *CAPTenantApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + if b.Labels == nil && len(entries) > 0 { + b.Labels = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.Labels[k] = v + } + return b +} + +// WithAnnotations puts the entries into the Annotations field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Annotations field, +// overwriting an existing map entries in Annotations field with the same key. +func (b *CAPTenantApplyConfiguration) WithAnnotations(entries map[string]string) *CAPTenantApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + if b.Annotations == nil && len(entries) > 0 { + b.Annotations = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.Annotations[k] = v + } + return b +} + +// WithOwnerReferences adds the given value to the OwnerReferences field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the OwnerReferences field. +func (b *CAPTenantApplyConfiguration) WithOwnerReferences(values ...*v1.OwnerReferenceApplyConfiguration) *CAPTenantApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + if values[i] == nil { + panic("nil value passed to WithOwnerReferences") + } + b.OwnerReferences = append(b.OwnerReferences, *values[i]) + } + return b +} + +// WithFinalizers adds the given value to the Finalizers field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Finalizers field. +func (b *CAPTenantApplyConfiguration) WithFinalizers(values ...string) *CAPTenantApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + b.Finalizers = append(b.Finalizers, values[i]) + } + return b +} + +func (b *CAPTenantApplyConfiguration) ensureObjectMetaApplyConfigurationExists() { + if b.ObjectMetaApplyConfiguration == nil { + b.ObjectMetaApplyConfiguration = &v1.ObjectMetaApplyConfiguration{} + } +} + +// WithSpec sets the Spec field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Spec field is set to the value of the last call. +func (b *CAPTenantApplyConfiguration) WithSpec(value *CAPTenantSpecApplyConfiguration) *CAPTenantApplyConfiguration { + b.Spec = value + return b +} + +// WithStatus sets the Status field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Status field is set to the value of the last call. +func (b *CAPTenantApplyConfiguration) WithStatus(value *CAPTenantStatusApplyConfiguration) *CAPTenantApplyConfiguration { + b.Status = value + return b +} diff --git a/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/captenantoperation.go b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/captenantoperation.go new file mode 100644 index 0000000..0516631 --- /dev/null +++ b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/captenantoperation.go @@ -0,0 +1,207 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + v1 "k8s.io/client-go/applyconfigurations/meta/v1" +) + +// CAPTenantOperationApplyConfiguration represents an declarative configuration of the CAPTenantOperation type for use +// with apply. +type CAPTenantOperationApplyConfiguration struct { + v1.TypeMetaApplyConfiguration `json:",inline"` + *v1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"` + Spec *CAPTenantOperationSpecApplyConfiguration `json:"spec,omitempty"` + Status *CAPTenantOperationStatusApplyConfiguration `json:"status,omitempty"` +} + +// CAPTenantOperation constructs an declarative configuration of the CAPTenantOperation type for use with +// apply. +func CAPTenantOperation(name, namespace string) *CAPTenantOperationApplyConfiguration { + b := &CAPTenantOperationApplyConfiguration{} + b.WithName(name) + b.WithNamespace(namespace) + b.WithKind("CAPTenantOperation") + b.WithAPIVersion("sme.sap.com/v1alpha1") + return b +} + +// WithKind sets the Kind field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Kind field is set to the value of the last call. +func (b *CAPTenantOperationApplyConfiguration) WithKind(value string) *CAPTenantOperationApplyConfiguration { + b.Kind = &value + return b +} + +// WithAPIVersion sets the APIVersion field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the APIVersion field is set to the value of the last call. +func (b *CAPTenantOperationApplyConfiguration) WithAPIVersion(value string) *CAPTenantOperationApplyConfiguration { + b.APIVersion = &value + return b +} + +// WithName sets the Name field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Name field is set to the value of the last call. +func (b *CAPTenantOperationApplyConfiguration) WithName(value string) *CAPTenantOperationApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.Name = &value + return b +} + +// WithGenerateName sets the GenerateName field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the GenerateName field is set to the value of the last call. +func (b *CAPTenantOperationApplyConfiguration) WithGenerateName(value string) *CAPTenantOperationApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.GenerateName = &value + return b +} + +// WithNamespace sets the Namespace field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Namespace field is set to the value of the last call. +func (b *CAPTenantOperationApplyConfiguration) WithNamespace(value string) *CAPTenantOperationApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.Namespace = &value + return b +} + +// WithUID sets the UID field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the UID field is set to the value of the last call. +func (b *CAPTenantOperationApplyConfiguration) WithUID(value types.UID) *CAPTenantOperationApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.UID = &value + return b +} + +// WithResourceVersion sets the ResourceVersion field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ResourceVersion field is set to the value of the last call. +func (b *CAPTenantOperationApplyConfiguration) WithResourceVersion(value string) *CAPTenantOperationApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ResourceVersion = &value + return b +} + +// WithGeneration sets the Generation field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Generation field is set to the value of the last call. +func (b *CAPTenantOperationApplyConfiguration) WithGeneration(value int64) *CAPTenantOperationApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.Generation = &value + return b +} + +// WithCreationTimestamp sets the CreationTimestamp field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the CreationTimestamp field is set to the value of the last call. +func (b *CAPTenantOperationApplyConfiguration) WithCreationTimestamp(value metav1.Time) *CAPTenantOperationApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.CreationTimestamp = &value + return b +} + +// WithDeletionTimestamp sets the DeletionTimestamp field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DeletionTimestamp field is set to the value of the last call. +func (b *CAPTenantOperationApplyConfiguration) WithDeletionTimestamp(value metav1.Time) *CAPTenantOperationApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.DeletionTimestamp = &value + return b +} + +// WithDeletionGracePeriodSeconds sets the DeletionGracePeriodSeconds field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DeletionGracePeriodSeconds field is set to the value of the last call. +func (b *CAPTenantOperationApplyConfiguration) WithDeletionGracePeriodSeconds(value int64) *CAPTenantOperationApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.DeletionGracePeriodSeconds = &value + return b +} + +// WithLabels puts the entries into the Labels field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Labels field, +// overwriting an existing map entries in Labels field with the same key. +func (b *CAPTenantOperationApplyConfiguration) WithLabels(entries map[string]string) *CAPTenantOperationApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + if b.Labels == nil && len(entries) > 0 { + b.Labels = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.Labels[k] = v + } + return b +} + +// WithAnnotations puts the entries into the Annotations field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Annotations field, +// overwriting an existing map entries in Annotations field with the same key. +func (b *CAPTenantOperationApplyConfiguration) WithAnnotations(entries map[string]string) *CAPTenantOperationApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + if b.Annotations == nil && len(entries) > 0 { + b.Annotations = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.Annotations[k] = v + } + return b +} + +// WithOwnerReferences adds the given value to the OwnerReferences field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the OwnerReferences field. +func (b *CAPTenantOperationApplyConfiguration) WithOwnerReferences(values ...*v1.OwnerReferenceApplyConfiguration) *CAPTenantOperationApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + if values[i] == nil { + panic("nil value passed to WithOwnerReferences") + } + b.OwnerReferences = append(b.OwnerReferences, *values[i]) + } + return b +} + +// WithFinalizers adds the given value to the Finalizers field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Finalizers field. +func (b *CAPTenantOperationApplyConfiguration) WithFinalizers(values ...string) *CAPTenantOperationApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + b.Finalizers = append(b.Finalizers, values[i]) + } + return b +} + +func (b *CAPTenantOperationApplyConfiguration) ensureObjectMetaApplyConfigurationExists() { + if b.ObjectMetaApplyConfiguration == nil { + b.ObjectMetaApplyConfiguration = &v1.ObjectMetaApplyConfiguration{} + } +} + +// WithSpec sets the Spec field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Spec field is set to the value of the last call. +func (b *CAPTenantOperationApplyConfiguration) WithSpec(value *CAPTenantOperationSpecApplyConfiguration) *CAPTenantOperationApplyConfiguration { + b.Spec = value + return b +} + +// WithStatus sets the Status field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Status field is set to the value of the last call. +func (b *CAPTenantOperationApplyConfiguration) WithStatus(value *CAPTenantOperationStatusApplyConfiguration) *CAPTenantOperationApplyConfiguration { + b.Status = value + return b +} diff --git a/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/captenantoperationspec.go b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/captenantoperationspec.go new file mode 100644 index 0000000..15d6720 --- /dev/null +++ b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/captenantoperationspec.go @@ -0,0 +1,71 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1alpha1 "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" +) + +// CAPTenantOperationSpecApplyConfiguration represents an declarative configuration of the CAPTenantOperationSpec type for use +// with apply. +type CAPTenantOperationSpecApplyConfiguration struct { + Operation *v1alpha1.CAPTenantOperationType `json:"operation,omitempty"` + BTPTenantIdentificationApplyConfiguration `json:",inline"` + CAPApplicationVersionInstance *string `json:"capApplicationVersionInstance,omitempty"` + Steps []CAPTenantOperationStepApplyConfiguration `json:"steps,omitempty"` +} + +// CAPTenantOperationSpecApplyConfiguration constructs an declarative configuration of the CAPTenantOperationSpec type for use with +// apply. +func CAPTenantOperationSpec() *CAPTenantOperationSpecApplyConfiguration { + return &CAPTenantOperationSpecApplyConfiguration{} +} + +// WithOperation sets the Operation field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Operation field is set to the value of the last call. +func (b *CAPTenantOperationSpecApplyConfiguration) WithOperation(value v1alpha1.CAPTenantOperationType) *CAPTenantOperationSpecApplyConfiguration { + b.Operation = &value + return b +} + +// WithSubDomain sets the SubDomain field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the SubDomain field is set to the value of the last call. +func (b *CAPTenantOperationSpecApplyConfiguration) WithSubDomain(value string) *CAPTenantOperationSpecApplyConfiguration { + b.SubDomain = &value + return b +} + +// WithTenantId sets the TenantId field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the TenantId field is set to the value of the last call. +func (b *CAPTenantOperationSpecApplyConfiguration) WithTenantId(value string) *CAPTenantOperationSpecApplyConfiguration { + b.TenantId = &value + return b +} + +// WithCAPApplicationVersionInstance sets the CAPApplicationVersionInstance field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the CAPApplicationVersionInstance field is set to the value of the last call. +func (b *CAPTenantOperationSpecApplyConfiguration) WithCAPApplicationVersionInstance(value string) *CAPTenantOperationSpecApplyConfiguration { + b.CAPApplicationVersionInstance = &value + return b +} + +// WithSteps adds the given value to the Steps field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Steps field. +func (b *CAPTenantOperationSpecApplyConfiguration) WithSteps(values ...*CAPTenantOperationStepApplyConfiguration) *CAPTenantOperationSpecApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithSteps") + } + b.Steps = append(b.Steps, *values[i]) + } + return b +} diff --git a/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/captenantoperationstatus.go b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/captenantoperationstatus.go new file mode 100644 index 0000000..67ec764 --- /dev/null +++ b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/captenantoperationstatus.go @@ -0,0 +1,69 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + smesapcomv1alpha1 "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// CAPTenantOperationStatusApplyConfiguration represents an declarative configuration of the CAPTenantOperationStatus type for use +// with apply. +type CAPTenantOperationStatusApplyConfiguration struct { + GenericStatusApplyConfiguration `json:",inline"` + State *smesapcomv1alpha1.CAPTenantOperationState `json:"state,omitempty"` + CurrentStep *uint32 `json:"currentStep,omitempty"` + ActiveJob *string `json:"activeJob,omitempty"` +} + +// CAPTenantOperationStatusApplyConfiguration constructs an declarative configuration of the CAPTenantOperationStatus type for use with +// apply. +func CAPTenantOperationStatus() *CAPTenantOperationStatusApplyConfiguration { + return &CAPTenantOperationStatusApplyConfiguration{} +} + +// WithObservedGeneration sets the ObservedGeneration field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ObservedGeneration field is set to the value of the last call. +func (b *CAPTenantOperationStatusApplyConfiguration) WithObservedGeneration(value int64) *CAPTenantOperationStatusApplyConfiguration { + b.ObservedGeneration = &value + return b +} + +// WithConditions adds the given value to the Conditions field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Conditions field. +func (b *CAPTenantOperationStatusApplyConfiguration) WithConditions(values ...v1.Condition) *CAPTenantOperationStatusApplyConfiguration { + for i := range values { + b.Conditions = append(b.Conditions, values[i]) + } + return b +} + +// WithState sets the State field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the State field is set to the value of the last call. +func (b *CAPTenantOperationStatusApplyConfiguration) WithState(value smesapcomv1alpha1.CAPTenantOperationState) *CAPTenantOperationStatusApplyConfiguration { + b.State = &value + return b +} + +// WithCurrentStep sets the CurrentStep field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the CurrentStep field is set to the value of the last call. +func (b *CAPTenantOperationStatusApplyConfiguration) WithCurrentStep(value uint32) *CAPTenantOperationStatusApplyConfiguration { + b.CurrentStep = &value + return b +} + +// WithActiveJob sets the ActiveJob field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ActiveJob field is set to the value of the last call. +func (b *CAPTenantOperationStatusApplyConfiguration) WithActiveJob(value string) *CAPTenantOperationStatusApplyConfiguration { + b.ActiveJob = &value + return b +} diff --git a/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/captenantoperationstep.go b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/captenantoperationstep.go new file mode 100644 index 0000000..fb22f53 --- /dev/null +++ b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/captenantoperationstep.go @@ -0,0 +1,49 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1alpha1 "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" +) + +// CAPTenantOperationStepApplyConfiguration represents an declarative configuration of the CAPTenantOperationStep type for use +// with apply. +type CAPTenantOperationStepApplyConfiguration struct { + Name *string `json:"name,omitempty"` + Type *v1alpha1.JobType `json:"type,omitempty"` + ContinueOnFailure *bool `json:"continueOnFailure,omitempty"` +} + +// CAPTenantOperationStepApplyConfiguration constructs an declarative configuration of the CAPTenantOperationStep type for use with +// apply. +func CAPTenantOperationStep() *CAPTenantOperationStepApplyConfiguration { + return &CAPTenantOperationStepApplyConfiguration{} +} + +// WithName sets the Name field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Name field is set to the value of the last call. +func (b *CAPTenantOperationStepApplyConfiguration) WithName(value string) *CAPTenantOperationStepApplyConfiguration { + b.Name = &value + return b +} + +// WithType sets the Type field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Type field is set to the value of the last call. +func (b *CAPTenantOperationStepApplyConfiguration) WithType(value v1alpha1.JobType) *CAPTenantOperationStepApplyConfiguration { + b.Type = &value + return b +} + +// WithContinueOnFailure sets the ContinueOnFailure field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ContinueOnFailure field is set to the value of the last call. +func (b *CAPTenantOperationStepApplyConfiguration) WithContinueOnFailure(value bool) *CAPTenantOperationStepApplyConfiguration { + b.ContinueOnFailure = &value + return b +} diff --git a/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/captenantspec.go b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/captenantspec.go new file mode 100644 index 0000000..1966560 --- /dev/null +++ b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/captenantspec.go @@ -0,0 +1,66 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + smesapcomv1alpha1 "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" +) + +// CAPTenantSpecApplyConfiguration represents an declarative configuration of the CAPTenantSpec type for use +// with apply. +type CAPTenantSpecApplyConfiguration struct { + CAPApplicationInstance *string `json:"capApplicationInstance,omitempty"` + BTPTenantIdentificationApplyConfiguration `json:",inline"` + Version *string `json:"version,omitempty"` + VersionUpgradeStrategy *smesapcomv1alpha1.VersionUpgradeStrategyType `json:"versionUpgradeStrategy,omitempty"` +} + +// CAPTenantSpecApplyConfiguration constructs an declarative configuration of the CAPTenantSpec type for use with +// apply. +func CAPTenantSpec() *CAPTenantSpecApplyConfiguration { + return &CAPTenantSpecApplyConfiguration{} +} + +// WithCAPApplicationInstance sets the CAPApplicationInstance field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the CAPApplicationInstance field is set to the value of the last call. +func (b *CAPTenantSpecApplyConfiguration) WithCAPApplicationInstance(value string) *CAPTenantSpecApplyConfiguration { + b.CAPApplicationInstance = &value + return b +} + +// WithSubDomain sets the SubDomain field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the SubDomain field is set to the value of the last call. +func (b *CAPTenantSpecApplyConfiguration) WithSubDomain(value string) *CAPTenantSpecApplyConfiguration { + b.SubDomain = &value + return b +} + +// WithTenantId sets the TenantId field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the TenantId field is set to the value of the last call. +func (b *CAPTenantSpecApplyConfiguration) WithTenantId(value string) *CAPTenantSpecApplyConfiguration { + b.TenantId = &value + return b +} + +// WithVersion sets the Version field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Version field is set to the value of the last call. +func (b *CAPTenantSpecApplyConfiguration) WithVersion(value string) *CAPTenantSpecApplyConfiguration { + b.Version = &value + return b +} + +// WithVersionUpgradeStrategy sets the VersionUpgradeStrategy field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the VersionUpgradeStrategy field is set to the value of the last call. +func (b *CAPTenantSpecApplyConfiguration) WithVersionUpgradeStrategy(value smesapcomv1alpha1.VersionUpgradeStrategyType) *CAPTenantSpecApplyConfiguration { + b.VersionUpgradeStrategy = &value + return b +} diff --git a/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/captenantstatus.go b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/captenantstatus.go new file mode 100644 index 0000000..68ed5db --- /dev/null +++ b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/captenantstatus.go @@ -0,0 +1,80 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + smesapcomv1alpha1 "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// CAPTenantStatusApplyConfiguration represents an declarative configuration of the CAPTenantStatus type for use +// with apply. +type CAPTenantStatusApplyConfiguration struct { + GenericStatusApplyConfiguration `json:",inline"` + State *smesapcomv1alpha1.CAPTenantState `json:"state,omitempty"` + CurrentCAPApplicationVersionInstance *string `json:"currentCAPApplicationVersionInstance,omitempty"` + PreviousCAPApplicationVersions []string `json:"previousCAPApplicationVersions,omitempty"` + LastFullReconciliationTime *v1.Time `json:"lastFullReconciliationTime,omitempty"` +} + +// CAPTenantStatusApplyConfiguration constructs an declarative configuration of the CAPTenantStatus type for use with +// apply. +func CAPTenantStatus() *CAPTenantStatusApplyConfiguration { + return &CAPTenantStatusApplyConfiguration{} +} + +// WithObservedGeneration sets the ObservedGeneration field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ObservedGeneration field is set to the value of the last call. +func (b *CAPTenantStatusApplyConfiguration) WithObservedGeneration(value int64) *CAPTenantStatusApplyConfiguration { + b.ObservedGeneration = &value + return b +} + +// WithConditions adds the given value to the Conditions field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Conditions field. +func (b *CAPTenantStatusApplyConfiguration) WithConditions(values ...v1.Condition) *CAPTenantStatusApplyConfiguration { + for i := range values { + b.Conditions = append(b.Conditions, values[i]) + } + return b +} + +// WithState sets the State field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the State field is set to the value of the last call. +func (b *CAPTenantStatusApplyConfiguration) WithState(value smesapcomv1alpha1.CAPTenantState) *CAPTenantStatusApplyConfiguration { + b.State = &value + return b +} + +// WithCurrentCAPApplicationVersionInstance sets the CurrentCAPApplicationVersionInstance field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the CurrentCAPApplicationVersionInstance field is set to the value of the last call. +func (b *CAPTenantStatusApplyConfiguration) WithCurrentCAPApplicationVersionInstance(value string) *CAPTenantStatusApplyConfiguration { + b.CurrentCAPApplicationVersionInstance = &value + return b +} + +// WithPreviousCAPApplicationVersions adds the given value to the PreviousCAPApplicationVersions field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the PreviousCAPApplicationVersions field. +func (b *CAPTenantStatusApplyConfiguration) WithPreviousCAPApplicationVersions(values ...string) *CAPTenantStatusApplyConfiguration { + for i := range values { + b.PreviousCAPApplicationVersions = append(b.PreviousCAPApplicationVersions, values[i]) + } + return b +} + +// WithLastFullReconciliationTime sets the LastFullReconciliationTime field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the LastFullReconciliationTime field is set to the value of the last call. +func (b *CAPTenantStatusApplyConfiguration) WithLastFullReconciliationTime(value v1.Time) *CAPTenantStatusApplyConfiguration { + b.LastFullReconciliationTime = &value + return b +} diff --git a/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/containerdetails.go b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/containerdetails.go new file mode 100644 index 0000000..3e9ad48 --- /dev/null +++ b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/containerdetails.go @@ -0,0 +1,89 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1 "k8s.io/api/core/v1" +) + +// ContainerDetailsApplyConfiguration represents an declarative configuration of the ContainerDetails type for use +// with apply. +type ContainerDetailsApplyConfiguration struct { + Image *string `json:"image,omitempty"` + ImagePullPolicy *v1.PullPolicy `json:"imagePullPolicy,omitempty"` + Command []string `json:"command,omitempty"` + Env []v1.EnvVar `json:"env,omitempty"` + Resources *v1.ResourceRequirements `json:"resources,omitempty"` + SecurityContext *v1.SecurityContext `json:"securityContext,omitempty"` + PodSecurityContext *v1.PodSecurityContext `json:"podSecurityContext,omitempty"` +} + +// ContainerDetailsApplyConfiguration constructs an declarative configuration of the ContainerDetails type for use with +// apply. +func ContainerDetails() *ContainerDetailsApplyConfiguration { + return &ContainerDetailsApplyConfiguration{} +} + +// WithImage sets the Image field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Image field is set to the value of the last call. +func (b *ContainerDetailsApplyConfiguration) WithImage(value string) *ContainerDetailsApplyConfiguration { + b.Image = &value + return b +} + +// WithImagePullPolicy sets the ImagePullPolicy field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ImagePullPolicy field is set to the value of the last call. +func (b *ContainerDetailsApplyConfiguration) WithImagePullPolicy(value v1.PullPolicy) *ContainerDetailsApplyConfiguration { + b.ImagePullPolicy = &value + return b +} + +// WithCommand adds the given value to the Command field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Command field. +func (b *ContainerDetailsApplyConfiguration) WithCommand(values ...string) *ContainerDetailsApplyConfiguration { + for i := range values { + b.Command = append(b.Command, values[i]) + } + return b +} + +// WithEnv adds the given value to the Env field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Env field. +func (b *ContainerDetailsApplyConfiguration) WithEnv(values ...v1.EnvVar) *ContainerDetailsApplyConfiguration { + for i := range values { + b.Env = append(b.Env, values[i]) + } + return b +} + +// WithResources sets the Resources field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Resources field is set to the value of the last call. +func (b *ContainerDetailsApplyConfiguration) WithResources(value v1.ResourceRequirements) *ContainerDetailsApplyConfiguration { + b.Resources = &value + return b +} + +// WithSecurityContext sets the SecurityContext field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the SecurityContext field is set to the value of the last call. +func (b *ContainerDetailsApplyConfiguration) WithSecurityContext(value v1.SecurityContext) *ContainerDetailsApplyConfiguration { + b.SecurityContext = &value + return b +} + +// WithPodSecurityContext sets the PodSecurityContext field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the PodSecurityContext field is set to the value of the last call. +func (b *ContainerDetailsApplyConfiguration) WithPodSecurityContext(value v1.PodSecurityContext) *ContainerDetailsApplyConfiguration { + b.PodSecurityContext = &value + return b +} diff --git a/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/deploymentdetails.go b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/deploymentdetails.go new file mode 100644 index 0000000..6bdecf3 --- /dev/null +++ b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/deploymentdetails.go @@ -0,0 +1,134 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + smesapcomv1alpha1 "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + v1 "k8s.io/api/core/v1" +) + +// DeploymentDetailsApplyConfiguration represents an declarative configuration of the DeploymentDetails type for use +// with apply. +type DeploymentDetailsApplyConfiguration struct { + ContainerDetailsApplyConfiguration `json:",inline"` + Type *smesapcomv1alpha1.DeploymentType `json:"type,omitempty"` + Replicas *int32 `json:"replicas,omitempty"` + Ports []PortsApplyConfiguration `json:"ports,omitempty"` + LivenessProbe *v1.Probe `json:"livenessProbe,omitempty"` + ReadinessProbe *v1.Probe `json:"readinessProbe,omitempty"` +} + +// DeploymentDetailsApplyConfiguration constructs an declarative configuration of the DeploymentDetails type for use with +// apply. +func DeploymentDetails() *DeploymentDetailsApplyConfiguration { + return &DeploymentDetailsApplyConfiguration{} +} + +// WithImage sets the Image field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Image field is set to the value of the last call. +func (b *DeploymentDetailsApplyConfiguration) WithImage(value string) *DeploymentDetailsApplyConfiguration { + b.Image = &value + return b +} + +// WithImagePullPolicy sets the ImagePullPolicy field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ImagePullPolicy field is set to the value of the last call. +func (b *DeploymentDetailsApplyConfiguration) WithImagePullPolicy(value v1.PullPolicy) *DeploymentDetailsApplyConfiguration { + b.ImagePullPolicy = &value + return b +} + +// WithCommand adds the given value to the Command field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Command field. +func (b *DeploymentDetailsApplyConfiguration) WithCommand(values ...string) *DeploymentDetailsApplyConfiguration { + for i := range values { + b.Command = append(b.Command, values[i]) + } + return b +} + +// WithEnv adds the given value to the Env field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Env field. +func (b *DeploymentDetailsApplyConfiguration) WithEnv(values ...v1.EnvVar) *DeploymentDetailsApplyConfiguration { + for i := range values { + b.Env = append(b.Env, values[i]) + } + return b +} + +// WithResources sets the Resources field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Resources field is set to the value of the last call. +func (b *DeploymentDetailsApplyConfiguration) WithResources(value v1.ResourceRequirements) *DeploymentDetailsApplyConfiguration { + b.Resources = &value + return b +} + +// WithSecurityContext sets the SecurityContext field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the SecurityContext field is set to the value of the last call. +func (b *DeploymentDetailsApplyConfiguration) WithSecurityContext(value v1.SecurityContext) *DeploymentDetailsApplyConfiguration { + b.SecurityContext = &value + return b +} + +// WithPodSecurityContext sets the PodSecurityContext field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the PodSecurityContext field is set to the value of the last call. +func (b *DeploymentDetailsApplyConfiguration) WithPodSecurityContext(value v1.PodSecurityContext) *DeploymentDetailsApplyConfiguration { + b.PodSecurityContext = &value + return b +} + +// WithType sets the Type field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Type field is set to the value of the last call. +func (b *DeploymentDetailsApplyConfiguration) WithType(value smesapcomv1alpha1.DeploymentType) *DeploymentDetailsApplyConfiguration { + b.Type = &value + return b +} + +// WithReplicas sets the Replicas field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Replicas field is set to the value of the last call. +func (b *DeploymentDetailsApplyConfiguration) WithReplicas(value int32) *DeploymentDetailsApplyConfiguration { + b.Replicas = &value + return b +} + +// WithPorts adds the given value to the Ports field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Ports field. +func (b *DeploymentDetailsApplyConfiguration) WithPorts(values ...*PortsApplyConfiguration) *DeploymentDetailsApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithPorts") + } + b.Ports = append(b.Ports, *values[i]) + } + return b +} + +// WithLivenessProbe sets the LivenessProbe field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the LivenessProbe field is set to the value of the last call. +func (b *DeploymentDetailsApplyConfiguration) WithLivenessProbe(value v1.Probe) *DeploymentDetailsApplyConfiguration { + b.LivenessProbe = &value + return b +} + +// WithReadinessProbe sets the ReadinessProbe field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ReadinessProbe field is set to the value of the last call. +func (b *DeploymentDetailsApplyConfiguration) WithReadinessProbe(value v1.Probe) *DeploymentDetailsApplyConfiguration { + b.ReadinessProbe = &value + return b +} diff --git a/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/genericstatus.go b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/genericstatus.go new file mode 100644 index 0000000..0b26b02 --- /dev/null +++ b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/genericstatus.go @@ -0,0 +1,42 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// GenericStatusApplyConfiguration represents an declarative configuration of the GenericStatus type for use +// with apply. +type GenericStatusApplyConfiguration struct { + ObservedGeneration *int64 `json:"observedGeneration,omitempty"` + Conditions []v1.Condition `json:"conditions,omitempty"` +} + +// GenericStatusApplyConfiguration constructs an declarative configuration of the GenericStatus type for use with +// apply. +func GenericStatus() *GenericStatusApplyConfiguration { + return &GenericStatusApplyConfiguration{} +} + +// WithObservedGeneration sets the ObservedGeneration field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ObservedGeneration field is set to the value of the last call. +func (b *GenericStatusApplyConfiguration) WithObservedGeneration(value int64) *GenericStatusApplyConfiguration { + b.ObservedGeneration = &value + return b +} + +// WithConditions adds the given value to the Conditions field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Conditions field. +func (b *GenericStatusApplyConfiguration) WithConditions(values ...v1.Condition) *GenericStatusApplyConfiguration { + for i := range values { + b.Conditions = append(b.Conditions, values[i]) + } + return b +} diff --git a/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/jobdetails.go b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/jobdetails.go new file mode 100644 index 0000000..3fd5674 --- /dev/null +++ b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/jobdetails.go @@ -0,0 +1,111 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + smesapcomv1alpha1 "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + v1 "k8s.io/api/core/v1" +) + +// JobDetailsApplyConfiguration represents an declarative configuration of the JobDetails type for use +// with apply. +type JobDetailsApplyConfiguration struct { + ContainerDetailsApplyConfiguration `json:",inline"` + Type *smesapcomv1alpha1.JobType `json:"type,omitempty"` + BackoffLimit *int32 `json:"backoffLimit,omitempty"` + TTLSecondsAfterFinished *int32 `json:"ttlSecondsAfterFinished,omitempty"` +} + +// JobDetailsApplyConfiguration constructs an declarative configuration of the JobDetails type for use with +// apply. +func JobDetails() *JobDetailsApplyConfiguration { + return &JobDetailsApplyConfiguration{} +} + +// WithImage sets the Image field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Image field is set to the value of the last call. +func (b *JobDetailsApplyConfiguration) WithImage(value string) *JobDetailsApplyConfiguration { + b.Image = &value + return b +} + +// WithImagePullPolicy sets the ImagePullPolicy field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ImagePullPolicy field is set to the value of the last call. +func (b *JobDetailsApplyConfiguration) WithImagePullPolicy(value v1.PullPolicy) *JobDetailsApplyConfiguration { + b.ImagePullPolicy = &value + return b +} + +// WithCommand adds the given value to the Command field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Command field. +func (b *JobDetailsApplyConfiguration) WithCommand(values ...string) *JobDetailsApplyConfiguration { + for i := range values { + b.Command = append(b.Command, values[i]) + } + return b +} + +// WithEnv adds the given value to the Env field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Env field. +func (b *JobDetailsApplyConfiguration) WithEnv(values ...v1.EnvVar) *JobDetailsApplyConfiguration { + for i := range values { + b.Env = append(b.Env, values[i]) + } + return b +} + +// WithResources sets the Resources field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Resources field is set to the value of the last call. +func (b *JobDetailsApplyConfiguration) WithResources(value v1.ResourceRequirements) *JobDetailsApplyConfiguration { + b.Resources = &value + return b +} + +// WithSecurityContext sets the SecurityContext field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the SecurityContext field is set to the value of the last call. +func (b *JobDetailsApplyConfiguration) WithSecurityContext(value v1.SecurityContext) *JobDetailsApplyConfiguration { + b.SecurityContext = &value + return b +} + +// WithPodSecurityContext sets the PodSecurityContext field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the PodSecurityContext field is set to the value of the last call. +func (b *JobDetailsApplyConfiguration) WithPodSecurityContext(value v1.PodSecurityContext) *JobDetailsApplyConfiguration { + b.PodSecurityContext = &value + return b +} + +// WithType sets the Type field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Type field is set to the value of the last call. +func (b *JobDetailsApplyConfiguration) WithType(value smesapcomv1alpha1.JobType) *JobDetailsApplyConfiguration { + b.Type = &value + return b +} + +// WithBackoffLimit sets the BackoffLimit field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the BackoffLimit field is set to the value of the last call. +func (b *JobDetailsApplyConfiguration) WithBackoffLimit(value int32) *JobDetailsApplyConfiguration { + b.BackoffLimit = &value + return b +} + +// WithTTLSecondsAfterFinished sets the TTLSecondsAfterFinished field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the TTLSecondsAfterFinished field is set to the value of the last call. +func (b *JobDetailsApplyConfiguration) WithTTLSecondsAfterFinished(value int32) *JobDetailsApplyConfiguration { + b.TTLSecondsAfterFinished = &value + return b +} diff --git a/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/namevalue.go b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/namevalue.go new file mode 100644 index 0000000..014db3d --- /dev/null +++ b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/namevalue.go @@ -0,0 +1,36 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +// NameValueApplyConfiguration represents an declarative configuration of the NameValue type for use +// with apply. +type NameValueApplyConfiguration struct { + Name *string `json:"name,omitempty"` + Value *string `json:"value,omitempty"` +} + +// NameValueApplyConfiguration constructs an declarative configuration of the NameValue type for use with +// apply. +func NameValue() *NameValueApplyConfiguration { + return &NameValueApplyConfiguration{} +} + +// WithName sets the Name field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Name field is set to the value of the last call. +func (b *NameValueApplyConfiguration) WithName(value string) *NameValueApplyConfiguration { + b.Name = &value + return b +} + +// WithValue sets the Value field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Value field is set to the value of the last call. +func (b *NameValueApplyConfiguration) WithValue(value string) *NameValueApplyConfiguration { + b.Value = &value + return b +} diff --git a/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/ports.go b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/ports.go new file mode 100644 index 0000000..e4f7771 --- /dev/null +++ b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/ports.go @@ -0,0 +1,67 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1alpha1 "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" +) + +// PortsApplyConfiguration represents an declarative configuration of the Ports type for use +// with apply. +type PortsApplyConfiguration struct { + AppProtocol *string `json:"appProtocol,omitempty"` + Name *string `json:"name,omitempty"` + NetworkPolicy *v1alpha1.PortNetworkPolicyType `json:"networkPolicy,omitempty"` + Port *int32 `json:"port,omitempty"` + RouterDestinationName *string `json:"routerDestinationName,omitempty"` +} + +// PortsApplyConfiguration constructs an declarative configuration of the Ports type for use with +// apply. +func Ports() *PortsApplyConfiguration { + return &PortsApplyConfiguration{} +} + +// WithAppProtocol sets the AppProtocol field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the AppProtocol field is set to the value of the last call. +func (b *PortsApplyConfiguration) WithAppProtocol(value string) *PortsApplyConfiguration { + b.AppProtocol = &value + return b +} + +// WithName sets the Name field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Name field is set to the value of the last call. +func (b *PortsApplyConfiguration) WithName(value string) *PortsApplyConfiguration { + b.Name = &value + return b +} + +// WithNetworkPolicy sets the NetworkPolicy field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the NetworkPolicy field is set to the value of the last call. +func (b *PortsApplyConfiguration) WithNetworkPolicy(value v1alpha1.PortNetworkPolicyType) *PortsApplyConfiguration { + b.NetworkPolicy = &value + return b +} + +// WithPort sets the Port field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Port field is set to the value of the last call. +func (b *PortsApplyConfiguration) WithPort(value int32) *PortsApplyConfiguration { + b.Port = &value + return b +} + +// WithRouterDestinationName sets the RouterDestinationName field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the RouterDestinationName field is set to the value of the last call. +func (b *PortsApplyConfiguration) WithRouterDestinationName(value string) *PortsApplyConfiguration { + b.RouterDestinationName = &value + return b +} diff --git a/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/serviceinfo.go b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/serviceinfo.go new file mode 100644 index 0000000..98bc167 --- /dev/null +++ b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/serviceinfo.go @@ -0,0 +1,45 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +// ServiceInfoApplyConfiguration represents an declarative configuration of the ServiceInfo type for use +// with apply. +type ServiceInfoApplyConfiguration struct { + Name *string `json:"name,omitempty"` + Secret *string `json:"secret,omitempty"` + Class *string `json:"class,omitempty"` +} + +// ServiceInfoApplyConfiguration constructs an declarative configuration of the ServiceInfo type for use with +// apply. +func ServiceInfo() *ServiceInfoApplyConfiguration { + return &ServiceInfoApplyConfiguration{} +} + +// WithName sets the Name field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Name field is set to the value of the last call. +func (b *ServiceInfoApplyConfiguration) WithName(value string) *ServiceInfoApplyConfiguration { + b.Name = &value + return b +} + +// WithSecret sets the Secret field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Secret field is set to the value of the last call. +func (b *ServiceInfoApplyConfiguration) WithSecret(value string) *ServiceInfoApplyConfiguration { + b.Secret = &value + return b +} + +// WithClass sets the Class field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Class field is set to the value of the last call. +func (b *ServiceInfoApplyConfiguration) WithClass(value string) *ServiceInfoApplyConfiguration { + b.Class = &value + return b +} diff --git a/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/tenantoperations.go b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/tenantoperations.go new file mode 100644 index 0000000..5b966e9 --- /dev/null +++ b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/tenantoperations.go @@ -0,0 +1,60 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +// TenantOperationsApplyConfiguration represents an declarative configuration of the TenantOperations type for use +// with apply. +type TenantOperationsApplyConfiguration struct { + Provisioning []TenantOperationWorkloadReferenceApplyConfiguration `json:"provisioning,omitempty"` + Upgrade []TenantOperationWorkloadReferenceApplyConfiguration `json:"upgrade,omitempty"` + Deprovisioning []TenantOperationWorkloadReferenceApplyConfiguration `json:"deprovisioning,omitempty"` +} + +// TenantOperationsApplyConfiguration constructs an declarative configuration of the TenantOperations type for use with +// apply. +func TenantOperations() *TenantOperationsApplyConfiguration { + return &TenantOperationsApplyConfiguration{} +} + +// WithProvisioning adds the given value to the Provisioning field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Provisioning field. +func (b *TenantOperationsApplyConfiguration) WithProvisioning(values ...*TenantOperationWorkloadReferenceApplyConfiguration) *TenantOperationsApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithProvisioning") + } + b.Provisioning = append(b.Provisioning, *values[i]) + } + return b +} + +// WithUpgrade adds the given value to the Upgrade field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Upgrade field. +func (b *TenantOperationsApplyConfiguration) WithUpgrade(values ...*TenantOperationWorkloadReferenceApplyConfiguration) *TenantOperationsApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithUpgrade") + } + b.Upgrade = append(b.Upgrade, *values[i]) + } + return b +} + +// WithDeprovisioning adds the given value to the Deprovisioning field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Deprovisioning field. +func (b *TenantOperationsApplyConfiguration) WithDeprovisioning(values ...*TenantOperationWorkloadReferenceApplyConfiguration) *TenantOperationsApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithDeprovisioning") + } + b.Deprovisioning = append(b.Deprovisioning, *values[i]) + } + return b +} diff --git a/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/tenantoperationworkloadreference.go b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/tenantoperationworkloadreference.go new file mode 100644 index 0000000..a919db6 --- /dev/null +++ b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/tenantoperationworkloadreference.go @@ -0,0 +1,36 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +// TenantOperationWorkloadReferenceApplyConfiguration represents an declarative configuration of the TenantOperationWorkloadReference type for use +// with apply. +type TenantOperationWorkloadReferenceApplyConfiguration struct { + WorkloadName *string `json:"workloadName,omitempty"` + ContinueOnFailure *bool `json:"continueOnFailure,omitempty"` +} + +// TenantOperationWorkloadReferenceApplyConfiguration constructs an declarative configuration of the TenantOperationWorkloadReference type for use with +// apply. +func TenantOperationWorkloadReference() *TenantOperationWorkloadReferenceApplyConfiguration { + return &TenantOperationWorkloadReferenceApplyConfiguration{} +} + +// WithWorkloadName sets the WorkloadName field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the WorkloadName field is set to the value of the last call. +func (b *TenantOperationWorkloadReferenceApplyConfiguration) WithWorkloadName(value string) *TenantOperationWorkloadReferenceApplyConfiguration { + b.WorkloadName = &value + return b +} + +// WithContinueOnFailure sets the ContinueOnFailure field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ContinueOnFailure field is set to the value of the last call. +func (b *TenantOperationWorkloadReferenceApplyConfiguration) WithContinueOnFailure(value bool) *TenantOperationWorkloadReferenceApplyConfiguration { + b.ContinueOnFailure = &value + return b +} diff --git a/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/workloaddetails.go b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/workloaddetails.go new file mode 100644 index 0000000..86e0de7 --- /dev/null +++ b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/workloaddetails.go @@ -0,0 +1,86 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +// WorkloadDetailsApplyConfiguration represents an declarative configuration of the WorkloadDetails type for use +// with apply. +type WorkloadDetailsApplyConfiguration struct { + Name *string `json:"name,omitempty"` + ConsumedBTPServices []string `json:"consumedBTPServices,omitempty"` + Labels map[string]string `json:"labels,omitempty"` + Annotations map[string]string `json:"annotations,omitempty"` + DeploymentDefinition *DeploymentDetailsApplyConfiguration `json:"deploymentDefinition,omitempty"` + JobDefinition *JobDetailsApplyConfiguration `json:"jobDefinition,omitempty"` +} + +// WorkloadDetailsApplyConfiguration constructs an declarative configuration of the WorkloadDetails type for use with +// apply. +func WorkloadDetails() *WorkloadDetailsApplyConfiguration { + return &WorkloadDetailsApplyConfiguration{} +} + +// WithName sets the Name field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Name field is set to the value of the last call. +func (b *WorkloadDetailsApplyConfiguration) WithName(value string) *WorkloadDetailsApplyConfiguration { + b.Name = &value + return b +} + +// WithConsumedBTPServices adds the given value to the ConsumedBTPServices field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the ConsumedBTPServices field. +func (b *WorkloadDetailsApplyConfiguration) WithConsumedBTPServices(values ...string) *WorkloadDetailsApplyConfiguration { + for i := range values { + b.ConsumedBTPServices = append(b.ConsumedBTPServices, values[i]) + } + return b +} + +// WithLabels puts the entries into the Labels field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Labels field, +// overwriting an existing map entries in Labels field with the same key. +func (b *WorkloadDetailsApplyConfiguration) WithLabels(entries map[string]string) *WorkloadDetailsApplyConfiguration { + if b.Labels == nil && len(entries) > 0 { + b.Labels = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.Labels[k] = v + } + return b +} + +// WithAnnotations puts the entries into the Annotations field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Annotations field, +// overwriting an existing map entries in Annotations field with the same key. +func (b *WorkloadDetailsApplyConfiguration) WithAnnotations(entries map[string]string) *WorkloadDetailsApplyConfiguration { + if b.Annotations == nil && len(entries) > 0 { + b.Annotations = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.Annotations[k] = v + } + return b +} + +// WithDeploymentDefinition sets the DeploymentDefinition field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DeploymentDefinition field is set to the value of the last call. +func (b *WorkloadDetailsApplyConfiguration) WithDeploymentDefinition(value *DeploymentDetailsApplyConfiguration) *WorkloadDetailsApplyConfiguration { + b.DeploymentDefinition = value + return b +} + +// WithJobDefinition sets the JobDefinition field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the JobDefinition field is set to the value of the last call. +func (b *WorkloadDetailsApplyConfiguration) WithJobDefinition(value *JobDetailsApplyConfiguration) *WorkloadDetailsApplyConfiguration { + b.JobDefinition = value + return b +} diff --git a/pkg/client/applyconfiguration/utils.go b/pkg/client/applyconfiguration/utils.go new file mode 100644 index 0000000..e90cf67 --- /dev/null +++ b/pkg/client/applyconfiguration/utils.go @@ -0,0 +1,75 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package applyconfiguration + +import ( + v1alpha1 "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + smesapcomv1alpha1 "github.com/sap/cap-operator/pkg/client/applyconfiguration/sme.sap.com/v1alpha1" + schema "k8s.io/apimachinery/pkg/runtime/schema" +) + +// ForKind returns an apply configuration type for the given GroupVersionKind, or nil if no +// apply configuration type exists for the given GroupVersionKind. +func ForKind(kind schema.GroupVersionKind) interface{} { + switch kind { + // Group=sme.sap.com, Version=v1alpha1 + case v1alpha1.SchemeGroupVersion.WithKind("ApplicationDomains"): + return &smesapcomv1alpha1.ApplicationDomainsApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("BTP"): + return &smesapcomv1alpha1.BTPApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("BTPTenantIdentification"): + return &smesapcomv1alpha1.BTPTenantIdentificationApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("CAPApplication"): + return &smesapcomv1alpha1.CAPApplicationApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("CAPApplicationSpec"): + return &smesapcomv1alpha1.CAPApplicationSpecApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("CAPApplicationStatus"): + return &smesapcomv1alpha1.CAPApplicationStatusApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("CAPApplicationVersion"): + return &smesapcomv1alpha1.CAPApplicationVersionApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("CAPApplicationVersionSpec"): + return &smesapcomv1alpha1.CAPApplicationVersionSpecApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("CAPApplicationVersionStatus"): + return &smesapcomv1alpha1.CAPApplicationVersionStatusApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("CAPTenant"): + return &smesapcomv1alpha1.CAPTenantApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("CAPTenantOperation"): + return &smesapcomv1alpha1.CAPTenantOperationApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("CAPTenantOperationSpec"): + return &smesapcomv1alpha1.CAPTenantOperationSpecApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("CAPTenantOperationStatus"): + return &smesapcomv1alpha1.CAPTenantOperationStatusApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("CAPTenantOperationStep"): + return &smesapcomv1alpha1.CAPTenantOperationStepApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("CAPTenantSpec"): + return &smesapcomv1alpha1.CAPTenantSpecApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("CAPTenantStatus"): + return &smesapcomv1alpha1.CAPTenantStatusApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("ContainerDetails"): + return &smesapcomv1alpha1.ContainerDetailsApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("DeploymentDetails"): + return &smesapcomv1alpha1.DeploymentDetailsApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("GenericStatus"): + return &smesapcomv1alpha1.GenericStatusApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("JobDetails"): + return &smesapcomv1alpha1.JobDetailsApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("NameValue"): + return &smesapcomv1alpha1.NameValueApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("Ports"): + return &smesapcomv1alpha1.PortsApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("ServiceInfo"): + return &smesapcomv1alpha1.ServiceInfoApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("TenantOperations"): + return &smesapcomv1alpha1.TenantOperationsApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("TenantOperationWorkloadReference"): + return &smesapcomv1alpha1.TenantOperationWorkloadReferenceApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("WorkloadDetails"): + return &smesapcomv1alpha1.WorkloadDetailsApplyConfiguration{} + + } + return nil +} diff --git a/pkg/client/clientset/versioned/clientset.go b/pkg/client/clientset/versioned/clientset.go new file mode 100644 index 0000000..1c4e34d --- /dev/null +++ b/pkg/client/clientset/versioned/clientset.go @@ -0,0 +1,108 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by client-gen. DO NOT EDIT. + +package versioned + +import ( + "fmt" + "net/http" + + smev1alpha1 "github.com/sap/cap-operator/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1" + discovery "k8s.io/client-go/discovery" + rest "k8s.io/client-go/rest" + flowcontrol "k8s.io/client-go/util/flowcontrol" +) + +type Interface interface { + Discovery() discovery.DiscoveryInterface + SmeV1alpha1() smev1alpha1.SmeV1alpha1Interface +} + +// Clientset contains the clients for groups. +type Clientset struct { + *discovery.DiscoveryClient + smeV1alpha1 *smev1alpha1.SmeV1alpha1Client +} + +// SmeV1alpha1 retrieves the SmeV1alpha1Client +func (c *Clientset) SmeV1alpha1() smev1alpha1.SmeV1alpha1Interface { + return c.smeV1alpha1 +} + +// Discovery retrieves the DiscoveryClient +func (c *Clientset) Discovery() discovery.DiscoveryInterface { + if c == nil { + return nil + } + return c.DiscoveryClient +} + +// NewForConfig creates a new Clientset for the given config. +// If config's RateLimiter is not set and QPS and Burst are acceptable, +// NewForConfig will generate a rate-limiter in configShallowCopy. +// NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), +// where httpClient was generated with rest.HTTPClientFor(c). +func NewForConfig(c *rest.Config) (*Clientset, error) { + configShallowCopy := *c + + if configShallowCopy.UserAgent == "" { + configShallowCopy.UserAgent = rest.DefaultKubernetesUserAgent() + } + + // share the transport between all clients + httpClient, err := rest.HTTPClientFor(&configShallowCopy) + if err != nil { + return nil, err + } + + return NewForConfigAndClient(&configShallowCopy, httpClient) +} + +// NewForConfigAndClient creates a new Clientset for the given config and http client. +// Note the http client provided takes precedence over the configured transport values. +// If config's RateLimiter is not set and QPS and Burst are acceptable, +// NewForConfigAndClient will generate a rate-limiter in configShallowCopy. +func NewForConfigAndClient(c *rest.Config, httpClient *http.Client) (*Clientset, error) { + configShallowCopy := *c + if configShallowCopy.RateLimiter == nil && configShallowCopy.QPS > 0 { + if configShallowCopy.Burst <= 0 { + return nil, fmt.Errorf("burst is required to be greater than 0 when RateLimiter is not set and QPS is set to greater than 0") + } + configShallowCopy.RateLimiter = flowcontrol.NewTokenBucketRateLimiter(configShallowCopy.QPS, configShallowCopy.Burst) + } + + var cs Clientset + var err error + cs.smeV1alpha1, err = smev1alpha1.NewForConfigAndClient(&configShallowCopy, httpClient) + if err != nil { + return nil, err + } + + cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfigAndClient(&configShallowCopy, httpClient) + if err != nil { + return nil, err + } + return &cs, nil +} + +// NewForConfigOrDie creates a new Clientset for the given config and +// panics if there is an error in the config. +func NewForConfigOrDie(c *rest.Config) *Clientset { + cs, err := NewForConfig(c) + if err != nil { + panic(err) + } + return cs +} + +// New creates a new Clientset for the given RESTClient. +func New(c rest.Interface) *Clientset { + var cs Clientset + cs.smeV1alpha1 = smev1alpha1.New(c) + + cs.DiscoveryClient = discovery.NewDiscoveryClient(c) + return &cs +} diff --git a/pkg/client/clientset/versioned/fake/clientset_generated.go b/pkg/client/clientset/versioned/fake/clientset_generated.go new file mode 100644 index 0000000..cd0c824 --- /dev/null +++ b/pkg/client/clientset/versioned/fake/clientset_generated.go @@ -0,0 +1,73 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + clientset "github.com/sap/cap-operator/pkg/client/clientset/versioned" + smev1alpha1 "github.com/sap/cap-operator/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1" + fakesmev1alpha1 "github.com/sap/cap-operator/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/fake" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/discovery" + fakediscovery "k8s.io/client-go/discovery/fake" + "k8s.io/client-go/testing" +) + +// NewSimpleClientset returns a clientset that will respond with the provided objects. +// It's backed by a very simple object tracker that processes creates, updates and deletions as-is, +// without applying any validations and/or defaults. It shouldn't be considered a replacement +// for a real clientset and is mostly useful in simple unit tests. +func NewSimpleClientset(objects ...runtime.Object) *Clientset { + o := testing.NewObjectTracker(scheme, codecs.UniversalDecoder()) + for _, obj := range objects { + if err := o.Add(obj); err != nil { + panic(err) + } + } + + cs := &Clientset{tracker: o} + cs.discovery = &fakediscovery.FakeDiscovery{Fake: &cs.Fake} + cs.AddReactor("*", "*", testing.ObjectReaction(o)) + cs.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) { + gvr := action.GetResource() + ns := action.GetNamespace() + watch, err := o.Watch(gvr, ns) + if err != nil { + return false, nil, err + } + return true, watch, nil + }) + + return cs +} + +// Clientset implements clientset.Interface. Meant to be embedded into a +// struct to get a default implementation. This makes faking out just the method +// you want to test easier. +type Clientset struct { + testing.Fake + discovery *fakediscovery.FakeDiscovery + tracker testing.ObjectTracker +} + +func (c *Clientset) Discovery() discovery.DiscoveryInterface { + return c.discovery +} + +func (c *Clientset) Tracker() testing.ObjectTracker { + return c.tracker +} + +var ( + _ clientset.Interface = &Clientset{} + _ testing.FakeClient = &Clientset{} +) + +// SmeV1alpha1 retrieves the SmeV1alpha1Client +func (c *Clientset) SmeV1alpha1() smev1alpha1.SmeV1alpha1Interface { + return &fakesmev1alpha1.FakeSmeV1alpha1{Fake: &c.Fake} +} diff --git a/pkg/client/clientset/versioned/fake/doc.go b/pkg/client/clientset/versioned/fake/doc.go new file mode 100644 index 0000000..29cce4b --- /dev/null +++ b/pkg/client/clientset/versioned/fake/doc.go @@ -0,0 +1,8 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by client-gen. DO NOT EDIT. + +// This package has the automatically generated fake clientset. +package fake diff --git a/pkg/client/clientset/versioned/fake/register.go b/pkg/client/clientset/versioned/fake/register.go new file mode 100644 index 0000000..a7fbaab --- /dev/null +++ b/pkg/client/clientset/versioned/fake/register.go @@ -0,0 +1,44 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + smev1alpha1 "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + schema "k8s.io/apimachinery/pkg/runtime/schema" + serializer "k8s.io/apimachinery/pkg/runtime/serializer" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" +) + +var scheme = runtime.NewScheme() +var codecs = serializer.NewCodecFactory(scheme) + +var localSchemeBuilder = runtime.SchemeBuilder{ + smev1alpha1.AddToScheme, +} + +// AddToScheme adds all types of this clientset into the given scheme. This allows composition +// of clientsets, like in: +// +// import ( +// "k8s.io/client-go/kubernetes" +// clientsetscheme "k8s.io/client-go/kubernetes/scheme" +// aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" +// ) +// +// kclientset, _ := kubernetes.NewForConfig(c) +// _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) +// +// After this, RawExtensions in Kubernetes types will serialize kube-aggregator types +// correctly. +var AddToScheme = localSchemeBuilder.AddToScheme + +func init() { + v1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"}) + utilruntime.Must(AddToScheme(scheme)) +} diff --git a/pkg/client/clientset/versioned/scheme/doc.go b/pkg/client/clientset/versioned/scheme/doc.go new file mode 100644 index 0000000..42d6fb6 --- /dev/null +++ b/pkg/client/clientset/versioned/scheme/doc.go @@ -0,0 +1,8 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by client-gen. DO NOT EDIT. + +// This package contains the scheme of the automatically generated clientset. +package scheme diff --git a/pkg/client/clientset/versioned/scheme/register.go b/pkg/client/clientset/versioned/scheme/register.go new file mode 100644 index 0000000..6558109 --- /dev/null +++ b/pkg/client/clientset/versioned/scheme/register.go @@ -0,0 +1,44 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by client-gen. DO NOT EDIT. + +package scheme + +import ( + smev1alpha1 "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + schema "k8s.io/apimachinery/pkg/runtime/schema" + serializer "k8s.io/apimachinery/pkg/runtime/serializer" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" +) + +var Scheme = runtime.NewScheme() +var Codecs = serializer.NewCodecFactory(Scheme) +var ParameterCodec = runtime.NewParameterCodec(Scheme) +var localSchemeBuilder = runtime.SchemeBuilder{ + smev1alpha1.AddToScheme, +} + +// AddToScheme adds all types of this clientset into the given scheme. This allows composition +// of clientsets, like in: +// +// import ( +// "k8s.io/client-go/kubernetes" +// clientsetscheme "k8s.io/client-go/kubernetes/scheme" +// aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" +// ) +// +// kclientset, _ := kubernetes.NewForConfig(c) +// _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) +// +// After this, RawExtensions in Kubernetes types will serialize kube-aggregator types +// correctly. +var AddToScheme = localSchemeBuilder.AddToScheme + +func init() { + v1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"}) + utilruntime.Must(AddToScheme(Scheme)) +} diff --git a/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/capapplication.go b/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/capapplication.go new file mode 100644 index 0000000..c54505f --- /dev/null +++ b/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/capapplication.go @@ -0,0 +1,244 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + json "encoding/json" + "fmt" + "time" + + v1alpha1 "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + smesapcomv1alpha1 "github.com/sap/cap-operator/pkg/client/applyconfiguration/sme.sap.com/v1alpha1" + scheme "github.com/sap/cap-operator/pkg/client/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" +) + +// CAPApplicationsGetter has a method to return a CAPApplicationInterface. +// A group's client should implement this interface. +type CAPApplicationsGetter interface { + CAPApplications(namespace string) CAPApplicationInterface +} + +// CAPApplicationInterface has methods to work with CAPApplication resources. +type CAPApplicationInterface interface { + Create(ctx context.Context, cAPApplication *v1alpha1.CAPApplication, opts v1.CreateOptions) (*v1alpha1.CAPApplication, error) + Update(ctx context.Context, cAPApplication *v1alpha1.CAPApplication, opts v1.UpdateOptions) (*v1alpha1.CAPApplication, error) + UpdateStatus(ctx context.Context, cAPApplication *v1alpha1.CAPApplication, opts v1.UpdateOptions) (*v1alpha1.CAPApplication, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha1.CAPApplication, error) + List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.CAPApplicationList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.CAPApplication, err error) + Apply(ctx context.Context, cAPApplication *smesapcomv1alpha1.CAPApplicationApplyConfiguration, opts v1.ApplyOptions) (result *v1alpha1.CAPApplication, err error) + ApplyStatus(ctx context.Context, cAPApplication *smesapcomv1alpha1.CAPApplicationApplyConfiguration, opts v1.ApplyOptions) (result *v1alpha1.CAPApplication, err error) + CAPApplicationExpansion +} + +// cAPApplications implements CAPApplicationInterface +type cAPApplications struct { + client rest.Interface + ns string +} + +// newCAPApplications returns a CAPApplications +func newCAPApplications(c *SmeV1alpha1Client, namespace string) *cAPApplications { + return &cAPApplications{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the cAPApplication, and returns the corresponding cAPApplication object, and an error if there is any. +func (c *cAPApplications) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.CAPApplication, err error) { + result = &v1alpha1.CAPApplication{} + err = c.client.Get(). + Namespace(c.ns). + Resource("capapplications"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(ctx). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of CAPApplications that match those selectors. +func (c *cAPApplications) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.CAPApplicationList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1alpha1.CAPApplicationList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("capapplications"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(ctx). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested cAPApplications. +func (c *cAPApplications) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + opts.Watch = true + return c.client.Get(). + Namespace(c.ns). + Resource("capapplications"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch(ctx) +} + +// Create takes the representation of a cAPApplication and creates it. Returns the server's representation of the cAPApplication, and an error, if there is any. +func (c *cAPApplications) Create(ctx context.Context, cAPApplication *v1alpha1.CAPApplication, opts v1.CreateOptions) (result *v1alpha1.CAPApplication, err error) { + result = &v1alpha1.CAPApplication{} + err = c.client.Post(). + Namespace(c.ns). + Resource("capapplications"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(cAPApplication). + Do(ctx). + Into(result) + return +} + +// Update takes the representation of a cAPApplication and updates it. Returns the server's representation of the cAPApplication, and an error, if there is any. +func (c *cAPApplications) Update(ctx context.Context, cAPApplication *v1alpha1.CAPApplication, opts v1.UpdateOptions) (result *v1alpha1.CAPApplication, err error) { + result = &v1alpha1.CAPApplication{} + err = c.client.Put(). + Namespace(c.ns). + Resource("capapplications"). + Name(cAPApplication.Name). + VersionedParams(&opts, scheme.ParameterCodec). + Body(cAPApplication). + Do(ctx). + Into(result) + return +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *cAPApplications) UpdateStatus(ctx context.Context, cAPApplication *v1alpha1.CAPApplication, opts v1.UpdateOptions) (result *v1alpha1.CAPApplication, err error) { + result = &v1alpha1.CAPApplication{} + err = c.client.Put(). + Namespace(c.ns). + Resource("capapplications"). + Name(cAPApplication.Name). + SubResource("status"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(cAPApplication). + Do(ctx). + Into(result) + return +} + +// Delete takes name of the cAPApplication and deletes it. Returns an error if one occurs. +func (c *cAPApplications) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("capapplications"). + Name(name). + Body(&opts). + Do(ctx). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *cAPApplications) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + var timeout time.Duration + if listOpts.TimeoutSeconds != nil { + timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second + } + return c.client.Delete(). + Namespace(c.ns). + Resource("capapplications"). + VersionedParams(&listOpts, scheme.ParameterCodec). + Timeout(timeout). + Body(&opts). + Do(ctx). + Error() +} + +// Patch applies the patch and returns the patched cAPApplication. +func (c *cAPApplications) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.CAPApplication, err error) { + result = &v1alpha1.CAPApplication{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("capapplications"). + Name(name). + SubResource(subresources...). + VersionedParams(&opts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} + +// Apply takes the given apply declarative configuration, applies it and returns the applied cAPApplication. +func (c *cAPApplications) Apply(ctx context.Context, cAPApplication *smesapcomv1alpha1.CAPApplicationApplyConfiguration, opts v1.ApplyOptions) (result *v1alpha1.CAPApplication, err error) { + if cAPApplication == nil { + return nil, fmt.Errorf("cAPApplication provided to Apply must not be nil") + } + patchOpts := opts.ToPatchOptions() + data, err := json.Marshal(cAPApplication) + if err != nil { + return nil, err + } + name := cAPApplication.Name + if name == nil { + return nil, fmt.Errorf("cAPApplication.Name must be provided to Apply") + } + result = &v1alpha1.CAPApplication{} + err = c.client.Patch(types.ApplyPatchType). + Namespace(c.ns). + Resource("capapplications"). + Name(*name). + VersionedParams(&patchOpts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} + +// ApplyStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating ApplyStatus(). +func (c *cAPApplications) ApplyStatus(ctx context.Context, cAPApplication *smesapcomv1alpha1.CAPApplicationApplyConfiguration, opts v1.ApplyOptions) (result *v1alpha1.CAPApplication, err error) { + if cAPApplication == nil { + return nil, fmt.Errorf("cAPApplication provided to Apply must not be nil") + } + patchOpts := opts.ToPatchOptions() + data, err := json.Marshal(cAPApplication) + if err != nil { + return nil, err + } + + name := cAPApplication.Name + if name == nil { + return nil, fmt.Errorf("cAPApplication.Name must be provided to Apply") + } + + result = &v1alpha1.CAPApplication{} + err = c.client.Patch(types.ApplyPatchType). + Namespace(c.ns). + Resource("capapplications"). + Name(*name). + SubResource("status"). + VersionedParams(&patchOpts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} diff --git a/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/capapplicationversion.go b/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/capapplicationversion.go new file mode 100644 index 0000000..a53428f --- /dev/null +++ b/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/capapplicationversion.go @@ -0,0 +1,244 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + json "encoding/json" + "fmt" + "time" + + v1alpha1 "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + smesapcomv1alpha1 "github.com/sap/cap-operator/pkg/client/applyconfiguration/sme.sap.com/v1alpha1" + scheme "github.com/sap/cap-operator/pkg/client/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" +) + +// CAPApplicationVersionsGetter has a method to return a CAPApplicationVersionInterface. +// A group's client should implement this interface. +type CAPApplicationVersionsGetter interface { + CAPApplicationVersions(namespace string) CAPApplicationVersionInterface +} + +// CAPApplicationVersionInterface has methods to work with CAPApplicationVersion resources. +type CAPApplicationVersionInterface interface { + Create(ctx context.Context, cAPApplicationVersion *v1alpha1.CAPApplicationVersion, opts v1.CreateOptions) (*v1alpha1.CAPApplicationVersion, error) + Update(ctx context.Context, cAPApplicationVersion *v1alpha1.CAPApplicationVersion, opts v1.UpdateOptions) (*v1alpha1.CAPApplicationVersion, error) + UpdateStatus(ctx context.Context, cAPApplicationVersion *v1alpha1.CAPApplicationVersion, opts v1.UpdateOptions) (*v1alpha1.CAPApplicationVersion, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha1.CAPApplicationVersion, error) + List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.CAPApplicationVersionList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.CAPApplicationVersion, err error) + Apply(ctx context.Context, cAPApplicationVersion *smesapcomv1alpha1.CAPApplicationVersionApplyConfiguration, opts v1.ApplyOptions) (result *v1alpha1.CAPApplicationVersion, err error) + ApplyStatus(ctx context.Context, cAPApplicationVersion *smesapcomv1alpha1.CAPApplicationVersionApplyConfiguration, opts v1.ApplyOptions) (result *v1alpha1.CAPApplicationVersion, err error) + CAPApplicationVersionExpansion +} + +// cAPApplicationVersions implements CAPApplicationVersionInterface +type cAPApplicationVersions struct { + client rest.Interface + ns string +} + +// newCAPApplicationVersions returns a CAPApplicationVersions +func newCAPApplicationVersions(c *SmeV1alpha1Client, namespace string) *cAPApplicationVersions { + return &cAPApplicationVersions{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the cAPApplicationVersion, and returns the corresponding cAPApplicationVersion object, and an error if there is any. +func (c *cAPApplicationVersions) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.CAPApplicationVersion, err error) { + result = &v1alpha1.CAPApplicationVersion{} + err = c.client.Get(). + Namespace(c.ns). + Resource("capapplicationversions"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(ctx). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of CAPApplicationVersions that match those selectors. +func (c *cAPApplicationVersions) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.CAPApplicationVersionList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1alpha1.CAPApplicationVersionList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("capapplicationversions"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(ctx). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested cAPApplicationVersions. +func (c *cAPApplicationVersions) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + opts.Watch = true + return c.client.Get(). + Namespace(c.ns). + Resource("capapplicationversions"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch(ctx) +} + +// Create takes the representation of a cAPApplicationVersion and creates it. Returns the server's representation of the cAPApplicationVersion, and an error, if there is any. +func (c *cAPApplicationVersions) Create(ctx context.Context, cAPApplicationVersion *v1alpha1.CAPApplicationVersion, opts v1.CreateOptions) (result *v1alpha1.CAPApplicationVersion, err error) { + result = &v1alpha1.CAPApplicationVersion{} + err = c.client.Post(). + Namespace(c.ns). + Resource("capapplicationversions"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(cAPApplicationVersion). + Do(ctx). + Into(result) + return +} + +// Update takes the representation of a cAPApplicationVersion and updates it. Returns the server's representation of the cAPApplicationVersion, and an error, if there is any. +func (c *cAPApplicationVersions) Update(ctx context.Context, cAPApplicationVersion *v1alpha1.CAPApplicationVersion, opts v1.UpdateOptions) (result *v1alpha1.CAPApplicationVersion, err error) { + result = &v1alpha1.CAPApplicationVersion{} + err = c.client.Put(). + Namespace(c.ns). + Resource("capapplicationversions"). + Name(cAPApplicationVersion.Name). + VersionedParams(&opts, scheme.ParameterCodec). + Body(cAPApplicationVersion). + Do(ctx). + Into(result) + return +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *cAPApplicationVersions) UpdateStatus(ctx context.Context, cAPApplicationVersion *v1alpha1.CAPApplicationVersion, opts v1.UpdateOptions) (result *v1alpha1.CAPApplicationVersion, err error) { + result = &v1alpha1.CAPApplicationVersion{} + err = c.client.Put(). + Namespace(c.ns). + Resource("capapplicationversions"). + Name(cAPApplicationVersion.Name). + SubResource("status"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(cAPApplicationVersion). + Do(ctx). + Into(result) + return +} + +// Delete takes name of the cAPApplicationVersion and deletes it. Returns an error if one occurs. +func (c *cAPApplicationVersions) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("capapplicationversions"). + Name(name). + Body(&opts). + Do(ctx). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *cAPApplicationVersions) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + var timeout time.Duration + if listOpts.TimeoutSeconds != nil { + timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second + } + return c.client.Delete(). + Namespace(c.ns). + Resource("capapplicationversions"). + VersionedParams(&listOpts, scheme.ParameterCodec). + Timeout(timeout). + Body(&opts). + Do(ctx). + Error() +} + +// Patch applies the patch and returns the patched cAPApplicationVersion. +func (c *cAPApplicationVersions) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.CAPApplicationVersion, err error) { + result = &v1alpha1.CAPApplicationVersion{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("capapplicationversions"). + Name(name). + SubResource(subresources...). + VersionedParams(&opts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} + +// Apply takes the given apply declarative configuration, applies it and returns the applied cAPApplicationVersion. +func (c *cAPApplicationVersions) Apply(ctx context.Context, cAPApplicationVersion *smesapcomv1alpha1.CAPApplicationVersionApplyConfiguration, opts v1.ApplyOptions) (result *v1alpha1.CAPApplicationVersion, err error) { + if cAPApplicationVersion == nil { + return nil, fmt.Errorf("cAPApplicationVersion provided to Apply must not be nil") + } + patchOpts := opts.ToPatchOptions() + data, err := json.Marshal(cAPApplicationVersion) + if err != nil { + return nil, err + } + name := cAPApplicationVersion.Name + if name == nil { + return nil, fmt.Errorf("cAPApplicationVersion.Name must be provided to Apply") + } + result = &v1alpha1.CAPApplicationVersion{} + err = c.client.Patch(types.ApplyPatchType). + Namespace(c.ns). + Resource("capapplicationversions"). + Name(*name). + VersionedParams(&patchOpts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} + +// ApplyStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating ApplyStatus(). +func (c *cAPApplicationVersions) ApplyStatus(ctx context.Context, cAPApplicationVersion *smesapcomv1alpha1.CAPApplicationVersionApplyConfiguration, opts v1.ApplyOptions) (result *v1alpha1.CAPApplicationVersion, err error) { + if cAPApplicationVersion == nil { + return nil, fmt.Errorf("cAPApplicationVersion provided to Apply must not be nil") + } + patchOpts := opts.ToPatchOptions() + data, err := json.Marshal(cAPApplicationVersion) + if err != nil { + return nil, err + } + + name := cAPApplicationVersion.Name + if name == nil { + return nil, fmt.Errorf("cAPApplicationVersion.Name must be provided to Apply") + } + + result = &v1alpha1.CAPApplicationVersion{} + err = c.client.Patch(types.ApplyPatchType). + Namespace(c.ns). + Resource("capapplicationversions"). + Name(*name). + SubResource("status"). + VersionedParams(&patchOpts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} diff --git a/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/captenant.go b/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/captenant.go new file mode 100644 index 0000000..19cb768 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/captenant.go @@ -0,0 +1,244 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + json "encoding/json" + "fmt" + "time" + + v1alpha1 "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + smesapcomv1alpha1 "github.com/sap/cap-operator/pkg/client/applyconfiguration/sme.sap.com/v1alpha1" + scheme "github.com/sap/cap-operator/pkg/client/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" +) + +// CAPTenantsGetter has a method to return a CAPTenantInterface. +// A group's client should implement this interface. +type CAPTenantsGetter interface { + CAPTenants(namespace string) CAPTenantInterface +} + +// CAPTenantInterface has methods to work with CAPTenant resources. +type CAPTenantInterface interface { + Create(ctx context.Context, cAPTenant *v1alpha1.CAPTenant, opts v1.CreateOptions) (*v1alpha1.CAPTenant, error) + Update(ctx context.Context, cAPTenant *v1alpha1.CAPTenant, opts v1.UpdateOptions) (*v1alpha1.CAPTenant, error) + UpdateStatus(ctx context.Context, cAPTenant *v1alpha1.CAPTenant, opts v1.UpdateOptions) (*v1alpha1.CAPTenant, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha1.CAPTenant, error) + List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.CAPTenantList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.CAPTenant, err error) + Apply(ctx context.Context, cAPTenant *smesapcomv1alpha1.CAPTenantApplyConfiguration, opts v1.ApplyOptions) (result *v1alpha1.CAPTenant, err error) + ApplyStatus(ctx context.Context, cAPTenant *smesapcomv1alpha1.CAPTenantApplyConfiguration, opts v1.ApplyOptions) (result *v1alpha1.CAPTenant, err error) + CAPTenantExpansion +} + +// cAPTenants implements CAPTenantInterface +type cAPTenants struct { + client rest.Interface + ns string +} + +// newCAPTenants returns a CAPTenants +func newCAPTenants(c *SmeV1alpha1Client, namespace string) *cAPTenants { + return &cAPTenants{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the cAPTenant, and returns the corresponding cAPTenant object, and an error if there is any. +func (c *cAPTenants) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.CAPTenant, err error) { + result = &v1alpha1.CAPTenant{} + err = c.client.Get(). + Namespace(c.ns). + Resource("captenants"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(ctx). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of CAPTenants that match those selectors. +func (c *cAPTenants) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.CAPTenantList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1alpha1.CAPTenantList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("captenants"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(ctx). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested cAPTenants. +func (c *cAPTenants) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + opts.Watch = true + return c.client.Get(). + Namespace(c.ns). + Resource("captenants"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch(ctx) +} + +// Create takes the representation of a cAPTenant and creates it. Returns the server's representation of the cAPTenant, and an error, if there is any. +func (c *cAPTenants) Create(ctx context.Context, cAPTenant *v1alpha1.CAPTenant, opts v1.CreateOptions) (result *v1alpha1.CAPTenant, err error) { + result = &v1alpha1.CAPTenant{} + err = c.client.Post(). + Namespace(c.ns). + Resource("captenants"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(cAPTenant). + Do(ctx). + Into(result) + return +} + +// Update takes the representation of a cAPTenant and updates it. Returns the server's representation of the cAPTenant, and an error, if there is any. +func (c *cAPTenants) Update(ctx context.Context, cAPTenant *v1alpha1.CAPTenant, opts v1.UpdateOptions) (result *v1alpha1.CAPTenant, err error) { + result = &v1alpha1.CAPTenant{} + err = c.client.Put(). + Namespace(c.ns). + Resource("captenants"). + Name(cAPTenant.Name). + VersionedParams(&opts, scheme.ParameterCodec). + Body(cAPTenant). + Do(ctx). + Into(result) + return +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *cAPTenants) UpdateStatus(ctx context.Context, cAPTenant *v1alpha1.CAPTenant, opts v1.UpdateOptions) (result *v1alpha1.CAPTenant, err error) { + result = &v1alpha1.CAPTenant{} + err = c.client.Put(). + Namespace(c.ns). + Resource("captenants"). + Name(cAPTenant.Name). + SubResource("status"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(cAPTenant). + Do(ctx). + Into(result) + return +} + +// Delete takes name of the cAPTenant and deletes it. Returns an error if one occurs. +func (c *cAPTenants) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("captenants"). + Name(name). + Body(&opts). + Do(ctx). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *cAPTenants) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + var timeout time.Duration + if listOpts.TimeoutSeconds != nil { + timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second + } + return c.client.Delete(). + Namespace(c.ns). + Resource("captenants"). + VersionedParams(&listOpts, scheme.ParameterCodec). + Timeout(timeout). + Body(&opts). + Do(ctx). + Error() +} + +// Patch applies the patch and returns the patched cAPTenant. +func (c *cAPTenants) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.CAPTenant, err error) { + result = &v1alpha1.CAPTenant{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("captenants"). + Name(name). + SubResource(subresources...). + VersionedParams(&opts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} + +// Apply takes the given apply declarative configuration, applies it and returns the applied cAPTenant. +func (c *cAPTenants) Apply(ctx context.Context, cAPTenant *smesapcomv1alpha1.CAPTenantApplyConfiguration, opts v1.ApplyOptions) (result *v1alpha1.CAPTenant, err error) { + if cAPTenant == nil { + return nil, fmt.Errorf("cAPTenant provided to Apply must not be nil") + } + patchOpts := opts.ToPatchOptions() + data, err := json.Marshal(cAPTenant) + if err != nil { + return nil, err + } + name := cAPTenant.Name + if name == nil { + return nil, fmt.Errorf("cAPTenant.Name must be provided to Apply") + } + result = &v1alpha1.CAPTenant{} + err = c.client.Patch(types.ApplyPatchType). + Namespace(c.ns). + Resource("captenants"). + Name(*name). + VersionedParams(&patchOpts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} + +// ApplyStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating ApplyStatus(). +func (c *cAPTenants) ApplyStatus(ctx context.Context, cAPTenant *smesapcomv1alpha1.CAPTenantApplyConfiguration, opts v1.ApplyOptions) (result *v1alpha1.CAPTenant, err error) { + if cAPTenant == nil { + return nil, fmt.Errorf("cAPTenant provided to Apply must not be nil") + } + patchOpts := opts.ToPatchOptions() + data, err := json.Marshal(cAPTenant) + if err != nil { + return nil, err + } + + name := cAPTenant.Name + if name == nil { + return nil, fmt.Errorf("cAPTenant.Name must be provided to Apply") + } + + result = &v1alpha1.CAPTenant{} + err = c.client.Patch(types.ApplyPatchType). + Namespace(c.ns). + Resource("captenants"). + Name(*name). + SubResource("status"). + VersionedParams(&patchOpts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} diff --git a/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/captenantoperation.go b/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/captenantoperation.go new file mode 100644 index 0000000..7d4f6a9 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/captenantoperation.go @@ -0,0 +1,244 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + json "encoding/json" + "fmt" + "time" + + v1alpha1 "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + smesapcomv1alpha1 "github.com/sap/cap-operator/pkg/client/applyconfiguration/sme.sap.com/v1alpha1" + scheme "github.com/sap/cap-operator/pkg/client/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" +) + +// CAPTenantOperationsGetter has a method to return a CAPTenantOperationInterface. +// A group's client should implement this interface. +type CAPTenantOperationsGetter interface { + CAPTenantOperations(namespace string) CAPTenantOperationInterface +} + +// CAPTenantOperationInterface has methods to work with CAPTenantOperation resources. +type CAPTenantOperationInterface interface { + Create(ctx context.Context, cAPTenantOperation *v1alpha1.CAPTenantOperation, opts v1.CreateOptions) (*v1alpha1.CAPTenantOperation, error) + Update(ctx context.Context, cAPTenantOperation *v1alpha1.CAPTenantOperation, opts v1.UpdateOptions) (*v1alpha1.CAPTenantOperation, error) + UpdateStatus(ctx context.Context, cAPTenantOperation *v1alpha1.CAPTenantOperation, opts v1.UpdateOptions) (*v1alpha1.CAPTenantOperation, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha1.CAPTenantOperation, error) + List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.CAPTenantOperationList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.CAPTenantOperation, err error) + Apply(ctx context.Context, cAPTenantOperation *smesapcomv1alpha1.CAPTenantOperationApplyConfiguration, opts v1.ApplyOptions) (result *v1alpha1.CAPTenantOperation, err error) + ApplyStatus(ctx context.Context, cAPTenantOperation *smesapcomv1alpha1.CAPTenantOperationApplyConfiguration, opts v1.ApplyOptions) (result *v1alpha1.CAPTenantOperation, err error) + CAPTenantOperationExpansion +} + +// cAPTenantOperations implements CAPTenantOperationInterface +type cAPTenantOperations struct { + client rest.Interface + ns string +} + +// newCAPTenantOperations returns a CAPTenantOperations +func newCAPTenantOperations(c *SmeV1alpha1Client, namespace string) *cAPTenantOperations { + return &cAPTenantOperations{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the cAPTenantOperation, and returns the corresponding cAPTenantOperation object, and an error if there is any. +func (c *cAPTenantOperations) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.CAPTenantOperation, err error) { + result = &v1alpha1.CAPTenantOperation{} + err = c.client.Get(). + Namespace(c.ns). + Resource("captenantoperations"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(ctx). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of CAPTenantOperations that match those selectors. +func (c *cAPTenantOperations) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.CAPTenantOperationList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1alpha1.CAPTenantOperationList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("captenantoperations"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(ctx). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested cAPTenantOperations. +func (c *cAPTenantOperations) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + opts.Watch = true + return c.client.Get(). + Namespace(c.ns). + Resource("captenantoperations"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch(ctx) +} + +// Create takes the representation of a cAPTenantOperation and creates it. Returns the server's representation of the cAPTenantOperation, and an error, if there is any. +func (c *cAPTenantOperations) Create(ctx context.Context, cAPTenantOperation *v1alpha1.CAPTenantOperation, opts v1.CreateOptions) (result *v1alpha1.CAPTenantOperation, err error) { + result = &v1alpha1.CAPTenantOperation{} + err = c.client.Post(). + Namespace(c.ns). + Resource("captenantoperations"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(cAPTenantOperation). + Do(ctx). + Into(result) + return +} + +// Update takes the representation of a cAPTenantOperation and updates it. Returns the server's representation of the cAPTenantOperation, and an error, if there is any. +func (c *cAPTenantOperations) Update(ctx context.Context, cAPTenantOperation *v1alpha1.CAPTenantOperation, opts v1.UpdateOptions) (result *v1alpha1.CAPTenantOperation, err error) { + result = &v1alpha1.CAPTenantOperation{} + err = c.client.Put(). + Namespace(c.ns). + Resource("captenantoperations"). + Name(cAPTenantOperation.Name). + VersionedParams(&opts, scheme.ParameterCodec). + Body(cAPTenantOperation). + Do(ctx). + Into(result) + return +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *cAPTenantOperations) UpdateStatus(ctx context.Context, cAPTenantOperation *v1alpha1.CAPTenantOperation, opts v1.UpdateOptions) (result *v1alpha1.CAPTenantOperation, err error) { + result = &v1alpha1.CAPTenantOperation{} + err = c.client.Put(). + Namespace(c.ns). + Resource("captenantoperations"). + Name(cAPTenantOperation.Name). + SubResource("status"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(cAPTenantOperation). + Do(ctx). + Into(result) + return +} + +// Delete takes name of the cAPTenantOperation and deletes it. Returns an error if one occurs. +func (c *cAPTenantOperations) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("captenantoperations"). + Name(name). + Body(&opts). + Do(ctx). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *cAPTenantOperations) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + var timeout time.Duration + if listOpts.TimeoutSeconds != nil { + timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second + } + return c.client.Delete(). + Namespace(c.ns). + Resource("captenantoperations"). + VersionedParams(&listOpts, scheme.ParameterCodec). + Timeout(timeout). + Body(&opts). + Do(ctx). + Error() +} + +// Patch applies the patch and returns the patched cAPTenantOperation. +func (c *cAPTenantOperations) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.CAPTenantOperation, err error) { + result = &v1alpha1.CAPTenantOperation{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("captenantoperations"). + Name(name). + SubResource(subresources...). + VersionedParams(&opts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} + +// Apply takes the given apply declarative configuration, applies it and returns the applied cAPTenantOperation. +func (c *cAPTenantOperations) Apply(ctx context.Context, cAPTenantOperation *smesapcomv1alpha1.CAPTenantOperationApplyConfiguration, opts v1.ApplyOptions) (result *v1alpha1.CAPTenantOperation, err error) { + if cAPTenantOperation == nil { + return nil, fmt.Errorf("cAPTenantOperation provided to Apply must not be nil") + } + patchOpts := opts.ToPatchOptions() + data, err := json.Marshal(cAPTenantOperation) + if err != nil { + return nil, err + } + name := cAPTenantOperation.Name + if name == nil { + return nil, fmt.Errorf("cAPTenantOperation.Name must be provided to Apply") + } + result = &v1alpha1.CAPTenantOperation{} + err = c.client.Patch(types.ApplyPatchType). + Namespace(c.ns). + Resource("captenantoperations"). + Name(*name). + VersionedParams(&patchOpts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} + +// ApplyStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating ApplyStatus(). +func (c *cAPTenantOperations) ApplyStatus(ctx context.Context, cAPTenantOperation *smesapcomv1alpha1.CAPTenantOperationApplyConfiguration, opts v1.ApplyOptions) (result *v1alpha1.CAPTenantOperation, err error) { + if cAPTenantOperation == nil { + return nil, fmt.Errorf("cAPTenantOperation provided to Apply must not be nil") + } + patchOpts := opts.ToPatchOptions() + data, err := json.Marshal(cAPTenantOperation) + if err != nil { + return nil, err + } + + name := cAPTenantOperation.Name + if name == nil { + return nil, fmt.Errorf("cAPTenantOperation.Name must be provided to Apply") + } + + result = &v1alpha1.CAPTenantOperation{} + err = c.client.Patch(types.ApplyPatchType). + Namespace(c.ns). + Resource("captenantoperations"). + Name(*name). + SubResource("status"). + VersionedParams(&patchOpts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} diff --git a/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/doc.go b/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/doc.go new file mode 100644 index 0000000..6b9df16 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/doc.go @@ -0,0 +1,8 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by client-gen. DO NOT EDIT. + +// This package has the automatically generated typed clients. +package v1alpha1 diff --git a/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/fake/doc.go b/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/fake/doc.go new file mode 100644 index 0000000..f8cf898 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/fake/doc.go @@ -0,0 +1,8 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by client-gen. DO NOT EDIT. + +// Package fake has the automatically generated clients. +package fake diff --git a/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/fake/fake_capapplication.go b/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/fake/fake_capapplication.go new file mode 100644 index 0000000..408dc20 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/fake/fake_capapplication.go @@ -0,0 +1,177 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + json "encoding/json" + "fmt" + + v1alpha1 "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + smesapcomv1alpha1 "github.com/sap/cap-operator/pkg/client/applyconfiguration/sme.sap.com/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeCAPApplications implements CAPApplicationInterface +type FakeCAPApplications struct { + Fake *FakeSmeV1alpha1 + ns string +} + +var capapplicationsResource = v1alpha1.SchemeGroupVersion.WithResource("capapplications") + +var capapplicationsKind = v1alpha1.SchemeGroupVersion.WithKind("CAPApplication") + +// Get takes name of the cAPApplication, and returns the corresponding cAPApplication object, and an error if there is any. +func (c *FakeCAPApplications) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.CAPApplication, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(capapplicationsResource, c.ns, name), &v1alpha1.CAPApplication{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.CAPApplication), err +} + +// List takes label and field selectors, and returns the list of CAPApplications that match those selectors. +func (c *FakeCAPApplications) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.CAPApplicationList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(capapplicationsResource, capapplicationsKind, c.ns, opts), &v1alpha1.CAPApplicationList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.CAPApplicationList{ListMeta: obj.(*v1alpha1.CAPApplicationList).ListMeta} + for _, item := range obj.(*v1alpha1.CAPApplicationList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested cAPApplications. +func (c *FakeCAPApplications) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(capapplicationsResource, c.ns, opts)) + +} + +// Create takes the representation of a cAPApplication and creates it. Returns the server's representation of the cAPApplication, and an error, if there is any. +func (c *FakeCAPApplications) Create(ctx context.Context, cAPApplication *v1alpha1.CAPApplication, opts v1.CreateOptions) (result *v1alpha1.CAPApplication, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(capapplicationsResource, c.ns, cAPApplication), &v1alpha1.CAPApplication{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.CAPApplication), err +} + +// Update takes the representation of a cAPApplication and updates it. Returns the server's representation of the cAPApplication, and an error, if there is any. +func (c *FakeCAPApplications) Update(ctx context.Context, cAPApplication *v1alpha1.CAPApplication, opts v1.UpdateOptions) (result *v1alpha1.CAPApplication, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(capapplicationsResource, c.ns, cAPApplication), &v1alpha1.CAPApplication{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.CAPApplication), err +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *FakeCAPApplications) UpdateStatus(ctx context.Context, cAPApplication *v1alpha1.CAPApplication, opts v1.UpdateOptions) (*v1alpha1.CAPApplication, error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateSubresourceAction(capapplicationsResource, "status", c.ns, cAPApplication), &v1alpha1.CAPApplication{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.CAPApplication), err +} + +// Delete takes name of the cAPApplication and deletes it. Returns an error if one occurs. +func (c *FakeCAPApplications) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteActionWithOptions(capapplicationsResource, c.ns, name, opts), &v1alpha1.CAPApplication{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeCAPApplications) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(capapplicationsResource, c.ns, listOpts) + + _, err := c.Fake.Invokes(action, &v1alpha1.CAPApplicationList{}) + return err +} + +// Patch applies the patch and returns the patched cAPApplication. +func (c *FakeCAPApplications) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.CAPApplication, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(capapplicationsResource, c.ns, name, pt, data, subresources...), &v1alpha1.CAPApplication{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.CAPApplication), err +} + +// Apply takes the given apply declarative configuration, applies it and returns the applied cAPApplication. +func (c *FakeCAPApplications) Apply(ctx context.Context, cAPApplication *smesapcomv1alpha1.CAPApplicationApplyConfiguration, opts v1.ApplyOptions) (result *v1alpha1.CAPApplication, err error) { + if cAPApplication == nil { + return nil, fmt.Errorf("cAPApplication provided to Apply must not be nil") + } + data, err := json.Marshal(cAPApplication) + if err != nil { + return nil, err + } + name := cAPApplication.Name + if name == nil { + return nil, fmt.Errorf("cAPApplication.Name must be provided to Apply") + } + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(capapplicationsResource, c.ns, *name, types.ApplyPatchType, data), &v1alpha1.CAPApplication{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.CAPApplication), err +} + +// ApplyStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating ApplyStatus(). +func (c *FakeCAPApplications) ApplyStatus(ctx context.Context, cAPApplication *smesapcomv1alpha1.CAPApplicationApplyConfiguration, opts v1.ApplyOptions) (result *v1alpha1.CAPApplication, err error) { + if cAPApplication == nil { + return nil, fmt.Errorf("cAPApplication provided to Apply must not be nil") + } + data, err := json.Marshal(cAPApplication) + if err != nil { + return nil, err + } + name := cAPApplication.Name + if name == nil { + return nil, fmt.Errorf("cAPApplication.Name must be provided to Apply") + } + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(capapplicationsResource, c.ns, *name, types.ApplyPatchType, data, "status"), &v1alpha1.CAPApplication{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.CAPApplication), err +} diff --git a/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/fake/fake_capapplicationversion.go b/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/fake/fake_capapplicationversion.go new file mode 100644 index 0000000..ce41d39 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/fake/fake_capapplicationversion.go @@ -0,0 +1,177 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + json "encoding/json" + "fmt" + + v1alpha1 "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + smesapcomv1alpha1 "github.com/sap/cap-operator/pkg/client/applyconfiguration/sme.sap.com/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeCAPApplicationVersions implements CAPApplicationVersionInterface +type FakeCAPApplicationVersions struct { + Fake *FakeSmeV1alpha1 + ns string +} + +var capapplicationversionsResource = v1alpha1.SchemeGroupVersion.WithResource("capapplicationversions") + +var capapplicationversionsKind = v1alpha1.SchemeGroupVersion.WithKind("CAPApplicationVersion") + +// Get takes name of the cAPApplicationVersion, and returns the corresponding cAPApplicationVersion object, and an error if there is any. +func (c *FakeCAPApplicationVersions) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.CAPApplicationVersion, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(capapplicationversionsResource, c.ns, name), &v1alpha1.CAPApplicationVersion{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.CAPApplicationVersion), err +} + +// List takes label and field selectors, and returns the list of CAPApplicationVersions that match those selectors. +func (c *FakeCAPApplicationVersions) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.CAPApplicationVersionList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(capapplicationversionsResource, capapplicationversionsKind, c.ns, opts), &v1alpha1.CAPApplicationVersionList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.CAPApplicationVersionList{ListMeta: obj.(*v1alpha1.CAPApplicationVersionList).ListMeta} + for _, item := range obj.(*v1alpha1.CAPApplicationVersionList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested cAPApplicationVersions. +func (c *FakeCAPApplicationVersions) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(capapplicationversionsResource, c.ns, opts)) + +} + +// Create takes the representation of a cAPApplicationVersion and creates it. Returns the server's representation of the cAPApplicationVersion, and an error, if there is any. +func (c *FakeCAPApplicationVersions) Create(ctx context.Context, cAPApplicationVersion *v1alpha1.CAPApplicationVersion, opts v1.CreateOptions) (result *v1alpha1.CAPApplicationVersion, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(capapplicationversionsResource, c.ns, cAPApplicationVersion), &v1alpha1.CAPApplicationVersion{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.CAPApplicationVersion), err +} + +// Update takes the representation of a cAPApplicationVersion and updates it. Returns the server's representation of the cAPApplicationVersion, and an error, if there is any. +func (c *FakeCAPApplicationVersions) Update(ctx context.Context, cAPApplicationVersion *v1alpha1.CAPApplicationVersion, opts v1.UpdateOptions) (result *v1alpha1.CAPApplicationVersion, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(capapplicationversionsResource, c.ns, cAPApplicationVersion), &v1alpha1.CAPApplicationVersion{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.CAPApplicationVersion), err +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *FakeCAPApplicationVersions) UpdateStatus(ctx context.Context, cAPApplicationVersion *v1alpha1.CAPApplicationVersion, opts v1.UpdateOptions) (*v1alpha1.CAPApplicationVersion, error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateSubresourceAction(capapplicationversionsResource, "status", c.ns, cAPApplicationVersion), &v1alpha1.CAPApplicationVersion{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.CAPApplicationVersion), err +} + +// Delete takes name of the cAPApplicationVersion and deletes it. Returns an error if one occurs. +func (c *FakeCAPApplicationVersions) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteActionWithOptions(capapplicationversionsResource, c.ns, name, opts), &v1alpha1.CAPApplicationVersion{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeCAPApplicationVersions) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(capapplicationversionsResource, c.ns, listOpts) + + _, err := c.Fake.Invokes(action, &v1alpha1.CAPApplicationVersionList{}) + return err +} + +// Patch applies the patch and returns the patched cAPApplicationVersion. +func (c *FakeCAPApplicationVersions) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.CAPApplicationVersion, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(capapplicationversionsResource, c.ns, name, pt, data, subresources...), &v1alpha1.CAPApplicationVersion{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.CAPApplicationVersion), err +} + +// Apply takes the given apply declarative configuration, applies it and returns the applied cAPApplicationVersion. +func (c *FakeCAPApplicationVersions) Apply(ctx context.Context, cAPApplicationVersion *smesapcomv1alpha1.CAPApplicationVersionApplyConfiguration, opts v1.ApplyOptions) (result *v1alpha1.CAPApplicationVersion, err error) { + if cAPApplicationVersion == nil { + return nil, fmt.Errorf("cAPApplicationVersion provided to Apply must not be nil") + } + data, err := json.Marshal(cAPApplicationVersion) + if err != nil { + return nil, err + } + name := cAPApplicationVersion.Name + if name == nil { + return nil, fmt.Errorf("cAPApplicationVersion.Name must be provided to Apply") + } + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(capapplicationversionsResource, c.ns, *name, types.ApplyPatchType, data), &v1alpha1.CAPApplicationVersion{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.CAPApplicationVersion), err +} + +// ApplyStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating ApplyStatus(). +func (c *FakeCAPApplicationVersions) ApplyStatus(ctx context.Context, cAPApplicationVersion *smesapcomv1alpha1.CAPApplicationVersionApplyConfiguration, opts v1.ApplyOptions) (result *v1alpha1.CAPApplicationVersion, err error) { + if cAPApplicationVersion == nil { + return nil, fmt.Errorf("cAPApplicationVersion provided to Apply must not be nil") + } + data, err := json.Marshal(cAPApplicationVersion) + if err != nil { + return nil, err + } + name := cAPApplicationVersion.Name + if name == nil { + return nil, fmt.Errorf("cAPApplicationVersion.Name must be provided to Apply") + } + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(capapplicationversionsResource, c.ns, *name, types.ApplyPatchType, data, "status"), &v1alpha1.CAPApplicationVersion{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.CAPApplicationVersion), err +} diff --git a/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/fake/fake_captenant.go b/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/fake/fake_captenant.go new file mode 100644 index 0000000..4f32862 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/fake/fake_captenant.go @@ -0,0 +1,177 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + json "encoding/json" + "fmt" + + v1alpha1 "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + smesapcomv1alpha1 "github.com/sap/cap-operator/pkg/client/applyconfiguration/sme.sap.com/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeCAPTenants implements CAPTenantInterface +type FakeCAPTenants struct { + Fake *FakeSmeV1alpha1 + ns string +} + +var captenantsResource = v1alpha1.SchemeGroupVersion.WithResource("captenants") + +var captenantsKind = v1alpha1.SchemeGroupVersion.WithKind("CAPTenant") + +// Get takes name of the cAPTenant, and returns the corresponding cAPTenant object, and an error if there is any. +func (c *FakeCAPTenants) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.CAPTenant, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(captenantsResource, c.ns, name), &v1alpha1.CAPTenant{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.CAPTenant), err +} + +// List takes label and field selectors, and returns the list of CAPTenants that match those selectors. +func (c *FakeCAPTenants) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.CAPTenantList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(captenantsResource, captenantsKind, c.ns, opts), &v1alpha1.CAPTenantList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.CAPTenantList{ListMeta: obj.(*v1alpha1.CAPTenantList).ListMeta} + for _, item := range obj.(*v1alpha1.CAPTenantList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested cAPTenants. +func (c *FakeCAPTenants) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(captenantsResource, c.ns, opts)) + +} + +// Create takes the representation of a cAPTenant and creates it. Returns the server's representation of the cAPTenant, and an error, if there is any. +func (c *FakeCAPTenants) Create(ctx context.Context, cAPTenant *v1alpha1.CAPTenant, opts v1.CreateOptions) (result *v1alpha1.CAPTenant, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(captenantsResource, c.ns, cAPTenant), &v1alpha1.CAPTenant{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.CAPTenant), err +} + +// Update takes the representation of a cAPTenant and updates it. Returns the server's representation of the cAPTenant, and an error, if there is any. +func (c *FakeCAPTenants) Update(ctx context.Context, cAPTenant *v1alpha1.CAPTenant, opts v1.UpdateOptions) (result *v1alpha1.CAPTenant, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(captenantsResource, c.ns, cAPTenant), &v1alpha1.CAPTenant{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.CAPTenant), err +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *FakeCAPTenants) UpdateStatus(ctx context.Context, cAPTenant *v1alpha1.CAPTenant, opts v1.UpdateOptions) (*v1alpha1.CAPTenant, error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateSubresourceAction(captenantsResource, "status", c.ns, cAPTenant), &v1alpha1.CAPTenant{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.CAPTenant), err +} + +// Delete takes name of the cAPTenant and deletes it. Returns an error if one occurs. +func (c *FakeCAPTenants) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteActionWithOptions(captenantsResource, c.ns, name, opts), &v1alpha1.CAPTenant{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeCAPTenants) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(captenantsResource, c.ns, listOpts) + + _, err := c.Fake.Invokes(action, &v1alpha1.CAPTenantList{}) + return err +} + +// Patch applies the patch and returns the patched cAPTenant. +func (c *FakeCAPTenants) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.CAPTenant, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(captenantsResource, c.ns, name, pt, data, subresources...), &v1alpha1.CAPTenant{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.CAPTenant), err +} + +// Apply takes the given apply declarative configuration, applies it and returns the applied cAPTenant. +func (c *FakeCAPTenants) Apply(ctx context.Context, cAPTenant *smesapcomv1alpha1.CAPTenantApplyConfiguration, opts v1.ApplyOptions) (result *v1alpha1.CAPTenant, err error) { + if cAPTenant == nil { + return nil, fmt.Errorf("cAPTenant provided to Apply must not be nil") + } + data, err := json.Marshal(cAPTenant) + if err != nil { + return nil, err + } + name := cAPTenant.Name + if name == nil { + return nil, fmt.Errorf("cAPTenant.Name must be provided to Apply") + } + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(captenantsResource, c.ns, *name, types.ApplyPatchType, data), &v1alpha1.CAPTenant{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.CAPTenant), err +} + +// ApplyStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating ApplyStatus(). +func (c *FakeCAPTenants) ApplyStatus(ctx context.Context, cAPTenant *smesapcomv1alpha1.CAPTenantApplyConfiguration, opts v1.ApplyOptions) (result *v1alpha1.CAPTenant, err error) { + if cAPTenant == nil { + return nil, fmt.Errorf("cAPTenant provided to Apply must not be nil") + } + data, err := json.Marshal(cAPTenant) + if err != nil { + return nil, err + } + name := cAPTenant.Name + if name == nil { + return nil, fmt.Errorf("cAPTenant.Name must be provided to Apply") + } + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(captenantsResource, c.ns, *name, types.ApplyPatchType, data, "status"), &v1alpha1.CAPTenant{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.CAPTenant), err +} diff --git a/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/fake/fake_captenantoperation.go b/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/fake/fake_captenantoperation.go new file mode 100644 index 0000000..5f7ff24 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/fake/fake_captenantoperation.go @@ -0,0 +1,177 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + json "encoding/json" + "fmt" + + v1alpha1 "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + smesapcomv1alpha1 "github.com/sap/cap-operator/pkg/client/applyconfiguration/sme.sap.com/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeCAPTenantOperations implements CAPTenantOperationInterface +type FakeCAPTenantOperations struct { + Fake *FakeSmeV1alpha1 + ns string +} + +var captenantoperationsResource = v1alpha1.SchemeGroupVersion.WithResource("captenantoperations") + +var captenantoperationsKind = v1alpha1.SchemeGroupVersion.WithKind("CAPTenantOperation") + +// Get takes name of the cAPTenantOperation, and returns the corresponding cAPTenantOperation object, and an error if there is any. +func (c *FakeCAPTenantOperations) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.CAPTenantOperation, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(captenantoperationsResource, c.ns, name), &v1alpha1.CAPTenantOperation{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.CAPTenantOperation), err +} + +// List takes label and field selectors, and returns the list of CAPTenantOperations that match those selectors. +func (c *FakeCAPTenantOperations) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.CAPTenantOperationList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(captenantoperationsResource, captenantoperationsKind, c.ns, opts), &v1alpha1.CAPTenantOperationList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.CAPTenantOperationList{ListMeta: obj.(*v1alpha1.CAPTenantOperationList).ListMeta} + for _, item := range obj.(*v1alpha1.CAPTenantOperationList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested cAPTenantOperations. +func (c *FakeCAPTenantOperations) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(captenantoperationsResource, c.ns, opts)) + +} + +// Create takes the representation of a cAPTenantOperation and creates it. Returns the server's representation of the cAPTenantOperation, and an error, if there is any. +func (c *FakeCAPTenantOperations) Create(ctx context.Context, cAPTenantOperation *v1alpha1.CAPTenantOperation, opts v1.CreateOptions) (result *v1alpha1.CAPTenantOperation, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(captenantoperationsResource, c.ns, cAPTenantOperation), &v1alpha1.CAPTenantOperation{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.CAPTenantOperation), err +} + +// Update takes the representation of a cAPTenantOperation and updates it. Returns the server's representation of the cAPTenantOperation, and an error, if there is any. +func (c *FakeCAPTenantOperations) Update(ctx context.Context, cAPTenantOperation *v1alpha1.CAPTenantOperation, opts v1.UpdateOptions) (result *v1alpha1.CAPTenantOperation, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(captenantoperationsResource, c.ns, cAPTenantOperation), &v1alpha1.CAPTenantOperation{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.CAPTenantOperation), err +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *FakeCAPTenantOperations) UpdateStatus(ctx context.Context, cAPTenantOperation *v1alpha1.CAPTenantOperation, opts v1.UpdateOptions) (*v1alpha1.CAPTenantOperation, error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateSubresourceAction(captenantoperationsResource, "status", c.ns, cAPTenantOperation), &v1alpha1.CAPTenantOperation{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.CAPTenantOperation), err +} + +// Delete takes name of the cAPTenantOperation and deletes it. Returns an error if one occurs. +func (c *FakeCAPTenantOperations) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteActionWithOptions(captenantoperationsResource, c.ns, name, opts), &v1alpha1.CAPTenantOperation{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeCAPTenantOperations) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(captenantoperationsResource, c.ns, listOpts) + + _, err := c.Fake.Invokes(action, &v1alpha1.CAPTenantOperationList{}) + return err +} + +// Patch applies the patch and returns the patched cAPTenantOperation. +func (c *FakeCAPTenantOperations) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.CAPTenantOperation, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(captenantoperationsResource, c.ns, name, pt, data, subresources...), &v1alpha1.CAPTenantOperation{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.CAPTenantOperation), err +} + +// Apply takes the given apply declarative configuration, applies it and returns the applied cAPTenantOperation. +func (c *FakeCAPTenantOperations) Apply(ctx context.Context, cAPTenantOperation *smesapcomv1alpha1.CAPTenantOperationApplyConfiguration, opts v1.ApplyOptions) (result *v1alpha1.CAPTenantOperation, err error) { + if cAPTenantOperation == nil { + return nil, fmt.Errorf("cAPTenantOperation provided to Apply must not be nil") + } + data, err := json.Marshal(cAPTenantOperation) + if err != nil { + return nil, err + } + name := cAPTenantOperation.Name + if name == nil { + return nil, fmt.Errorf("cAPTenantOperation.Name must be provided to Apply") + } + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(captenantoperationsResource, c.ns, *name, types.ApplyPatchType, data), &v1alpha1.CAPTenantOperation{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.CAPTenantOperation), err +} + +// ApplyStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating ApplyStatus(). +func (c *FakeCAPTenantOperations) ApplyStatus(ctx context.Context, cAPTenantOperation *smesapcomv1alpha1.CAPTenantOperationApplyConfiguration, opts v1.ApplyOptions) (result *v1alpha1.CAPTenantOperation, err error) { + if cAPTenantOperation == nil { + return nil, fmt.Errorf("cAPTenantOperation provided to Apply must not be nil") + } + data, err := json.Marshal(cAPTenantOperation) + if err != nil { + return nil, err + } + name := cAPTenantOperation.Name + if name == nil { + return nil, fmt.Errorf("cAPTenantOperation.Name must be provided to Apply") + } + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(captenantoperationsResource, c.ns, *name, types.ApplyPatchType, data, "status"), &v1alpha1.CAPTenantOperation{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.CAPTenantOperation), err +} diff --git a/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/fake/fake_sme.sap.com_client.go b/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/fake/fake_sme.sap.com_client.go new file mode 100644 index 0000000..239fc90 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/fake/fake_sme.sap.com_client.go @@ -0,0 +1,40 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + v1alpha1 "github.com/sap/cap-operator/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1" + rest "k8s.io/client-go/rest" + testing "k8s.io/client-go/testing" +) + +type FakeSmeV1alpha1 struct { + *testing.Fake +} + +func (c *FakeSmeV1alpha1) CAPApplications(namespace string) v1alpha1.CAPApplicationInterface { + return &FakeCAPApplications{c, namespace} +} + +func (c *FakeSmeV1alpha1) CAPApplicationVersions(namespace string) v1alpha1.CAPApplicationVersionInterface { + return &FakeCAPApplicationVersions{c, namespace} +} + +func (c *FakeSmeV1alpha1) CAPTenants(namespace string) v1alpha1.CAPTenantInterface { + return &FakeCAPTenants{c, namespace} +} + +func (c *FakeSmeV1alpha1) CAPTenantOperations(namespace string) v1alpha1.CAPTenantOperationInterface { + return &FakeCAPTenantOperations{c, namespace} +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *FakeSmeV1alpha1) RESTClient() rest.Interface { + var ret *rest.RESTClient + return ret +} diff --git a/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/generated_expansion.go b/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/generated_expansion.go new file mode 100644 index 0000000..bb475c0 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/generated_expansion.go @@ -0,0 +1,15 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +type CAPApplicationExpansion interface{} + +type CAPApplicationVersionExpansion interface{} + +type CAPTenantExpansion interface{} + +type CAPTenantOperationExpansion interface{} diff --git a/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/sme.sap.com_client.go b/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/sme.sap.com_client.go new file mode 100644 index 0000000..21feefc --- /dev/null +++ b/pkg/client/clientset/versioned/typed/sme.sap.com/v1alpha1/sme.sap.com_client.go @@ -0,0 +1,110 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "net/http" + + v1alpha1 "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + "github.com/sap/cap-operator/pkg/client/clientset/versioned/scheme" + rest "k8s.io/client-go/rest" +) + +type SmeV1alpha1Interface interface { + RESTClient() rest.Interface + CAPApplicationsGetter + CAPApplicationVersionsGetter + CAPTenantsGetter + CAPTenantOperationsGetter +} + +// SmeV1alpha1Client is used to interact with features provided by the sme.sap.com group. +type SmeV1alpha1Client struct { + restClient rest.Interface +} + +func (c *SmeV1alpha1Client) CAPApplications(namespace string) CAPApplicationInterface { + return newCAPApplications(c, namespace) +} + +func (c *SmeV1alpha1Client) CAPApplicationVersions(namespace string) CAPApplicationVersionInterface { + return newCAPApplicationVersions(c, namespace) +} + +func (c *SmeV1alpha1Client) CAPTenants(namespace string) CAPTenantInterface { + return newCAPTenants(c, namespace) +} + +func (c *SmeV1alpha1Client) CAPTenantOperations(namespace string) CAPTenantOperationInterface { + return newCAPTenantOperations(c, namespace) +} + +// NewForConfig creates a new SmeV1alpha1Client for the given config. +// NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), +// where httpClient was generated with rest.HTTPClientFor(c). +func NewForConfig(c *rest.Config) (*SmeV1alpha1Client, error) { + config := *c + if err := setConfigDefaults(&config); err != nil { + return nil, err + } + httpClient, err := rest.HTTPClientFor(&config) + if err != nil { + return nil, err + } + return NewForConfigAndClient(&config, httpClient) +} + +// NewForConfigAndClient creates a new SmeV1alpha1Client for the given config and http client. +// Note the http client provided takes precedence over the configured transport values. +func NewForConfigAndClient(c *rest.Config, h *http.Client) (*SmeV1alpha1Client, error) { + config := *c + if err := setConfigDefaults(&config); err != nil { + return nil, err + } + client, err := rest.RESTClientForConfigAndClient(&config, h) + if err != nil { + return nil, err + } + return &SmeV1alpha1Client{client}, nil +} + +// NewForConfigOrDie creates a new SmeV1alpha1Client for the given config and +// panics if there is an error in the config. +func NewForConfigOrDie(c *rest.Config) *SmeV1alpha1Client { + client, err := NewForConfig(c) + if err != nil { + panic(err) + } + return client +} + +// New creates a new SmeV1alpha1Client for the given RESTClient. +func New(c rest.Interface) *SmeV1alpha1Client { + return &SmeV1alpha1Client{c} +} + +func setConfigDefaults(config *rest.Config) error { + gv := v1alpha1.SchemeGroupVersion + config.GroupVersion = &gv + config.APIPath = "/apis" + config.NegotiatedSerializer = scheme.Codecs.WithoutConversion() + + if config.UserAgent == "" { + config.UserAgent = rest.DefaultKubernetesUserAgent() + } + + return nil +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *SmeV1alpha1Client) RESTClient() rest.Interface { + if c == nil { + return nil + } + return c.restClient +} diff --git a/pkg/client/informers/externalversions/factory.go b/pkg/client/informers/externalversions/factory.go new file mode 100644 index 0000000..32e1513 --- /dev/null +++ b/pkg/client/informers/externalversions/factory.go @@ -0,0 +1,239 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by informer-gen. DO NOT EDIT. + +package externalversions + +import ( + reflect "reflect" + sync "sync" + time "time" + + versioned "github.com/sap/cap-operator/pkg/client/clientset/versioned" + internalinterfaces "github.com/sap/cap-operator/pkg/client/informers/externalversions/internalinterfaces" + smesapcom "github.com/sap/cap-operator/pkg/client/informers/externalversions/sme.sap.com" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + schema "k8s.io/apimachinery/pkg/runtime/schema" + cache "k8s.io/client-go/tools/cache" +) + +// SharedInformerOption defines the functional option type for SharedInformerFactory. +type SharedInformerOption func(*sharedInformerFactory) *sharedInformerFactory + +type sharedInformerFactory struct { + client versioned.Interface + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc + lock sync.Mutex + defaultResync time.Duration + customResync map[reflect.Type]time.Duration + + informers map[reflect.Type]cache.SharedIndexInformer + // startedInformers is used for tracking which informers have been started. + // This allows Start() to be called multiple times safely. + startedInformers map[reflect.Type]bool + // wg tracks how many goroutines were started. + wg sync.WaitGroup + // shuttingDown is true when Shutdown has been called. It may still be running + // because it needs to wait for goroutines. + shuttingDown bool +} + +// WithCustomResyncConfig sets a custom resync period for the specified informer types. +func WithCustomResyncConfig(resyncConfig map[v1.Object]time.Duration) SharedInformerOption { + return func(factory *sharedInformerFactory) *sharedInformerFactory { + for k, v := range resyncConfig { + factory.customResync[reflect.TypeOf(k)] = v + } + return factory + } +} + +// WithTweakListOptions sets a custom filter on all listers of the configured SharedInformerFactory. +func WithTweakListOptions(tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerOption { + return func(factory *sharedInformerFactory) *sharedInformerFactory { + factory.tweakListOptions = tweakListOptions + return factory + } +} + +// WithNamespace limits the SharedInformerFactory to the specified namespace. +func WithNamespace(namespace string) SharedInformerOption { + return func(factory *sharedInformerFactory) *sharedInformerFactory { + factory.namespace = namespace + return factory + } +} + +// NewSharedInformerFactory constructs a new instance of sharedInformerFactory for all namespaces. +func NewSharedInformerFactory(client versioned.Interface, defaultResync time.Duration) SharedInformerFactory { + return NewSharedInformerFactoryWithOptions(client, defaultResync) +} + +// NewFilteredSharedInformerFactory constructs a new instance of sharedInformerFactory. +// Listers obtained via this SharedInformerFactory will be subject to the same filters +// as specified here. +// Deprecated: Please use NewSharedInformerFactoryWithOptions instead +func NewFilteredSharedInformerFactory(client versioned.Interface, defaultResync time.Duration, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerFactory { + return NewSharedInformerFactoryWithOptions(client, defaultResync, WithNamespace(namespace), WithTweakListOptions(tweakListOptions)) +} + +// NewSharedInformerFactoryWithOptions constructs a new instance of a SharedInformerFactory with additional options. +func NewSharedInformerFactoryWithOptions(client versioned.Interface, defaultResync time.Duration, options ...SharedInformerOption) SharedInformerFactory { + factory := &sharedInformerFactory{ + client: client, + namespace: v1.NamespaceAll, + defaultResync: defaultResync, + informers: make(map[reflect.Type]cache.SharedIndexInformer), + startedInformers: make(map[reflect.Type]bool), + customResync: make(map[reflect.Type]time.Duration), + } + + // Apply all options + for _, opt := range options { + factory = opt(factory) + } + + return factory +} + +func (f *sharedInformerFactory) Start(stopCh <-chan struct{}) { + f.lock.Lock() + defer f.lock.Unlock() + + if f.shuttingDown { + return + } + + for informerType, informer := range f.informers { + if !f.startedInformers[informerType] { + f.wg.Add(1) + // We need a new variable in each loop iteration, + // otherwise the goroutine would use the loop variable + // and that keeps changing. + informer := informer + go func() { + defer f.wg.Done() + informer.Run(stopCh) + }() + f.startedInformers[informerType] = true + } + } +} + +func (f *sharedInformerFactory) Shutdown() { + f.lock.Lock() + f.shuttingDown = true + f.lock.Unlock() + + // Will return immediately if there is nothing to wait for. + f.wg.Wait() +} + +func (f *sharedInformerFactory) WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool { + informers := func() map[reflect.Type]cache.SharedIndexInformer { + f.lock.Lock() + defer f.lock.Unlock() + + informers := map[reflect.Type]cache.SharedIndexInformer{} + for informerType, informer := range f.informers { + if f.startedInformers[informerType] { + informers[informerType] = informer + } + } + return informers + }() + + res := map[reflect.Type]bool{} + for informType, informer := range informers { + res[informType] = cache.WaitForCacheSync(stopCh, informer.HasSynced) + } + return res +} + +// InformerFor returns the SharedIndexInformer for obj using an internal +// client. +func (f *sharedInformerFactory) InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer { + f.lock.Lock() + defer f.lock.Unlock() + + informerType := reflect.TypeOf(obj) + informer, exists := f.informers[informerType] + if exists { + return informer + } + + resyncPeriod, exists := f.customResync[informerType] + if !exists { + resyncPeriod = f.defaultResync + } + + informer = newFunc(f.client, resyncPeriod) + f.informers[informerType] = informer + + return informer +} + +// SharedInformerFactory provides shared informers for resources in all known +// API group versions. +// +// It is typically used like this: +// +// ctx, cancel := context.Background() +// defer cancel() +// factory := NewSharedInformerFactory(client, resyncPeriod) +// defer factory.WaitForStop() // Returns immediately if nothing was started. +// genericInformer := factory.ForResource(resource) +// typedInformer := factory.SomeAPIGroup().V1().SomeType() +// factory.Start(ctx.Done()) // Start processing these informers. +// synced := factory.WaitForCacheSync(ctx.Done()) +// for v, ok := range synced { +// if !ok { +// fmt.Fprintf(os.Stderr, "caches failed to sync: %v", v) +// return +// } +// } +// +// // Creating informers can also be created after Start, but then +// // Start must be called again: +// anotherGenericInformer := factory.ForResource(resource) +// factory.Start(ctx.Done()) +type SharedInformerFactory interface { + internalinterfaces.SharedInformerFactory + + // Start initializes all requested informers. They are handled in goroutines + // which run until the stop channel gets closed. + Start(stopCh <-chan struct{}) + + // Shutdown marks a factory as shutting down. At that point no new + // informers can be started anymore and Start will return without + // doing anything. + // + // In addition, Shutdown blocks until all goroutines have terminated. For that + // to happen, the close channel(s) that they were started with must be closed, + // either before Shutdown gets called or while it is waiting. + // + // Shutdown may be called multiple times, even concurrently. All such calls will + // block until all goroutines have terminated. + Shutdown() + + // WaitForCacheSync blocks until all started informers' caches were synced + // or the stop channel gets closed. + WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool + + // ForResource gives generic access to a shared informer of the matching type. + ForResource(resource schema.GroupVersionResource) (GenericInformer, error) + + // InformerFor returns the SharedIndexInformer for obj using an internal + // client. + InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer + + Sme() smesapcom.Interface +} + +func (f *sharedInformerFactory) Sme() smesapcom.Interface { + return smesapcom.New(f, f.namespace, f.tweakListOptions) +} diff --git a/pkg/client/informers/externalversions/generic.go b/pkg/client/informers/externalversions/generic.go new file mode 100644 index 0000000..fbe7916 --- /dev/null +++ b/pkg/client/informers/externalversions/generic.go @@ -0,0 +1,56 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by informer-gen. DO NOT EDIT. + +package externalversions + +import ( + "fmt" + + v1alpha1 "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + schema "k8s.io/apimachinery/pkg/runtime/schema" + cache "k8s.io/client-go/tools/cache" +) + +// GenericInformer is type of SharedIndexInformer which will locate and delegate to other +// sharedInformers based on type +type GenericInformer interface { + Informer() cache.SharedIndexInformer + Lister() cache.GenericLister +} + +type genericInformer struct { + informer cache.SharedIndexInformer + resource schema.GroupResource +} + +// Informer returns the SharedIndexInformer. +func (f *genericInformer) Informer() cache.SharedIndexInformer { + return f.informer +} + +// Lister returns the GenericLister. +func (f *genericInformer) Lister() cache.GenericLister { + return cache.NewGenericLister(f.Informer().GetIndexer(), f.resource) +} + +// ForResource gives generic access to a shared informer of the matching type +// TODO extend this to unknown resources with a client pool +func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) { + switch resource { + // Group=sme.sap.com, Version=v1alpha1 + case v1alpha1.SchemeGroupVersion.WithResource("capapplications"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Sme().V1alpha1().CAPApplications().Informer()}, nil + case v1alpha1.SchemeGroupVersion.WithResource("capapplicationversions"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Sme().V1alpha1().CAPApplicationVersions().Informer()}, nil + case v1alpha1.SchemeGroupVersion.WithResource("captenants"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Sme().V1alpha1().CAPTenants().Informer()}, nil + case v1alpha1.SchemeGroupVersion.WithResource("captenantoperations"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Sme().V1alpha1().CAPTenantOperations().Informer()}, nil + + } + + return nil, fmt.Errorf("no informer found for %v", resource) +} diff --git a/pkg/client/informers/externalversions/internalinterfaces/factory_interfaces.go b/pkg/client/informers/externalversions/internalinterfaces/factory_interfaces.go new file mode 100644 index 0000000..b2b93ad --- /dev/null +++ b/pkg/client/informers/externalversions/internalinterfaces/factory_interfaces.go @@ -0,0 +1,28 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by informer-gen. DO NOT EDIT. + +package internalinterfaces + +import ( + time "time" + + versioned "github.com/sap/cap-operator/pkg/client/clientset/versioned" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + cache "k8s.io/client-go/tools/cache" +) + +// NewInformerFunc takes versioned.Interface and time.Duration to return a SharedIndexInformer. +type NewInformerFunc func(versioned.Interface, time.Duration) cache.SharedIndexInformer + +// SharedInformerFactory a small interface to allow for adding an informer without an import cycle +type SharedInformerFactory interface { + Start(stopCh <-chan struct{}) + InformerFor(obj runtime.Object, newFunc NewInformerFunc) cache.SharedIndexInformer +} + +// TweakListOptionsFunc is a function that transforms a v1.ListOptions. +type TweakListOptionsFunc func(*v1.ListOptions) diff --git a/pkg/client/informers/externalversions/sme.sap.com/interface.go b/pkg/client/informers/externalversions/sme.sap.com/interface.go new file mode 100644 index 0000000..f52db38 --- /dev/null +++ b/pkg/client/informers/externalversions/sme.sap.com/interface.go @@ -0,0 +1,34 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by informer-gen. DO NOT EDIT. + +package sme + +import ( + internalinterfaces "github.com/sap/cap-operator/pkg/client/informers/externalversions/internalinterfaces" + v1alpha1 "github.com/sap/cap-operator/pkg/client/informers/externalversions/sme.sap.com/v1alpha1" +) + +// Interface provides access to each of this group's versions. +type Interface interface { + // V1alpha1 provides access to shared informers for resources in V1alpha1. + V1alpha1() v1alpha1.Interface +} + +type group struct { + factory internalinterfaces.SharedInformerFactory + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// New returns a new Interface. +func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { + return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} +} + +// V1alpha1 returns a new v1alpha1.Interface. +func (g *group) V1alpha1() v1alpha1.Interface { + return v1alpha1.New(g.factory, g.namespace, g.tweakListOptions) +} diff --git a/pkg/client/informers/externalversions/sme.sap.com/v1alpha1/capapplication.go b/pkg/client/informers/externalversions/sme.sap.com/v1alpha1/capapplication.go new file mode 100644 index 0000000..33eab36 --- /dev/null +++ b/pkg/client/informers/externalversions/sme.sap.com/v1alpha1/capapplication.go @@ -0,0 +1,78 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + time "time" + + smesapcomv1alpha1 "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + versioned "github.com/sap/cap-operator/pkg/client/clientset/versioned" + internalinterfaces "github.com/sap/cap-operator/pkg/client/informers/externalversions/internalinterfaces" + v1alpha1 "github.com/sap/cap-operator/pkg/client/listers/sme.sap.com/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// CAPApplicationInformer provides access to a shared informer and lister for +// CAPApplications. +type CAPApplicationInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.CAPApplicationLister +} + +type cAPApplicationInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewCAPApplicationInformer constructs a new informer for CAPApplication type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewCAPApplicationInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredCAPApplicationInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredCAPApplicationInformer constructs a new informer for CAPApplication type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredCAPApplicationInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.SmeV1alpha1().CAPApplications(namespace).List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.SmeV1alpha1().CAPApplications(namespace).Watch(context.TODO(), options) + }, + }, + &smesapcomv1alpha1.CAPApplication{}, + resyncPeriod, + indexers, + ) +} + +func (f *cAPApplicationInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredCAPApplicationInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *cAPApplicationInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&smesapcomv1alpha1.CAPApplication{}, f.defaultInformer) +} + +func (f *cAPApplicationInformer) Lister() v1alpha1.CAPApplicationLister { + return v1alpha1.NewCAPApplicationLister(f.Informer().GetIndexer()) +} diff --git a/pkg/client/informers/externalversions/sme.sap.com/v1alpha1/capapplicationversion.go b/pkg/client/informers/externalversions/sme.sap.com/v1alpha1/capapplicationversion.go new file mode 100644 index 0000000..66f44f6 --- /dev/null +++ b/pkg/client/informers/externalversions/sme.sap.com/v1alpha1/capapplicationversion.go @@ -0,0 +1,78 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + time "time" + + smesapcomv1alpha1 "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + versioned "github.com/sap/cap-operator/pkg/client/clientset/versioned" + internalinterfaces "github.com/sap/cap-operator/pkg/client/informers/externalversions/internalinterfaces" + v1alpha1 "github.com/sap/cap-operator/pkg/client/listers/sme.sap.com/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// CAPApplicationVersionInformer provides access to a shared informer and lister for +// CAPApplicationVersions. +type CAPApplicationVersionInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.CAPApplicationVersionLister +} + +type cAPApplicationVersionInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewCAPApplicationVersionInformer constructs a new informer for CAPApplicationVersion type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewCAPApplicationVersionInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredCAPApplicationVersionInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredCAPApplicationVersionInformer constructs a new informer for CAPApplicationVersion type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredCAPApplicationVersionInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.SmeV1alpha1().CAPApplicationVersions(namespace).List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.SmeV1alpha1().CAPApplicationVersions(namespace).Watch(context.TODO(), options) + }, + }, + &smesapcomv1alpha1.CAPApplicationVersion{}, + resyncPeriod, + indexers, + ) +} + +func (f *cAPApplicationVersionInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredCAPApplicationVersionInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *cAPApplicationVersionInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&smesapcomv1alpha1.CAPApplicationVersion{}, f.defaultInformer) +} + +func (f *cAPApplicationVersionInformer) Lister() v1alpha1.CAPApplicationVersionLister { + return v1alpha1.NewCAPApplicationVersionLister(f.Informer().GetIndexer()) +} diff --git a/pkg/client/informers/externalversions/sme.sap.com/v1alpha1/captenant.go b/pkg/client/informers/externalversions/sme.sap.com/v1alpha1/captenant.go new file mode 100644 index 0000000..27dd930 --- /dev/null +++ b/pkg/client/informers/externalversions/sme.sap.com/v1alpha1/captenant.go @@ -0,0 +1,78 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + time "time" + + smesapcomv1alpha1 "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + versioned "github.com/sap/cap-operator/pkg/client/clientset/versioned" + internalinterfaces "github.com/sap/cap-operator/pkg/client/informers/externalversions/internalinterfaces" + v1alpha1 "github.com/sap/cap-operator/pkg/client/listers/sme.sap.com/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// CAPTenantInformer provides access to a shared informer and lister for +// CAPTenants. +type CAPTenantInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.CAPTenantLister +} + +type cAPTenantInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewCAPTenantInformer constructs a new informer for CAPTenant type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewCAPTenantInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredCAPTenantInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredCAPTenantInformer constructs a new informer for CAPTenant type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredCAPTenantInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.SmeV1alpha1().CAPTenants(namespace).List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.SmeV1alpha1().CAPTenants(namespace).Watch(context.TODO(), options) + }, + }, + &smesapcomv1alpha1.CAPTenant{}, + resyncPeriod, + indexers, + ) +} + +func (f *cAPTenantInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredCAPTenantInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *cAPTenantInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&smesapcomv1alpha1.CAPTenant{}, f.defaultInformer) +} + +func (f *cAPTenantInformer) Lister() v1alpha1.CAPTenantLister { + return v1alpha1.NewCAPTenantLister(f.Informer().GetIndexer()) +} diff --git a/pkg/client/informers/externalversions/sme.sap.com/v1alpha1/captenantoperation.go b/pkg/client/informers/externalversions/sme.sap.com/v1alpha1/captenantoperation.go new file mode 100644 index 0000000..5645022 --- /dev/null +++ b/pkg/client/informers/externalversions/sme.sap.com/v1alpha1/captenantoperation.go @@ -0,0 +1,78 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + time "time" + + smesapcomv1alpha1 "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + versioned "github.com/sap/cap-operator/pkg/client/clientset/versioned" + internalinterfaces "github.com/sap/cap-operator/pkg/client/informers/externalversions/internalinterfaces" + v1alpha1 "github.com/sap/cap-operator/pkg/client/listers/sme.sap.com/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// CAPTenantOperationInformer provides access to a shared informer and lister for +// CAPTenantOperations. +type CAPTenantOperationInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.CAPTenantOperationLister +} + +type cAPTenantOperationInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewCAPTenantOperationInformer constructs a new informer for CAPTenantOperation type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewCAPTenantOperationInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredCAPTenantOperationInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredCAPTenantOperationInformer constructs a new informer for CAPTenantOperation type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredCAPTenantOperationInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.SmeV1alpha1().CAPTenantOperations(namespace).List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.SmeV1alpha1().CAPTenantOperations(namespace).Watch(context.TODO(), options) + }, + }, + &smesapcomv1alpha1.CAPTenantOperation{}, + resyncPeriod, + indexers, + ) +} + +func (f *cAPTenantOperationInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredCAPTenantOperationInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *cAPTenantOperationInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&smesapcomv1alpha1.CAPTenantOperation{}, f.defaultInformer) +} + +func (f *cAPTenantOperationInformer) Lister() v1alpha1.CAPTenantOperationLister { + return v1alpha1.NewCAPTenantOperationLister(f.Informer().GetIndexer()) +} diff --git a/pkg/client/informers/externalversions/sme.sap.com/v1alpha1/interface.go b/pkg/client/informers/externalversions/sme.sap.com/v1alpha1/interface.go new file mode 100644 index 0000000..7165764 --- /dev/null +++ b/pkg/client/informers/externalversions/sme.sap.com/v1alpha1/interface.go @@ -0,0 +1,54 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + internalinterfaces "github.com/sap/cap-operator/pkg/client/informers/externalversions/internalinterfaces" +) + +// Interface provides access to all the informers in this group version. +type Interface interface { + // CAPApplications returns a CAPApplicationInformer. + CAPApplications() CAPApplicationInformer + // CAPApplicationVersions returns a CAPApplicationVersionInformer. + CAPApplicationVersions() CAPApplicationVersionInformer + // CAPTenants returns a CAPTenantInformer. + CAPTenants() CAPTenantInformer + // CAPTenantOperations returns a CAPTenantOperationInformer. + CAPTenantOperations() CAPTenantOperationInformer +} + +type version struct { + factory internalinterfaces.SharedInformerFactory + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// New returns a new Interface. +func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { + return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} +} + +// CAPApplications returns a CAPApplicationInformer. +func (v *version) CAPApplications() CAPApplicationInformer { + return &cAPApplicationInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + +// CAPApplicationVersions returns a CAPApplicationVersionInformer. +func (v *version) CAPApplicationVersions() CAPApplicationVersionInformer { + return &cAPApplicationVersionInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + +// CAPTenants returns a CAPTenantInformer. +func (v *version) CAPTenants() CAPTenantInformer { + return &cAPTenantInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + +// CAPTenantOperations returns a CAPTenantOperationInformer. +func (v *version) CAPTenantOperations() CAPTenantOperationInformer { + return &cAPTenantOperationInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} diff --git a/pkg/client/listers/sme.sap.com/v1alpha1/capapplication.go b/pkg/client/listers/sme.sap.com/v1alpha1/capapplication.go new file mode 100644 index 0000000..6be54bf --- /dev/null +++ b/pkg/client/listers/sme.sap.com/v1alpha1/capapplication.go @@ -0,0 +1,87 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1alpha1 "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// CAPApplicationLister helps list CAPApplications. +// All objects returned here must be treated as read-only. +type CAPApplicationLister interface { + // List lists all CAPApplications in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.CAPApplication, err error) + // CAPApplications returns an object that can list and get CAPApplications. + CAPApplications(namespace string) CAPApplicationNamespaceLister + CAPApplicationListerExpansion +} + +// cAPApplicationLister implements the CAPApplicationLister interface. +type cAPApplicationLister struct { + indexer cache.Indexer +} + +// NewCAPApplicationLister returns a new CAPApplicationLister. +func NewCAPApplicationLister(indexer cache.Indexer) CAPApplicationLister { + return &cAPApplicationLister{indexer: indexer} +} + +// List lists all CAPApplications in the indexer. +func (s *cAPApplicationLister) List(selector labels.Selector) (ret []*v1alpha1.CAPApplication, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.CAPApplication)) + }) + return ret, err +} + +// CAPApplications returns an object that can list and get CAPApplications. +func (s *cAPApplicationLister) CAPApplications(namespace string) CAPApplicationNamespaceLister { + return cAPApplicationNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// CAPApplicationNamespaceLister helps list and get CAPApplications. +// All objects returned here must be treated as read-only. +type CAPApplicationNamespaceLister interface { + // List lists all CAPApplications in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.CAPApplication, err error) + // Get retrieves the CAPApplication from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*v1alpha1.CAPApplication, error) + CAPApplicationNamespaceListerExpansion +} + +// cAPApplicationNamespaceLister implements the CAPApplicationNamespaceLister +// interface. +type cAPApplicationNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all CAPApplications in the indexer for a given namespace. +func (s cAPApplicationNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.CAPApplication, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.CAPApplication)) + }) + return ret, err +} + +// Get retrieves the CAPApplication from the indexer for a given namespace and name. +func (s cAPApplicationNamespaceLister) Get(name string) (*v1alpha1.CAPApplication, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha1.Resource("capapplication"), name) + } + return obj.(*v1alpha1.CAPApplication), nil +} diff --git a/pkg/client/listers/sme.sap.com/v1alpha1/capapplicationversion.go b/pkg/client/listers/sme.sap.com/v1alpha1/capapplicationversion.go new file mode 100644 index 0000000..43fef60 --- /dev/null +++ b/pkg/client/listers/sme.sap.com/v1alpha1/capapplicationversion.go @@ -0,0 +1,87 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1alpha1 "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// CAPApplicationVersionLister helps list CAPApplicationVersions. +// All objects returned here must be treated as read-only. +type CAPApplicationVersionLister interface { + // List lists all CAPApplicationVersions in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.CAPApplicationVersion, err error) + // CAPApplicationVersions returns an object that can list and get CAPApplicationVersions. + CAPApplicationVersions(namespace string) CAPApplicationVersionNamespaceLister + CAPApplicationVersionListerExpansion +} + +// cAPApplicationVersionLister implements the CAPApplicationVersionLister interface. +type cAPApplicationVersionLister struct { + indexer cache.Indexer +} + +// NewCAPApplicationVersionLister returns a new CAPApplicationVersionLister. +func NewCAPApplicationVersionLister(indexer cache.Indexer) CAPApplicationVersionLister { + return &cAPApplicationVersionLister{indexer: indexer} +} + +// List lists all CAPApplicationVersions in the indexer. +func (s *cAPApplicationVersionLister) List(selector labels.Selector) (ret []*v1alpha1.CAPApplicationVersion, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.CAPApplicationVersion)) + }) + return ret, err +} + +// CAPApplicationVersions returns an object that can list and get CAPApplicationVersions. +func (s *cAPApplicationVersionLister) CAPApplicationVersions(namespace string) CAPApplicationVersionNamespaceLister { + return cAPApplicationVersionNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// CAPApplicationVersionNamespaceLister helps list and get CAPApplicationVersions. +// All objects returned here must be treated as read-only. +type CAPApplicationVersionNamespaceLister interface { + // List lists all CAPApplicationVersions in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.CAPApplicationVersion, err error) + // Get retrieves the CAPApplicationVersion from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*v1alpha1.CAPApplicationVersion, error) + CAPApplicationVersionNamespaceListerExpansion +} + +// cAPApplicationVersionNamespaceLister implements the CAPApplicationVersionNamespaceLister +// interface. +type cAPApplicationVersionNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all CAPApplicationVersions in the indexer for a given namespace. +func (s cAPApplicationVersionNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.CAPApplicationVersion, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.CAPApplicationVersion)) + }) + return ret, err +} + +// Get retrieves the CAPApplicationVersion from the indexer for a given namespace and name. +func (s cAPApplicationVersionNamespaceLister) Get(name string) (*v1alpha1.CAPApplicationVersion, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha1.Resource("capapplicationversion"), name) + } + return obj.(*v1alpha1.CAPApplicationVersion), nil +} diff --git a/pkg/client/listers/sme.sap.com/v1alpha1/captenant.go b/pkg/client/listers/sme.sap.com/v1alpha1/captenant.go new file mode 100644 index 0000000..18bb6a6 --- /dev/null +++ b/pkg/client/listers/sme.sap.com/v1alpha1/captenant.go @@ -0,0 +1,87 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1alpha1 "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// CAPTenantLister helps list CAPTenants. +// All objects returned here must be treated as read-only. +type CAPTenantLister interface { + // List lists all CAPTenants in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.CAPTenant, err error) + // CAPTenants returns an object that can list and get CAPTenants. + CAPTenants(namespace string) CAPTenantNamespaceLister + CAPTenantListerExpansion +} + +// cAPTenantLister implements the CAPTenantLister interface. +type cAPTenantLister struct { + indexer cache.Indexer +} + +// NewCAPTenantLister returns a new CAPTenantLister. +func NewCAPTenantLister(indexer cache.Indexer) CAPTenantLister { + return &cAPTenantLister{indexer: indexer} +} + +// List lists all CAPTenants in the indexer. +func (s *cAPTenantLister) List(selector labels.Selector) (ret []*v1alpha1.CAPTenant, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.CAPTenant)) + }) + return ret, err +} + +// CAPTenants returns an object that can list and get CAPTenants. +func (s *cAPTenantLister) CAPTenants(namespace string) CAPTenantNamespaceLister { + return cAPTenantNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// CAPTenantNamespaceLister helps list and get CAPTenants. +// All objects returned here must be treated as read-only. +type CAPTenantNamespaceLister interface { + // List lists all CAPTenants in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.CAPTenant, err error) + // Get retrieves the CAPTenant from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*v1alpha1.CAPTenant, error) + CAPTenantNamespaceListerExpansion +} + +// cAPTenantNamespaceLister implements the CAPTenantNamespaceLister +// interface. +type cAPTenantNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all CAPTenants in the indexer for a given namespace. +func (s cAPTenantNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.CAPTenant, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.CAPTenant)) + }) + return ret, err +} + +// Get retrieves the CAPTenant from the indexer for a given namespace and name. +func (s cAPTenantNamespaceLister) Get(name string) (*v1alpha1.CAPTenant, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha1.Resource("captenant"), name) + } + return obj.(*v1alpha1.CAPTenant), nil +} diff --git a/pkg/client/listers/sme.sap.com/v1alpha1/captenantoperation.go b/pkg/client/listers/sme.sap.com/v1alpha1/captenantoperation.go new file mode 100644 index 0000000..ee4d86a --- /dev/null +++ b/pkg/client/listers/sme.sap.com/v1alpha1/captenantoperation.go @@ -0,0 +1,87 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1alpha1 "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// CAPTenantOperationLister helps list CAPTenantOperations. +// All objects returned here must be treated as read-only. +type CAPTenantOperationLister interface { + // List lists all CAPTenantOperations in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.CAPTenantOperation, err error) + // CAPTenantOperations returns an object that can list and get CAPTenantOperations. + CAPTenantOperations(namespace string) CAPTenantOperationNamespaceLister + CAPTenantOperationListerExpansion +} + +// cAPTenantOperationLister implements the CAPTenantOperationLister interface. +type cAPTenantOperationLister struct { + indexer cache.Indexer +} + +// NewCAPTenantOperationLister returns a new CAPTenantOperationLister. +func NewCAPTenantOperationLister(indexer cache.Indexer) CAPTenantOperationLister { + return &cAPTenantOperationLister{indexer: indexer} +} + +// List lists all CAPTenantOperations in the indexer. +func (s *cAPTenantOperationLister) List(selector labels.Selector) (ret []*v1alpha1.CAPTenantOperation, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.CAPTenantOperation)) + }) + return ret, err +} + +// CAPTenantOperations returns an object that can list and get CAPTenantOperations. +func (s *cAPTenantOperationLister) CAPTenantOperations(namespace string) CAPTenantOperationNamespaceLister { + return cAPTenantOperationNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// CAPTenantOperationNamespaceLister helps list and get CAPTenantOperations. +// All objects returned here must be treated as read-only. +type CAPTenantOperationNamespaceLister interface { + // List lists all CAPTenantOperations in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.CAPTenantOperation, err error) + // Get retrieves the CAPTenantOperation from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*v1alpha1.CAPTenantOperation, error) + CAPTenantOperationNamespaceListerExpansion +} + +// cAPTenantOperationNamespaceLister implements the CAPTenantOperationNamespaceLister +// interface. +type cAPTenantOperationNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all CAPTenantOperations in the indexer for a given namespace. +func (s cAPTenantOperationNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.CAPTenantOperation, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.CAPTenantOperation)) + }) + return ret, err +} + +// Get retrieves the CAPTenantOperation from the indexer for a given namespace and name. +func (s cAPTenantOperationNamespaceLister) Get(name string) (*v1alpha1.CAPTenantOperation, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha1.Resource("captenantoperation"), name) + } + return obj.(*v1alpha1.CAPTenantOperation), nil +} diff --git a/pkg/client/listers/sme.sap.com/v1alpha1/expansion_generated.go b/pkg/client/listers/sme.sap.com/v1alpha1/expansion_generated.go new file mode 100644 index 0000000..f0872b7 --- /dev/null +++ b/pkg/client/listers/sme.sap.com/v1alpha1/expansion_generated.go @@ -0,0 +1,39 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +// CAPApplicationListerExpansion allows custom methods to be added to +// CAPApplicationLister. +type CAPApplicationListerExpansion interface{} + +// CAPApplicationNamespaceListerExpansion allows custom methods to be added to +// CAPApplicationNamespaceLister. +type CAPApplicationNamespaceListerExpansion interface{} + +// CAPApplicationVersionListerExpansion allows custom methods to be added to +// CAPApplicationVersionLister. +type CAPApplicationVersionListerExpansion interface{} + +// CAPApplicationVersionNamespaceListerExpansion allows custom methods to be added to +// CAPApplicationVersionNamespaceLister. +type CAPApplicationVersionNamespaceListerExpansion interface{} + +// CAPTenantListerExpansion allows custom methods to be added to +// CAPTenantLister. +type CAPTenantListerExpansion interface{} + +// CAPTenantNamespaceListerExpansion allows custom methods to be added to +// CAPTenantNamespaceLister. +type CAPTenantNamespaceListerExpansion interface{} + +// CAPTenantOperationListerExpansion allows custom methods to be added to +// CAPTenantOperationLister. +type CAPTenantOperationListerExpansion interface{} + +// CAPTenantOperationNamespaceListerExpansion allows custom methods to be added to +// CAPTenantOperationNamespaceLister. +type CAPTenantOperationNamespaceListerExpansion interface{} diff --git a/website/archetypes/default.md b/website/archetypes/default.md new file mode 100644 index 0000000..00e77bd --- /dev/null +++ b/website/archetypes/default.md @@ -0,0 +1,6 @@ +--- +title: "{{ replace .Name "-" " " | title }}" +date: {{ .Date }} +draft: true +--- + diff --git a/website/content/en/_index.md b/website/content/en/_index.md new file mode 100644 index 0000000..adcd3a5 --- /dev/null +++ b/website/content/en/_index.md @@ -0,0 +1,17 @@ +--- +title: CAP Operator +linkTitle: CAP Operator +description: A Kubernetes Operator for managing the lifecycle of multi-tenant CAP applications +--- + +{{% blocks/cover title="Welcome to CAP Operator !" image_anchor="top" height="full" color="primary" %}} +
+ A Kubernetes Operator for managing the lifecycle of multi-tenant CAP applications


+ + Documentation + + + Source Repository + +
+{{% /blocks/cover %}} \ No newline at end of file diff --git a/website/content/en/docs/_index.md b/website/content/en/docs/_index.md new file mode 100644 index 0000000..4e8c88d --- /dev/null +++ b/website/content/en/docs/_index.md @@ -0,0 +1,25 @@ +--- +title: "Documentation" +linkTitle: "Documentation" +weight: 20 +menu: + main: + weight: 10 + pre: "" +--- + +The [**CAP Operator**](https://github.com/sap/cap-operator) deploys and manages the lifecycle of multi-tenant BTP Golden Path based [CAP](https://cap.cloud.sap/docs) applications and related components, within a Kubernetes cluster. + +Main features of the CAP Operator: + +- Quick and easy deployment of CAP application backends, router and related networking components. +- Integrates with BTP SaaS Provisioning to handle asynchronous tenant subscription requests, executing provisioning / deprovisioning tasks as Kubernetes jobs. +- Automatically upgrades known tenants when newer application versions are available. +- Supports deployment of service specific content / configuration as a Kubernetes job with with every application version (e.g. HTML5 application content to HTML5 Repository Service). +- Manages TLS certificates and DNS entries related to the deployed application, with support for customer specific domains. + +The following picture provides an overview of the major automation steps handled by the Operator during application deployment. + +![workflow](/img/workflow.png) + +Explore the following chapters to learn more. diff --git a/website/content/en/docs/concepts/_index.md b/website/content/en/docs/concepts/_index.md new file mode 100644 index 0000000..1965a7f --- /dev/null +++ b/website/content/en/docs/concepts/_index.md @@ -0,0 +1,16 @@ +--- +title: "Concepts" +linkTitle: "Concepts" +weight: 10 +type: "docs" +description: > + Motivation and overview of components +--- + +Provisioning and operating a CAP Application on a Kubernetes cluster requires deployment of various components in addition to the CAP application server (see [a list of typical components]({{< ref "/cap-application-components.md" >}})). Some of these components can be created at the time of system provisioning, while others need to be created (or updated) at different points during the lifecycle of the application (DAY 2 operational tasks). + +Using helm charts to manage a CAP Application deployment can support the initial system provisioning, but further lifecycle operations like tenant provisioning, which are initiated from external components (SAP BTP), will require manual adjustment of the deployed resources. An example of such an instance would be the creation of `VirtualServices` (part of Istio service mesh) during tenant provisioning to route application (HTTP) requests submitted on the new tenant subdomain to the application server. Another limitation of using helm charts is the lack of control over the order in which resources are created. + +More control over the deployment and further automation of lifecycle operations can be achieved by extending the Kubernetes API with custom resources, which describe the components and configuration of CAP applications, and controllers to reconcile them. Similar to standard controllers of Kubernetes the custom controllers watch for changes in the custom resource objects and work towards moving the cluster state to the desired state. + +The [CAP Operator](https://github.com/sap/cap-operator) comprises of Custom Resource Definitions which describe the CAP application components, the controller to reconcile these resources, and other components which support the lifecycle management. diff --git a/website/content/en/docs/concepts/cap-application-components.md b/website/content/en/docs/concepts/cap-application-components.md new file mode 100644 index 0000000..91bbb2c --- /dev/null +++ b/website/content/en/docs/concepts/cap-application-components.md @@ -0,0 +1,30 @@ +--- +title: "CAP Application Components" +linkTitle: "CAP Application Components" +weight: 20 +type: "docs" +description: > + A typical multi-tenant CAP application +--- + +A full stack application built using the CAP programming model will have the following components + +### SAP BTP Service Instances + +Multi-tenant CAP based applications need to consume services from SAP BTP like XSUAA, SaaS Provisioning etc. These service instances need to be created within a BTP Provider Account. Service keys (bindings) need to be created for these instances which generate the credentials used by the application for accessing these services. + +### CAP application server + +The application provides data models which will be deployed to the connected database. An HTTP server exposes defined services and handles server side application logic. For more details check out [CAP documentation](https://cap.cloud.sap/docs). It is also possible that the application is split into multiple servers (services) which work together. + +### CAP components to support Multi-tenancy + +CAP provides the module `@sap/cds-mtxs` which can be operated as a sidecar (component running independently from the application server). This component is then responsible for handling requests related to tenant management like onboarding which then creates the required schema in the connected database. This module also supports triggering tenant management tasks as CLI commands. + +### AppRouter + +The [AppRouter](https://www.npmjs.com/package/@sap/approuter), or an extended version of it, takes care of authenticating requests (using the XSUAA service) and routes the requests to the application servers or related services (e.g. HTML5 Application Repository Service). + +### SAP Fiori applications + +Multiple SAP Fiori frontend applications may connect to the CAP application backend. These UI5 applications are deployed to the HTML5 Application Repository Service and served from there. Similarly, the application may have content specific to other services which need to be deployed, like the Portal Service. diff --git a/website/content/en/docs/concepts/operator-components/_index.md b/website/content/en/docs/concepts/operator-components/_index.md new file mode 100644 index 0000000..39b1747 --- /dev/null +++ b/website/content/en/docs/concepts/operator-components/_index.md @@ -0,0 +1,23 @@ +--- +title: "CAP Operator Overview" +linkTitle: "CAP Operator Overview" +weight: 10 +type: "docs" +description: > + An overview of the architecture +--- + +The CAP Operator is comprised of the following components + +1. **CAP Controller**: a native Kubernetes controller that reconciles custom resources which are defined as part of the operator +2. **Web-hooks**: validating web-hooks to ensure consistency of custom resource objects submitted to kubernetes API server +3. **Subscription Server**: web server for handling HTTP requests submitted by the BTP `saas-registry` service instances during tenant subscription (and unsubscribe) +4. **MTX Job** _[DEPRECATED]_: wrapper component which enables execution of tenant lifecycle operations using `cds/mtx` module provided by CAP, as kubernetes Jobs. _This module is no longer required for applications using the newer `@sap/cds-mtxs` module._ + +> Note: [`@sap/cds-mtx` is no longer supported with CDS 7](https://cap.cloud.sap/docs/releases/jun23#migration-from-old-mtx). The MTX Job component will be removed once support for older CDS version ends. + +The following diagram depicts how the main components interact when deployed to a cluster. + +![cluster-components](/img/block-cluster.png) + +The following pages provide further details about the CAP Operator components. diff --git a/website/content/en/docs/concepts/operator-components/controller.md b/website/content/en/docs/concepts/operator-components/controller.md new file mode 100644 index 0000000..61faac0 --- /dev/null +++ b/website/content/en/docs/concepts/operator-components/controller.md @@ -0,0 +1,23 @@ +--- +title: "Controller" +linkTitle: "Controller" +weight: 10 +type: "docs" +description: > + Reconciliation of Custom Resource Objects +--- + +The CAP Controller is implemented using the [client-go](https://github.com/kubernetes/client-go) from Kubernetes, which provides the required tools and utilities to interact with the Kubernetes API server. It manages custom resources which are included with the operator. + +The controller uses `Informers` to watch certain resources and invokes registered event handlers when these resources are modified. To streamline the processing of such notifications, rate limiting queues are implemented which store the changes and allow processing of these items in independent reconciliation threads (go routines). Such a design allows sequential processing of the changed items and avoids conflicts. + +The following _namespaced_ Custom Resources have been defined to be reconciled by the CAP controller: + +- `CAPApplication`: defines a high level application, its domains and consumed BTP services +- `CAPApplicationVersion`: defines a child resource of the `CAPApplication` which contains container images which will be used to deploy application components (workloads) of a specific version +- `CAPTenant`: represents a child resource of the `CAPApplication` which corresponds to a BTP sub-account which has subscribed to the application +- `CAPTenantOperation`: represents a provisioning, deprovisioning or upgrade operation on a tenant which is scheduled as a child resource of a `CAPTenant` and executed as a sequence of specified steps. + +> Parent-child relationships between custom resources are established by defining owner references for the children. + +![controller](/img/block-controller.png) diff --git a/website/content/en/docs/concepts/operator-components/mtx-job.md b/website/content/en/docs/concepts/operator-components/mtx-job.md new file mode 100644 index 0000000..fca4fe5 --- /dev/null +++ b/website/content/en/docs/concepts/operator-components/mtx-job.md @@ -0,0 +1,16 @@ +--- +title: "MTX Job" +linkTitle: "MTX Job" +weight: 30 +type: "docs" +description: > + Executing `cds-mtx` routes as Kubernetes Jobs +--- + +CAP provides the [`cds/mtx`](https://cap.cloud.sap/docs/guides/multitenancy/old-mtx-apis) module which provides APIs (HTTP endpoints) for triggering tenant lifecycle operations like provisioning or upgrade. Even though these routes can be served from a deployment of the CAP application server, they consume considerably higher memory compared to application routes. It would be ideal to execute such operations, independent from the application server, as dedicated Kubernetes jobs which can be monitored. As the current `cds-mtx` version operates only as a web-server serving specific lifecycle routes, a trigger or wrapper is required which starts the `cds-mtx` server, executes the required lifecycle route with the given payload, and finally shuts down the server. + +MTX Job component was developed as such a wrapper which runs as a sidecar container within the job pod, along with the `cds-mtx` server, and manages the lifecycle operation. During the reconciliation of custom resource `CAPTenantOperation` which represents a tenant lifecycle operation, the controller creates Jobs where MTX Job is added as a sidecar to the `cds-mtx` server. The MTX Job component waits for the `cds-mtx` server to start, triggers the required lifecycle route, and waits for it to be completed. Finally, it shuts down the `cds-mtx` server and completes the job with the appropriate exit code. This is achieved using a `netcat` listener in the job pod template to allow the trigger to manage the mtx container by sending a dummy connect to the mtx pod (listener) and hence exit gracefully. + +MTX Job will connect to [XSUAA service](https://help.sap.com/viewer/5088c3bb02144e7782959bb1529ca70e/SHIP/en-US/9cde62c2d3a8440caae18f7dbcf68d4c.html) for a valid authentication token to trigger the required lifecycle operation. + +> Note: [`@sap/cds-mtx` is no longer supported with CDS 7](https://cap.cloud.sap/docs/releases/jun23#migration-from-old-mtx). This MTX Job component will be removed once support for older CDS version ends. diff --git a/website/content/en/docs/concepts/operator-components/subscription-server.md b/website/content/en/docs/concepts/operator-components/subscription-server.md new file mode 100644 index 0000000..f5d42ef --- /dev/null +++ b/website/content/en/docs/concepts/operator-components/subscription-server.md @@ -0,0 +1,26 @@ +--- +title: "Subscription Server" +linkTitle: "Subscription Server" +weight: 20 +type: "docs" +description: > + Integration with SaaS Provisioning Service +--- + +The Subscription Server handles HTTP requests from the [SaaS Provisioning Service](https://help.sap.com/viewer/65de2977205c403bbc107264b8eccf4b/Cloud/en-US/2cd8913a50bc4d3e8172f84bb4bfba20.html) for tenant subscription operations on CAP applications which have been installed in the cluster. + +During the creation of a `saas-registry` service instance (in the provider sub-account) [callback URLs are configured](../../../usage/prerequisites/#saas-provisioning-service) which should point to the subscription server routes. + +When a consumer tenant subscribes to an application managed by the operator a subscription callback is received by the subscription server which then generates the `CAPTenant` custom resource object. + +The subscription server returns an `Accepted` (202) response code and starts a routine/thread which keeps polling for the tenant status until the changes to the `CAPTenant` are then independently reconciled by the controller. + +Once the tenant provisioning process has completed (or has failed), the tracking routine will return the appropriate status to the SaaS Registry via an asynchronous callback (by obtaining the necessary authorization token). + + +![subscription](/img/block-subscription.png) + + +([More details about asynchronous tenant subscription](https://controlcenter.ondemand.com/index.html#/knowledge_center/articles/2316430f7d804820934910db736cefbf).) + +Such an asynchronous processing allows us to avoid timeouts during synchronous calls, as well as schedule dedicated jobs (via `CAPTenantOperation`) for completion of the subscription and perform any further tasks required in the cluster (e.g. create a `VirtualService` corresponding to the tenant subdomain). diff --git a/website/content/en/docs/configuration/_index.md b/website/content/en/docs/configuration/_index.md new file mode 100644 index 0000000..13d58ba --- /dev/null +++ b/website/content/en/docs/configuration/_index.md @@ -0,0 +1,20 @@ +--- +title: "Configuration" +linkTitle: "Configuration" +weight: 30 +type: "docs" +tags: ["setup"] +description: > + Configuration options +--- + +This page provides a list of environment variables used by the CAP Operator. + +### Controller + +- `CERT_MANAGER`: Specifies the certificate manager to be used for TLS certificates. Possible values are: + - `gardener`: [SAP Gardener Certificate Management](https://github.com/gardener/cert-management) + - `cert-manager.io`: [cert-manager.io cert-manager](https://github.com/cert-manager/cert-manager) +- `DNS_MANAGER`: Specifies the External DNS Manager to be used. Possible values are: + - `gardener`: [SAP Gardener External DNS Manager](https://github.com/gardener/external-dns-management) + - `kubernetes`: [External DNS Management from Kubernetes](https://github.com/kubernetes-sigs/external-dns) diff --git a/website/content/en/docs/installation/_index.md b/website/content/en/docs/installation/_index.md new file mode 100644 index 0000000..295eb86 --- /dev/null +++ b/website/content/en/docs/installation/_index.md @@ -0,0 +1,10 @@ +--- +title: "Installation" +linkTitle: "Installation" +weight: 20 +type: "docs" +description: > + How to install CAP Operator in a Kubernetes cluster +--- + +This page provides an overview of available methods to install the CAP Operator on a Kubernetes cluster. diff --git a/website/content/en/docs/installation/helm-install.md b/website/content/en/docs/installation/helm-install.md new file mode 100644 index 0000000..521c1da --- /dev/null +++ b/website/content/en/docs/installation/helm-install.md @@ -0,0 +1,56 @@ +--- +title: "Using Helm" +linkTitle: "Using Helm" +weight: 20 +type: "docs" +tags: ["setup"] +description: > + How to deploy using Helm chart +--- + +The recommended way to install the CAP operator components is using the [Helm chart](https://github.com/sap/cap-operator-lifecycle/tree/main/chart) which is published in as an OCI package here: oci://ghcr.io/sap/cap-operator-helm/cap-operator. + +To install create a namespace and helm install the chart into it. You need to supply your `domain` and the `dnsTarget` of your subscription server, either as command line parameter or with a values file: + +Command line parameters: + +```bash +kubectl create namespace cap-operator-system +helm upgrade -i -n cap-operator-system cap-operator oci://ghcr.io/sap/cap-operator-helm/cap-operator --set subscriptionServer.domain=cap-operator. --set subscriptionServer.dnsTarget=public-ingress. +``` + +Values file: + +```bash +kubectl create namespace cap-operator-system +helm upgrade -i -n cap-operator-system cap-operator oci://ghcr.io/sap/cap-operator-helm/cap-operator -f my-cap-operator-values.yaml +``` + +with a values file `my-cap-operator-values.yaml` like + +```yaml +subscriptionServer: + dnsTarget: public-ingress. + domain: cap-operator. +``` + +Note: +This uses your existing docker credentials, if any. Alternatively, you can pass the desired credentials with options `username` and `password`: + +```bash + helm upgrade -i -n cap-operator-system cap-operator oci://ghcr.io/sap/cap-operator-helm/cap-operator --set subscriptionServer.domain=cap-operator. --set subscriptionServer.dnsTarget=public-ingress. --username --password +``` + +To utilize the [local version](https://github.com/sap/cap-operator-lifecycle/tree/main/chart), use + +```bash +helm upgrade -i -n cap-operator-system cap-operator PATH_TO_CAP_OPERATOR_LIFECYCLE_REPOSITORY/chart --set subscriptionServer.domain=cap-operator. --set subscriptionServer.dnsTarget=public-ingress. +``` + +or + +```bash +helm upgrade -i -n cap-operator-system cap-operator PATH_TO_CAP_OPERATOR_LIFECYCLE_REPOSITORY/chart -f my-values.yaml +``` + +{{% include "includes/chart-values.md" %}} diff --git a/website/content/en/docs/installation/prerequisites.md b/website/content/en/docs/installation/prerequisites.md new file mode 100644 index 0000000..afed244 --- /dev/null +++ b/website/content/en/docs/installation/prerequisites.md @@ -0,0 +1,30 @@ +--- +title: "Prerequisites" +linkTitle: "Prerequisites" +weight: 10 +type: "docs" +description: > + How to prepare the cluster before installing CAP Operator +--- + +It is recommended to use a [Gardener](https://gardener.cloud/) managed cluster for deploying CAP applications managed using the CAP Operator. + +The Kubernetes cluster must be setup with the following prerequisites before installing the CAP Operator. + +##### [Istio](https://istio.io/latest/docs/concepts/traffic-management/) (version >= 1.12) + +Istio Service Mesh is used for HTTP traffic management. The CAP Operator creates Istio resources for managing incoming HTTP requests to the application as well as for routing requests on specific (tenant) subdomains. + +> It is required to determine the public ingress gateway subdomain and the overall shoot domain for the system and specify them in the [Chart values](../../installation/helm-install/#values) + +##### [cf-service-operator](https://sap.github.io/cf-service-operator/docs/) or [sap-btp-service-operator](https://github.com/SAP/sap-btp-service-operator) + +These operators can be used for managing SAP BTP service instances and service bindings from within the Kubernetes cluster. + +> Due to unavailability of certain BTP services for Kubernetes platforms, it is recommended to use the [cf-service-operator](https://github.com/sap/cf-service-operator/) which creates the services for a Cloud Foundry space and injects the required access credentials as secrets into the Kubernetes cluster. + +> Please note that service credentials added as Kubernetes Secrets to a namespace, by these operators, supports additional metadata. If you do not use this feature of these operators, it is also required that you use `secretKey: credentials` in the spec of these operators to ensure the service credentials retain any JSON data as it is. **It is recommended to use `secretKey`, even when credential metadata is available to reduce the overhead of interpreting parsing multiple JSON attributes.** + +##### [SAP Gardener Certificate Management](https://github.com/gardener/cert-management) + +This component is available in clusters managed by SAP Gardener and will be used to manage TLS Certificates and issuers. SAP Gardener manages encryption, issuing and signing of certificates. Alternatively, [cert-manager.io cert-manager](https://github.com/cert-manager/cert-manager) can be used. diff --git a/website/content/en/docs/reference/_index.md b/website/content/en/docs/reference/_index.md new file mode 100644 index 0000000..ea2b46e --- /dev/null +++ b/website/content/en/docs/reference/_index.md @@ -0,0 +1,10 @@ +--- +title: "Reference" +linkTitle: "Reference" +weight: 99 +type: "docs" +description: > + API reference +--- + +{{% include "includes/api-reference.html" %}} diff --git a/website/content/en/docs/support/_index.md b/website/content/en/docs/support/_index.md new file mode 100644 index 0000000..91b4302 --- /dev/null +++ b/website/content/en/docs/support/_index.md @@ -0,0 +1,19 @@ +--- +title: Support +linkTitle: "Support" +weight: 95 +type: "docs" +description: > + How to get Support +--- + +Reach out to the project team and the project community via the communication channels listed below. + +To report a bug, please create an [issue](https://github.com/sap/cap-operator/issues). + +See anything missing? Please let us know or [raise a PR](https://github.com/sap/cap-operator/pulls). + +## Communication Channels + +- Issues: [GitHub](https://github.com/sap/cap-operator/issues) +- Email: [CAP Operator](mailto:cap-operator@sap.com) \ No newline at end of file diff --git a/website/content/en/docs/troubleshoot/_index.md b/website/content/en/docs/troubleshoot/_index.md new file mode 100644 index 0000000..9340699 --- /dev/null +++ b/website/content/en/docs/troubleshoot/_index.md @@ -0,0 +1,80 @@ +--- +title: "Troubleshooting" +linkTitle: "Troubleshooting" +weight: 90 +type: "docs" +description: > + Common issues and how to solve them +--- + +**Usage of @sap/cds-mtxs library for multitenancy** + +> By default, the CAP Operator utilizes the `@sap/cds-mtxs` library. However, you can disable this by setting the IS_MTXS_ENABLED environment variable to "false" in the TenantOperation workload, in which case the old `@sap/cds-mtx` library based wrapper job will be used instead. As mentioned in the CAP documentation, [`@sap/cds-mtx` is no longer supported with CDS 7](https://cap.cloud.sap/docs/releases/jun23#migration-from-old-mtx). The MTX Job component will be removed once support for older CDS version ends. + +The CAP Operator supports usage of `@sap/cds-mtxs` (which is the replacement for the former `@sap/cds-mtx` library) from CAP by default. + +This enables us to get rid of our [wrapper implementation](../concepts/operator-components/mtx-job/) and instead use built-in (into `@sap/cds-mtxs`) cli based handling for tenant operations during provisioning, deprovisioning and upgrading tenants. + +As of now for the usage of this new library you may (depending on your k8s cluster hardening setup) need to add additional `securityContext` for the `TenantOperation` and also `CAP` workloads as shown in the sample below. + +``` yaml + - name: tenant-job + consumedBTPServices: + - "{{ include "xsuaaInstance" . }}" + - "{{ include "serviceManagerInstance" . }}" + - "{{ include "saasRegistryInstance" . }}" + jobDefinition: + type: TenantOperation + env: + - name: CDS_ENV + value: production + - name: CDS_MTX_PROVISIONING_CONTAINER + value: '{ "provisioning_parameters": { "database_id": "16e25c51-5455-4b17-a4d7-43545345345" } }' + image: "some.repo.example.com/cap-app/server" + securityContext: # needed until CAP resolves issue with folder creation in the root dir of the app container at runtime + runAsUser: 1000 +``` + +**Secret/credential handling for different workloads of the CAP Operator** + +Libraries like `xsenv`/`cds`(CAP) handle credentials differently in different environments (CF, K8s) and on K8s when using credential data directly from secrets, any JSON data type information related to the data values may get lost and lead to inconsistencies. + +This issue is now addressed by the SAP Service Binding Specification, which mandates the addition of metadata to these secrets. Both `btp-service-operator` and `cf-service-operator` supports the addition of metadata. But, in case this feature is not used in your clusters, the CAP Operator avoids inconsistencies by creating `VCAP_SERVICES` environment variable across all workloads and expects all BTP services credentials to be available in Kubernetes secrets under a key `credentials`. + +This can be achieved using the `secretKey` property for a `ServiceBinding` created using `btp-service-operator` or `cf-service-operator`, e.g.: + +``` +apiVersion: cf.cs.sap.com/v1alpha1 +kind: ServiceBinding +metadata: + name: uaa + namespace: demo +spec: + serviceInstanceName: uaa + name: app-uaa + secretKey: credentials +``` + +> It is recommended to use `secretKey`, even when credential metadata is available to reduce the overhead of interpreting parsing multiple JSON attributes. + +**HTTP requests reaching the AppRouter are not getting forwarded to the application server (pods)** + +The AppRouter component maps incoming requests to destinations (applications or services) which have been configured. If you are using an `xs-app.json` file with your AppRouter to specify route mapping to various destinations, ensure that the `destinationName` property for the CAP backend is specified in the corresponding CAPApplicationVersion configuration. The CAP operator will inject this destination to the AppRouter pods (via environment variables). + + +**HTTP requests are timing out in the AppRouter for long running operations in backend workload** + +If your backend service is known to take a long time, configure the `destinations` environment variable on the AppRouter component to set the desired timeout configuration for that destination (`destinationName`). The CAP Operator will just overwrite the URL part of that destination to point to the right workload, remaining settings are taken over exactly as configured. + +**Recommended AppRouter version** + +Use `@sap/approuter` version `14.x.x` (or higher). + +**CAP Operator resources cannot be deleted in the k8s cluster/namespace** + +All custom resource objects (CROs) created by the CAP Operator are protected with `finalizers` to ensure proper cleanup takes place. +For instance, when deleting a `CAPApplication` CRO any existing tenants would be deprovisioned automatically to avoid inconsistenties. Once the deprovisioning is successful the corresponding CROs would be removed automatically. +The provider `CAPTenant` resource cannot be deleted while before deleting a consistent `CAPApplication`. +_NOTE_: The CAP operator needs the `secrets` from service instances/bindings to exist for the entire lifecycle of the cap application. Removing the service instances/bindings i.e. the secrets from the cluster while the CAP application related CROs still exist would cause leftover resources in cluster (and perhaps the db). Recovering from such inconsistent states might not even be possible. +Such a situation can easily arise when using `helm` delete/uninstall as the order of deletion of resouces is not configurable. We recommend you do this with care. +It is important to ensure that the secrets from service instance/bindings are not deleted before any CAP application that consumes those secrets is completely removed. diff --git a/website/content/en/docs/usage/_index.md b/website/content/en/docs/usage/_index.md new file mode 100644 index 0000000..5b7ea62 --- /dev/null +++ b/website/content/en/docs/usage/_index.md @@ -0,0 +1,8 @@ +--- +title: "Usage" +linkTitle: "Usage" +weight: 40 +type: "docs" +description: > + Managing application with the CAP Operator +--- diff --git a/website/content/en/docs/usage/deploying-application.md b/website/content/en/docs/usage/deploying-application.md new file mode 100644 index 0000000..056280c --- /dev/null +++ b/website/content/en/docs/usage/deploying-application.md @@ -0,0 +1,127 @@ +--- +title: "Deploying a CAP Application" +linkTitle: "Deploying a CAP Application" +weight: 20 +type: "docs" +description: > + How to deploy a new CAP based application +--- + +Just by defining two resources provided by the CAP Operator, namely `capapplications.sme.sap.com` and `capapplicationversions.sme.sap.com`, it possible to deploy a multi-tenant CAP application and start using it. These resources are _namespaced_ and so the CAP Operator will create all related resources (deployments, gateways, jobs etc.) within the same namespace. + +The object, `CAPApplication`, describes the high level attributes of an application like the BTP account where it is hosted, consumed BTP services, domains where the application routes will be made available etc. See [API Reference](../../reference/#sme.sap.com/v1alpha1.CAPApplication). + +```yaml +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + name: cap-app-01 + namespace: cap-app-01 +spec: + btpAppName: cap-app-01 # <-- short name (similar to BTP XSAPPNAME) + btp: + services: + - class: xsuaa # <-- BTP service technical name + name: app-uaa # <-- name of the service instance + secret: cap-app-01-uaa-bind-cf # <-- secret containing the credentials to access the service existing in the same namespace + - class: saas-registry + name: app-saas-registry + secret: cap-app-01-saas-bind-cf + - class: service-manager + name: app-service-manager + secret: cap-app-01-svc-man-bind-cf + - class: destination + name: app-destination + secret: cap-app-01-dest-bind-cf + - class: html5-apps-repo + name: app-html5-repo-host + secret: cap-app-01-html5-repo-bind-cf + - class: html5-apps-repo + name: app-html5-repo-runtime + secret: cap-app-01-html5-rt-bind-cf + - class: portal + name: app-portal + secret: cap-app-01-portal-bind-cf + domains: + istioIngressGatewayLabels: # <-- labels used to identify the istio ingress gateway (the values provided here are the default values) + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: "cap-app-01.cluster.shoot.url.k8s.example.com" # <-- primary domain where the application is exposed. Each tenant will have access to a subdomain of this domain. Ensure that this is at most 62 chars long. + secondary: + - "alt.shoot.example.com" + globalAccountId: global-account-id + provider: + subDomain: cap-app-01-provider + tenantId: e55d7b5-279-48be-a7b0-aa2bae55d7b5 +``` + +The object, `CAPApplicationVersion`, describes the different components of an application version including the container images to be used and the services consumed by each component. See [API Reference](../../reference/#sme.sap.com/v1alpha1.CAPApplicationVersion). + +The `CAPApplicationVersion` must be created in the same namespace as the `CAPApplication` and refers to it. + +```yaml +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + name: cav-cap-app-01-1 + namespace: cap-app-01 +spec: + capApplicationInstance: cap-cap-app-01 # <-- reference to CAPApplication in the same namespace + version: "1" # <-- semantic version + registrySecrets: + - regcred + workloads: + - name: cap-backend + consumedBTPServices: # <-- these are services used by the application server (already defines as part of CAPApplication resource). Corresponding credential secrets will be mounted onto the component pods as volumes. + - app-uaa + - app-service-manager + - app-saas-registry + deploymentDefinition: + type: CAP # <-- indicates the CAP application server + image: app.some.repo.example.com/srv/server:0.0.1 + env: + - name: CDS_ENV + value: production + - name: CDS_MTX_PROVISIONING_CONTAINER + value: '{"provisioning_parameters": { "database_id": "16e25c51-5455-4b17-a4d7-43545345345"}}' + - name: app-router + consumedBTPServices: + - app-uaa + - app-destination + - app-saas-registry + - app-html5-repo-runtime + - app-portal + deploymentDefinition: + type: Router + image: app.some.repo.example.com/approuter/approuter:0.0.1 + env: + - name: PORT + value: 4000 + - name: TENANT_HOST_PATTERN + value: "^(.*).(cap-app-01.cluster.shoot.canary.k8s-hana.ondemand.co|alt.shoot.example.com)" + - name: service-content + consumedBTPServices: + - app-uaa + - app-html5-repo-host + - app-portal + jobDefinition: + type: Content + image: app.some.repo.example.com/approuter/content:0.0.1 + backoffLimit: 1 +``` + +> NOTE: The above example is a minimal `CAPApplicationVersion` that may be deployed. For more supported configuration and their explanations, see [here](../resources/capapplicationversion). + +The controller component of the CAP Operator reacts to these objects and creates further resources which constitute a running application: + +- Deployment (and service) for the application server with credentials (from secrets) to access BTP services injected as `VCAP_SERVICES` environment variable +- Deployment (and service) for the AppRouter with destination mapping to the application server and subscription server (for the tenant provisioning route) +- Job for the version content deployer +- TLS Certificates for provided domains using either [SAP Gardener cert-management](https://github.com/gardener/cert-management) or [cert-manager.io cert-manager](https://github.com/cert-manager/cert-manager) +- Istio Gateway resource for the application domains + +> The content deployer is used to deploy content or configuration to BTP services, before using them. + +Once these resources are available, the `CAPApplicationVersion` status changes to `Ready`. **The controller then proceeds to automatically create an object of type `CAPTenant` which corresponds to the tenant of the provider sub-account.** Please see the page on [tenant subscription]({{< ref "/tenant-provisioning.md" >}}) for details on how the `CAPTenant` resource is reconciled. diff --git a/website/content/en/docs/usage/prerequisites.md b/website/content/en/docs/usage/prerequisites.md new file mode 100644 index 0000000..380f52a --- /dev/null +++ b/website/content/en/docs/usage/prerequisites.md @@ -0,0 +1,105 @@ +--- +title: "Prerequisites" +linkTitle: "Prerequisites" +weight: 10 +type: "docs" +description: > + What to do before deploying a new CAP Application +--- + +### Prepare BTP Global Account and Provider Subaccount + +CAP based applications make use of various BTP services which are created in a Provider Subaccount. So, before the application can be deployed, it is required to create a Global Account and assign the required services which will be used. You can do this using [SAP BTP Control Center](https://controlcenter.ondemand.com/index.html). Once this is done, a Provider Subaccount needs to be created, where the required service instances can be created. + +### Create Service Instances and Bindings + +A multi-tenant CAP based application will need to consume the following BTP services. While creating these service instances, some of the parameters supplied require special attention. Service Keys (Bindings) are then created to generate access credentials, which in turn should be provided as Kubernetes Secrets in the namespace where the application is being deployed. + +Other services (not listed here) may also be used depending on the requirement (e.g. HTML5 Repository Service, Business Logging etc.). + +> IMPORTANT: Due to limited availability of BTP services on Kubernetes, certain services will need to be created by enabling Cloud Foundry for the provider subaccount. We recommend to use the [cf-service-operator](https://sap.github.io/cf-service-operator/docs/) for managing the Service Instances and Service Bindings directly from within the Kubernetes cluster. Based on the Service Bindings, it automatically generates the secrets containing the service access credentials. + +##### XSUAA Service + +The parameter `oauth2-configuration.redirect-uris` must include the domain used by the application. As an example based on the cluster setup this url may have the form `https://*...shoot.url.k8s.example.com/**`. + +Scope required to make asynchronous tenant subscription operations need to be included. Additionally, check the [CAP Multi-tenancy](https://cap.cloud.sap/docs/java/multitenancy#xsuaa-mt-configuration) documentation for additional scopes which are required. + +```yaml +parameters: + authorities: + - $XSAPPNAME.mtcallback + - $XSAPPNAME.mtdeployment + oauth2-configuration: + redirect-uris: + - https://*my-cap-app.cluster-x.my-project.shoot.url.k8s.example.com/** + role-collections: + ... + role-templates: + ... + scopes: + - description: UAA + name: uaa.user + - description: With this scope set, the callbacks for tenant onboarding, offboarding and getDependencies can be called + grant-as-authority-to-apps: + - $XSAPPNAME(application,sap-provisioning,tenant-onboarding) + name: $XSAPPNAME.Callback + - description: Async callback to update the saas-registry (provisioning succeeded/failed) + name: $XSAPPNAME.subscription.write + - description: Deploy applications + name: $XSAPPNAME.mtdeployment + - description: Subscribe to applications + grant-as-authority-to-apps: + - $XSAPPNAME(application,sap-provisioning,tenant-onboarding) + name: $XSAPPNAME.mtcallback + ... +``` +When using mulitple xsuaa service instances in the app (e.g. one for the `application` and other `apiaccess`). The primary xsuaa instance can be set using the annotation: "sme.sap.com/primary-xsuaa" with the value being the `name` of the service instance, as shown below: + +```yaml +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + annotations: + "sme.sap.com/primary-xsuaa": "my-cap-app-uaa" # This let's the CAP Operator determine/use the right UAA instance for the application. + name: test-cap-01 + ... +spec: + btp: + services: + - class: xsuaa + name: my-cap-app-uaa-api + secret: my-cap-app-uaa-api-bind-cf + - class: xsuaa + name: my-cap-app-uaa + secret: my-cap-app-uaa-bind-cf + - class: saas-registry + name: my-cap-app-saas-registry + secret: my-cap-app-saas-bind-cf + ... + btpAppName: my-cap-app + ... +``` + +##### SaaS Provisioning Service + +When creating an instance of the SaaS Provisioning Service, it should be configured to use asynchronous tenant subscription callbacks. See [here](https://controlcenter.ondemand.com/index.html#/knowledge_center/articles/f239e5501a534b64ab5f8dde9bd83c53) for more details. + +```yaml +parameters: + appName: + appUrls: + callbackTimeoutMillis: 300000 # <-- used to fail subscription process when no response is received + getDependencies: https://..cluster-x.my-project.shoot.url.k8s.example.com/callback/v1.0/dependencies # <-- handled by the application + onSubscription: https:///provision/tenants/{tenantId} # <-- the /provision route is forwarded directly to the CAP Operator (Subscription Server) and should be specified as such + onSubscriptionAsync: true + onUnSubscriptionAsync: true +``` + +##### SAP HANA Cloud + +A SAP HANA Cloud instance (preferably shared and accessible from the provider subaccount) is required. The Instance ID of the database must be noted for usage in relevant workloads. SAP HANA Schemas & HDI Containers service should also be entitled for the provider subaccount. + +##### Service Manager + +The Service Manager Service allows CAP to retrieve schema (tenant) specific credentials to connect to the HANA database. diff --git a/website/content/en/docs/usage/resources/_index.md b/website/content/en/docs/usage/resources/_index.md new file mode 100644 index 0000000..acf49d7 --- /dev/null +++ b/website/content/en/docs/usage/resources/_index.md @@ -0,0 +1,8 @@ +--- +title: "Resources" +linkTitle: "Resources" +weight: 50 +type: "docs" +description: > + Detailed configuration of resources managed by the the CAP Operator +--- diff --git a/website/content/en/docs/usage/resources/capapplication.md b/website/content/en/docs/usage/resources/capapplication.md new file mode 100644 index 0000000..4f83066 --- /dev/null +++ b/website/content/en/docs/usage/resources/capapplication.md @@ -0,0 +1,67 @@ +--- +title: "CAPApplication" +linkTitle: "CAPApplication" +weight: 10 +type: "docs" +description: > + How to configure the `CAPApplication` resource +--- + +The below example shows a fully configured `CAPApplication`: + +```yaml +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + name: cap-app + namespace: cap-ns +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-uaa-bind + - class: saas-registry + name: cap-saas-reg + secret: cap-saas-reg-bind + - class: service-manager + name: cap-service-manager + secret: cap-svc-man-bind + - class: destination + name: cap-destination + secret: cap-bem-02-dest-bind + - class: html5-apps-repo + name: cap-html5-repo-host + secret: cap-html5-repo-bind + - class: html5-apps-repo + name: cap-html5-repo-runtime + secret: cap-html5-rt-bind + - class: portal + name: cap-portal + secret: cap-portal-bind + - class: business-logging + name: cap-business-logging + secret: cap-business-logging-bind + btpAppName: cap-app + domains: + istioIngressGatewayLabels: # <-- labels used to identify Load Balancer service used by Istio + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: cap-app.cluster.project.shoot.url.k8s.example.com + secondary: + - alt-cap.cluster.project.shoot.url.k8s.example.com + globalAccountId: 2dddd48d-b45f-45a5-b861-a80872a0c8a8 + provider: # <-- provider tenant details + subDomain: cap-app-provider + tenantId: 7a49218f-c750-4e1f-a248-7f1cefa13010 +``` + +The overall list of BTP service instances and respective secrets (credentials) required by the application is specified as an array in `btp.services`. These service instances are assumed to exist in the provider sub-account. Operators like [cf-service-operator](https://sap.github.io/cf-service-operator/docs/) or [sap-btp-service-operator](https://github.com/SAP/sap-btp-service-operator) can be used to declaratively create these service instances and their credentials as Kubernetes resources. + +The `provider` section specifies details of the provider sub-account linked to this application, while `globalAccountId` denotes the global account in which the provider sub-account is created. Within a global account the `btpAppName` has to be unique as this is equivalent to `XSAPPNAME` which is used in various BTP service and application constructs. + +The `domains` section provides details of where the application routes are exposed. Within a SAP Gardener cluster the primary application domain is a subdomain of the cluster domain, and Gardener [cert-management](https://github.com/gardener/cert-management) will be used to request a wildcard TLS certificate for the primary domain. Additional secondary domains may also be specified (e.g. for customer specific domains) for the application. + +`istioIngressGatewayLabels` are key-value pairs (string) used to identify the ingress controller component of Istio and the related Load Balancer service. These values are configured during installation of Istio service mesh in the cluster. diff --git a/website/content/en/docs/usage/resources/capapplicationversion.md b/website/content/en/docs/usage/resources/capapplicationversion.md new file mode 100644 index 0000000..b1b5243 --- /dev/null +++ b/website/content/en/docs/usage/resources/capapplicationversion.md @@ -0,0 +1,318 @@ +--- +title: "CAPApplicationVersion" +linkTitle: "CAPApplicationVersion" +weight: 20 +type: "docs" +description: > + How to configure the `CAPApplicationVersion` resource +--- + +The `CAPApplicationVersion` has the following high level structure: + +```yaml +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + name: cav-cap-app-v1 + namespace: cap-ns +spec: + version: 3.2.1 # <-- semantic version (should be unique within the versions of a CAP application) + capApplicationInstance: cap-app + registrySecrets: # <-- image pull secrets to be used in the workloads + - regcred + workloads: # <-- define deployments and jobs used for this application version + - name: "cap-backend" + deploymentDefinition: # ... + consumedBTPServices: # ... + - name: "app-router" + deploymentDefinition: # ... + consumedBTPServices: # ... + - name: "service-content" + jobDefinition: # ... + consumedBTPServices: # ... + - name: "mtx-runner" + jobDefinition: # ... + consumedBTPServices: # ... + tenantOperations: # ... <-- (optional) +``` + +- An instance of `CAPApplicationVersion` is always related to an instance of `CAPApplication` in the same namespace. This reference is established using the attribute `capApplicationInstance`. +- An array of workloads (`workloads`) should be defined which include the various software components of the CAP application. A deployment representing the CAP application server or a job which is used for tenant operations are examples of such workloads. A workload should have either a `deploymentDefinition` or a `jobDefinition`. See the next section for more details. +- An optional attribute `tenantOperations` can be used to define a sequence of steps (jobs) to be executed during tenant operations (provisioning / upgrade / deprovisioning). + +### Workloads with `deploymentDefinition` + +```yaml +name: cap-backend +consumedBTPServices: # <-- an array of service instance names referencing the BTP services defined in the CAPApplication resource + - cap-uaa + - cap-saas-reg +deploymentDefinition: + type: CAP # <-- possible values are CAP / Router / Additional + image: some.repo.example.com/cap-app/server:3.22.11 # <-- container image + env: # <-- (optional) same as in core v1 pod.spec.containers.env + - name: SAY + value: "I'm GROOT" + replicas: 3 # <-- (optional) replicas for scaling + ports: + - name: app-port + port: 4004 + routerDestinationName: cap-server-url + - name: tech-port + port: 4005 +``` + +The `type` of the deployment is important to indicate how the operator should handle this workload (for example, injection of `destinations` to be used by the AppRouter). Valid values are: + +- `CAP` to indicate a CAP application server. Only one workload of this type can be used at present. +- `Router` to indicate a version of [AppRouter](https://www.npmjs.com/package/@sap/approuter). Only one workload of this type can be used. +- `Additional` to indicate supporting components which can be deployed along with the CAP application server. + +Optional attributes like `replicas`, `env`, `resources`, `probes`, `securityContext` and `ports` can be defined to configure the deployment. + +#### Port configuration + +It is possible to define which (and how many) ports exposed by a deployment container are exposed inside the cluster (via. services of type `ClusterIP`). The port definition includes a `name` in addition to the `port` number being exposed. + +For `deploymentDefinition`, other than type `Router` it would be possible to specify a `routerDestinationName` which would be used as a named `destination` injected into the AppRouter. + +The port configurations are not mandatory and can be omitted. This would mean that the operator will configure services using defaults. The following defaults are applied in case port configuration is omitted: + +- For workload of type `CAP`, the default port used by CAP, `4004`, will be added to the service and a destination with name `srv-api` will be added to the AppRouter referring to this service port (any existing `destinations` env configuration for this workload will be taken over by overwriting the `URL`). +- For workload of type `Router`, the port `4000` will be exposed in the service. This service will be used as the target for HTTP traffic reaching the application domain (domains are specified within the `CAPApplication` resource). + +> NOTE: If multiple ports are configured for a workload of type `Router`, the first available port will be used to target external traffic to the application domain. + +### Workloads with `jobDefinition` + +```yaml +workloads: + # ... deployment workloads have been omitted in this example + - name: "content-deployer" + consumedServices: # ... + jobDefinition: + type: Content + image: some.repo.example.com/cap-app/content:1.0.1 + - name: "mtx-runner" + consumedServices: # ... + jobDefinition: + type: TenantOperation + image: some.repo.example.com/cap-app/server:3.22.11 + backoffLimit: 2 # <-- determines retry attempts for the job on failure (default is 6) + ttlSecondsAfterFinished: 300 # <-- the job will be cleaned up after this duration + env: + - name: CDS_ENV + value: production + - name: CDS_MTX_PROVISIONING_CONTAINER + value: '{"provisioning_parameters": { "database_id": "16e25c51-5455-4b17-a4d7-43545345345"}}' + - name: "notify-upgrade" + consumedServices: # ... + jobDefinition: + type: CustomTenantOperation + image: # ... + command: ["npm", "run", "notify:upgrade"] # <-- custom entry point for the container allows reuse of a container image with multiple entry points + backoffLimit: 1 + - name: "create-test-data" + consumedServices: # ... + jobDefinition: + type: CustomTenantOperation + image: # ... + command: ["npm", "run ", "deploy:testdata"] +``` + +Workloads with a `jobDefinition` represent a job execution at a particular point in the lifecycle of the application or tenant. The following values are allowed for `type` in such workloads: + +- `Content`: A content deployer job which can be used to deploy (BTP) service specific content from the application version. This job is executed as soon as a new `CAPApplicationVersion` resource is created in the cluster. A workload of this type is required in a `CAPApplicationVersion`. +- `TenantOperation`: A job executed during provisioning, upgrade or deprovisioning of a tenant (`CAPTenant`). These jobs are controlled by the operator and uses the `cds/mtxs` APIs to perform HDI content deployment by default. In order to use `cds/mtx` APIs for HDI content deployment, set environment variable `IS_MTXS_ENABLED` to `"false"` on the `TenantOperation` job. In case a workload of type `TenantOperation` is not provided as part of the `CAPApplicationVersion`, the workload with `deploymentDefinition` of type `CAP` will be used to determine the `jobDefinition` (`image`, `env`, etc. will be used and in such cases to trigger deployment via `cds/mtx` APIs, the environment variable `IS_MTXS_ENABLED` should be set in the `CAP` workload). Also if `cds/mtxs` APIs are used, `command` can be used by applications to trigger tenant operations with custom command. +- `CustomTenantOperation`: An optional job which runs before or after the `TenantOperation` where the application can perform tenant specific tasks (for example, create test data). + +> NOTE: `command` will be ignored for workloads of type `TenantOperation` (for non mtxs based scenarios) as this is controlled by the operator. Also, [`@sap/cds-mtx` is no longer supported with CDS 7](https://cap.cloud.sap/docs/releases/jun23#migration-from-old-mtx). + +### Sequencing tenant operations + +A tenant operation refers to `provisioning`, `upgrade` or `deprovisioning` which are executed in the context of a CAP application for individual tenants (i.e. using the `cds/mtx` or similar modules provided by CAP). Within the `workloads` we have already defined two types of jobs which are valid for such operations, namely `TenantOperation` and `CustomTenantOperation`. + +The `TenantOperation` is mandatory for all tenant operations. + +In addition, you can choose which `CustomTenantOperation` jobs shall be run for a specific operation and in which order. For example, a `CustomTenantOperation` deploying test data to the tenant database schema would need to run during `provisioning`, but should not during `deprovisioning`. + +The field `tenantOperations` specifies which jobs are executed during the different tenant operations and what order they are executed in. + +```yaml +spec: + workloads: # ... + tenantOperations: + provisioning: + - workloadName: "mtx-runner" + - workloadName: "create-test-data" + upgrade: + - workloadName: "notify-upgrade" + continueOnFailure: true # <-- indicates the overall operation may proceed even if this step fails + - workloadName: "mtx-runner" + - workloadName: "create-test-data" + # <-- as the deprovisioning steps are not specified, only the `TenantOperation` workload (first available) will be executed +``` + +In the above example, for each tenant operation, not only are the valid jobs (steps) specified, but also the order in which they are to be executed. Each step in an operation is defined with: + +- `workloadName`refers to the job workload executed in this operation step +- `continueOnFailure` is valid only for `CustomTenantOperation` steps and indicates whether the overall tenant operation can proceed when this operation step fails. + +> NOTE: +> +> - Specifying `tenantOperations` is required only in case `CustomTenantOperations` are to be used. If not specified, each operation will comprise of only the `TenantOperation` step (the first one available from `workloads`). +> - The `tenantOperations`, and specified sequencing are valid only for tenants provisioned (or deprovisioned) on the corresponding `CAPApplicationVersion` and for tenants being upgraded to this `CAPApplicationVersion`. + +### Full Example + +```yaml +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + name: cav-cap-app-v1 + namespace: cap-ns +spec: + version: 3.2.1 + capApplicationInstance: cap-app + registrySecrets: + - regcred + workloads: + - name: cap-backend + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-reg + deploymentDefinition: + type: CAP + image: some.repo.example.com/cap-app/server:3.22.11 + env: + - name: CDS_ENV + value: production + - name: CDS_MTX_PROVISIONING_CONTAINER + value: '{"provisioning_parameters": { "database_id": "16e25c51-5455-4b17-a4d7-43545345345"}}' + replicas: 3 + ports: + - name: app-port + port: 4004 + routerDestinationName: cap-server-url + - name: tech-port + port: 4005 + appProtocol: grpc + livenessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4005 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + readinessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 4005 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + resources: + limits: + cpu: 200m + memory: 500Mi + requests: + cpu: 20m + memory: 50Mi + securityContext: + runAsUser: 1000 + runAsGroup: 2000 + - name: "app-router" + consumedBTPServices: + - cap-uaa + - cap-saas-reg + - cap-html5-repo-rt + deploymentDefinition: + type: Router + image: some.repo.example.com/cap-app/router:4.0.1 + env: + - name: PORT + value: "3000" + ports: + - name: router-port + port: 3000 + livenessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 3000 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + readinessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 3000 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 2 + resources: + limits: + cpu: 200m + memory: 500Mi + requests: + cpu: 20m + memory: 50Mi + podSecurityContext: + runAsUser: 2000 + fsGroup: 2000 + - name: "service-content" + consumedServices: + - cap-uaa + - cap-portal + - cap-html5-repo-host + jobDefinition: + type: Content + image: some.repo.example.com/cap-app/content:1.0.1 + securityContext: + runAsUser: 1000 + runAsGroup: 2000 + - name: "mtx-runner" + consumedServices: # ... + jobDefinition: + type: TenantOperation + image: some.repo.example.com/cap-app/server:3.22.11 + backoffLimit: 2 + ttlSecondsAfterFinished: 300 + env: + - name: CDS_ENV + value: production + - name: CDS_MTX_PROVISIONING_CONTAINER + value: '{"provisioning_parameters": { "database_id": "16e25c51-5455-4b17-a4d7-43545345345"}}' + - name: "notify-upgrade" + consumedServices: [] + jobDefinition: + type: CustomTenantOperation + image: some.repo.example.com/cap-app/server:3.22.11 + command: ["npm", "run", "notify:upgrade"] + backoffLimit: 1 + - name: "create-test-data" + consumedServices: + - cap-service-manager + jobDefinition: + type: CustomTenantOperation + image: some.repo.example.com/cap-app/server:3.22.11 + command: ["npm", "run ", "deploy:testdata"] + tenantOperations: + provisioning: + - workloadName: "mtx-runner" + - workloadName: "create-test-data" + upgrade: + - workloadName: "notify-upgrade" + continueOnFailure: true + - workloadName: "mtx-runner" + - workloadName: "create-test-data" +``` +> NOTE: +> The CAP Operator [workloads](../../../reference/#sme.sap.com/v1alpha1.WorkloadDetails) supports several configurations (present in the [kubernetes API](https://kubernetes.io/docs/reference/using-api/)) which can be configured by looking into our API reference: +> - [Container API reference](../../../reference/#sme.sap.com/v1alpha1.ContainerDetails) for generic container specific configuration +> - [Deployment API reference](../../../reference/#sme.sap.com/v1alpha1.DeploymentDetails) for deployment specific configuration +> - [Job API reference](../../../reference/#sme.sap.com/v1alpha1.JobDetails) for job specific configuration \ No newline at end of file diff --git a/website/content/en/docs/usage/resources/captenant.md b/website/content/en/docs/usage/resources/captenant.md new file mode 100644 index 0000000..4297b4a --- /dev/null +++ b/website/content/en/docs/usage/resources/captenant.md @@ -0,0 +1,32 @@ +--- +title: "CAPTenant" +linkTitle: "CAPTenant" +weight: 30 +type: "docs" +description: > + How to configure the `CAPTenant` resource +--- + +{{< alert color="warning" title="Warning" >}} +The `CAPTenant` resource is completely managed by the operator and should not be created / modified manually. For details of how `CAPTenant` is created, see page on [tenant subscription](../../tenant-provisioning). +{{< /alert >}} + +The `CAPTenant` resource indicates the existence of tenant in the related application (or one that is current being provisioned). The resource starts with a `Provisioning` state and moves to `Ready` when successfully provisioned. Managing tenants as Kubernetes resources allows not only to control the lifecycle of the entity, but also allows to control other requirements which should be fulfilled for the application to serve tenant specific requests (for example, creating of networking resources). + +```yaml +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + name: cap-app-consumer-ge455 + namespace: cap-ns +spec: + capApplicationInstance: cap-app + subDomain: consumer-x + tenantId: cb46733-1279-48be-fdf434-aa2bae55d7b5 + version: "1" + versionUpgradeStrategy: always +``` + +The specification contains attributes relevant for BTP which identifies a tenant like `tenantId` and `subDomain`. + +The `version` field corresponds to the `CAPApplicationVersion` on which the tenant is provisioned or was upgraded. When a newer `CAPApplicationVersion` is available, the operator automatically increments the tenant version which triggers the upgrade process. The `versionUpgradeStrategy` is by default `always`, but could be set to `none` in exceptional cases to prevent automatic upgrade of the tenant. diff --git a/website/content/en/docs/usage/resources/captenantoperation.md b/website/content/en/docs/usage/resources/captenantoperation.md new file mode 100644 index 0000000..de41cc9 --- /dev/null +++ b/website/content/en/docs/usage/resources/captenantoperation.md @@ -0,0 +1,44 @@ +--- +title: "CAPTenantOperation" +linkTitle: "CAPTenantOperation" +weight: 40 +type: "docs" +description: > + How to configure the `CAPTenantOperation` resource +--- + +{{< alert color="warning" title="Warning" >}} +The `CAPTenantOperation` resource is managed by the operator and should not be created / modified manually. The creation of `CAPTenantOperation` is initiated by the `CAPTenant` for executing provisioning, deprovisioning or upgrade. +{{< /alert >}} + +```yaml +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: cap-app-consumer-ge455-77kb9 + namespace: cap-ns +spec: + capApplicationVersionInstance: cav-cap-app-v2 + operation: upgrade + steps: + - continueOnFailure: true + name: mtx-runner + type: CustomTenantOperation + - name: mtx-runner + type: TenantOperation + - name: create-test-data + type: CustomTenantOperation + subDomain: consumer-x + tenantId: cb46733-1279-48be-fdf434-aa2bae55d7b5 +``` + +The above example shows a `CAPTenantOperation` created to execute upgrade operation on a tenant. In addition to tenant details, the `CAPApplicationVersion` to be used for the operation is specified. In case of upgrade or provisioning operation this would be the target `CAPApplicationVersion` whereas for deprovisioning it would be the current `CAPApplicationVersion` of the tenant. + +The operation is completed by executing a series of steps (jobs) which are specified in or derived from the `CAPApplicationVersion`. Each step refers to a workload of type `TenantOperation` or `CustomTenantOperation`. When `CAPTenantOperation` is created by the operator, there should be at least one step of type `TenantOperation` (which is the job used for the database schema update using CAP provided modules). + +`CustomTenantOperation` jobs are hooks provided to the application, which can be executed before or after the actual `TenantOperation`. For applications to be able to identify the context of an execution each job is injected with the following environment variables: + +- `CAPOP_APP_VERSION`: The (semantic) version from the relevant `CAPApplicationVersion` +- `CAPOP_TENANT_ID`: Tenant identifier of the tenant for which the operation is executed +- `CAPOP_TENANT_OPERATION`: The type of operation - `provisioning`, `deprovisioning` or `upgrade` +- `CAPOP_TENANT_SUBDOMAIN`: Subdomain (from sub-account) belonging to the tenant for which the operation is executed diff --git a/website/content/en/docs/usage/tenant-provisioning.md b/website/content/en/docs/usage/tenant-provisioning.md new file mode 100644 index 0000000..7ad8299 --- /dev/null +++ b/website/content/en/docs/usage/tenant-provisioning.md @@ -0,0 +1,65 @@ +--- +title: "Tenant Subscription" +linkTitle: "Tenant Subscription" +weight: 30 +type: "docs" +description: > + How tenant provisioning works +--- + +From the perspective of the CAP Operator, a valid tenant for an application is represented by the resource `CAPTenant`. It refers to the `CAPApplication` it belongs to and specifies details of the BTP sub-account representing the tenant. + +```yaml +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + name: cap-app-01-provider + namespace: cap-app-01 +spec: + capApplicationInstance: cap-app-01 # <-- reference to the CAPApplication + subDomain: app-provider + tenantId: aa2bae55d7b5-1279-456564-a7b0-aa2bae55d7b5 + version: "1.0.0" # <-- expected version of the application + versionUpgradeStrategy: always # <-- always / never +``` + +## Tenant Provisioning + +The process of tenant provisioning starts when a consumer sub-account subscribes to the application, either via the BTP Cockpit or using the APIs provided by the SaaS Provisioning Service. This in turn initiates the asynchronous callback from the SaaS Provisioning Service instance into the cluster, and the request is handled by the [Subscription Server]({{< ref "docs/concepts/operator-components/subscription-server.md" >}}). The subscription server validates the request and creates an instance of `CAPTenant` for the identified `CAPApplication`. + +{{< alert color="warning" title="Warning" >}} +An instance of `CAPTenant` must not be created or deleted manually within the cluster. A new instance has to be created by the Subscription Server after receiving a provisioning call from BTP SaaS Provisioning Service. +{{< /alert >}} + +The controller, observing the new `CAPTenant` will initiate the provisioning process by creating the resource `CAPTenantOperation` which represents the provisioning operation. + +```yaml +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: cap-app-01-provider-sgz8b + namespace: cap-app-01 +spec: + capApplicationVersionInstance: cav-cap-app-01-1 # <-- latest CAPApplicationVersion in Ready state + subDomain: app-provider + tenantId: aa2bae55d7b5-1279-456564-a7b0-aa2bae55d7b5 + operation: provisioning # <-- valid values are provisioning, deprovisioning and upgrade + steps: + - name: cap-backend # <-- derived from workload of type CAP (when workload of type TenantOperation is not specified) + type: TenantOperation +``` + +The `CAPTenantOperation` is further reconciled to create Kubernetes Jobs (steps) which are derived from the latest `CAPApplicationVersion` which is in `Ready` state. The steps comprise of a `TenantOperation` job and optional `CustomTenantOperation` steps. The `TenantOperation` step uses built in cli based tenant operations from `@sap/cds-mtxs` to execute tenant provisioning. + +The `CAPTenant` reaches a Ready state, only after + +- Successful completion of all `CAPTenantOperation` steps +- Creation of Istio `VirtualService` which allows HTTP requests on the tenant subdomain to reach the application + +![tenant-provisioning](/img/activity-tenantprovisioning.png) + +## Tenant Deprovisioning + +Similar to the tenant provisioning process, when a tenant unsubscribes from the application, the request is received by the Subscription Server. It validates the existence and status of the `CAPTenant` and submits a request for deletion to the Kubernetes API server. + +The controller identifies that the `CAPTenant` has to be deleted, but withholds deletion till it can create and watch for successful completion of a `CAPTenantOperation` of type deprovisioning. The `CAPTenantOperation` creates the corresponding jobs (steps) which executes the tenant deprovisioning. diff --git a/website/content/en/docs/usage/version-upgrade.md b/website/content/en/docs/usage/version-upgrade.md new file mode 100644 index 0000000..5b65315 --- /dev/null +++ b/website/content/en/docs/usage/version-upgrade.md @@ -0,0 +1,120 @@ +--- +title: "Application Upgrade" +linkTitle: "Application Upgrade" +weight: 40 +type: "docs" +description: > + How to upgrade to a new Application Version +--- + +An important lifecycle aspect of operating multi-tenant CAP Applications is the tenant upgrade process. With CAP Operator these tenant upgrades can be fully automated by providing a new instance of `capapplicationversions.sme.sap.com` custom resource. +As you've already seen during [initial deployment]({{< ref "/deploying-application.md" >}}), the `CAPApplicationVersion` resource describes the different components (workloads) of an application version that includes the container image to be used and the services consumed by each component. +To upgrade the application, one needs to provide a new `CAPApplicationVersion` with the relevant `image` for each component and use a newer (higher) semantic version in the `version` field. See [API Reference](../../reference/#sme.sap.com/v1alpha1.CAPApplicationVersion). + +```yaml +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + name: cav-cap-app-01-2 + namespace: cap-app-01 +spec: + capApplicationInstance: cap-cap-app-01 # <-- reference to CAPApplication in the same namespace + version: "2.0.1" # <-- semantic version + registrySecrets: + - regcred + workloads: + - name: cap-backend + consumedBTPServices: + - app-uaa + - app-service-manager + - app-saas-registry + deploymentDefinition: + type: CAP # <-- indicates the CAP application server + image: app.some.repo.example.com/srv/server:0.0.2 + env: + - name: CDS_ENV + value: production + - name: CDS_MTX_PROVISIONING_CONTAINER + value: '{"provisioning_parameters": { "database_id": "16e25c51-5455-4b17-a4d7-43545345345"}}' + - name: app-router + consumedBTPServices: + - app-uaa + - app-destination + - app-saas-registry + - app-html5-repo-runtime + - app-portal + deploymentDefinition: + type: Router + image: app.some.repo.example.com/approuter/approuter:0.0.2 + env: + - name: PORT + value: 4000 + - name: TENANT_HOST_PATTERN + value: "^(.*).(cap-app-01.cluster.shoot.canary.k8s-hana.ondemand.co|alt.shoot.example.com)" + - name: service-content + consumedBTPServices: + - app-uaa + - app-html5-repo-host + - app-portal + jobDefinition: + type: Content + image: app.some.repo.example.com/approuter/content:0.0.2 + backoffLimit: 1 + - name: mtx-runner + consumedBTPServices: + - app-uaa + - app-service-manager + - app-saas-registry + jobDefinition: + type: TenantOperation + image: app.some.repo.example.com/approuter/content:0.0.2 + env: + - name: CDS_ENV + value: production + - name: CDS_MTX_PROVISIONING_CONTAINER + value: '{"provisioning_parameters": { "database_id": "16e25c51-5455-4b17-a4d7-43545345345"}}' + - name: notify-upgrade + consumedBTPServices: [] + jobDefinition: + type: CustomTenantOperation + image: app.some.repo.example.com/approuter/content:0.0.2 + command: ["npm", "run", "notify:upgrade"] + backoffLimit: 1 + env: + - name: TARGET_DL + value: group_xyz@sap.com + tenantOperations: + upgrade: + - workloadName: mtx-runner + - workloadName: notify-upgrade + continueOnFailure: true +``` + +Note that in this version (compared to version "1" used for [initial deployment]({{< ref "/deploying-application.md" >}})), new workloads of type `TenantOperation` and `CustomTenantOperation` have been added. + +The controller component of the CAP Operator reacts to the new `CAPApplicationVersion` resource and triggers another deployment for application server, router and triggers the content deployment job. Once the new `CAPApplicationVersion` is `Ready`, **the controller then proceeds to automatically upgrade all relevant tenants** i.e. by updating the `version` attribute on the `CAPTenant` resources. + +The reconciliation of a `CAPTenant` changes its state to `Upgrading` and creates the `CAPTenantOperation` resource of type upgrade. + +```yaml +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenantOperation +metadata: + name: cap-app-01-provider-fgdfg + namespace: cap-app-01 +spec: + capApplicationVersionInstance: cav-cap-app-01-2 + subDomain: cap-provider + tenantId: aa2bae55d7b5-1279-456564-a7b0-aa2bae55d7b5 + operation: upgrade # possible values are provisioning / upgrade / deprovisioning + steps: + - name: "mtx-runner" + type: TenantOperation + - name: "notify-upgrade" + type: CustomTenantOperation + continueOnFailure: true # <-- can be set for workloads of type CustomTenantOperation to indicate that the success of this job is optional for the completion of the overall operation +``` + +The `CAPTenantOperation` creates jobs for each of the steps involved and executes them sequentially, till all the jobs are finished or one of them fails. The `CAPTenant` is notified about the result and updates its state accordingly. + +A successful completion of the `CAPTenantOperation` will cause the `VirtualService` managed by the `CAPTenant` to be modified to route HTTP traffic to the deployments of the newer `CAPApplicationVersion`. Once all tenants have been upgraded the older `CAPApplicationVersion` can be deleted. diff --git a/website/go.mod b/website/go.mod new file mode 100644 index 0000000..e7ed3ea --- /dev/null +++ b/website/go.mod @@ -0,0 +1,5 @@ +module github.com/sap/cap-operator/website + +go 1.21 + +require github.com/google/docsy v0.7.1 // indirect diff --git a/website/go.sum b/website/go.sum new file mode 100644 index 0000000..d9ed4c5 --- /dev/null +++ b/website/go.sum @@ -0,0 +1,5 @@ +github.com/FortAwesome/Font-Awesome v0.0.0-20230327165841-0698449d50f2/go.mod h1:IUgezN/MFpCDIlFezw3L8j83oeiIuYoj28Miwr/KUYo= +github.com/google/docsy v0.7.1 h1:DUriA7Nr3lJjNi9Ulev1SfiG1sUYmvyDeU4nTp7uDxY= +github.com/google/docsy v0.7.1/go.mod h1:JCmE+c+izhE0Rvzv3y+AzHhz1KdwlA9Oj5YBMklJcfc= +github.com/google/docsy/dependencies v0.7.1/go.mod h1:gihhs5gmgeO+wuoay4FwOzob+jYJVyQbNaQOh788lD4= +github.com/twbs/bootstrap v5.2.3+incompatible/go.mod h1:fZTSrkpSf0/HkL0IIJzvVspTt1r9zuf7XlZau8kpcY0= diff --git a/website/hugo.yaml b/website/hugo.yaml new file mode 100644 index 0000000..34d6e73 --- /dev/null +++ b/website/hugo.yaml @@ -0,0 +1,167 @@ +baseURL: "https://sap.github.io/cap-operator" +title: "CAP Operator" + +enableRobotsTXT: true + +# Will give values to .Lastmod etc. +enableGitInfo: true + +# Language settings +contentDir: "content/en" +defaultContentLanguage: "en" +defaultContentLanguageInSubdir: false +# Useful when translating. +enableMissingTranslationPlaceholders: true + +# horizontal (top level) menu items +menu: + main: + # NOTE: the "Documentation" menu item is sourced from the front matter of the _index.md file + - name: "GitHub" + weight: 90 + url: "https://github.com/sap/cap-operator/" + pre: "" + +# You can add your own taxonomies +taxonomies: + tag: "tags" + category: "categories" + +# If used, must have same lang as taxonomyCloud +taxonomyCloudTitle: ["Tag Cloud", "Categories"] + +# set taxonomyPageHeader = [] to hide taxonomies on the page headers +taxonomyPageHeader: ["tags", "categories"] + +# Configure how URLs look like per section. +permalinks: + blog: "/:section/:year/:month/:day/:slug/" + +# Image processing configuration. +imaging: + resampleFilter: "CatmullRom" + quality: 90 + anchor: "smart" + +# Language configuration +languages: + en: + languageName: "English" + contentDir: "content/en" + # Weight used for sorting. + weight: 1 + params: + title: "CAP Operator" + description: "CAP Multi-tenant Application Operator for Kubernetes" + +markup: + goldmark: + renderer: + unsafe: true + highlight: + # See a complete list of available styles at https://xyproto.github.io/splash/docs/all.html + style: "nord" + # Uncomment if you want your chosen highlight style used for code blocks without a specified language + guessSyntax: "true" + codeFences: true + +# Everything below this are Site Params + +# Comment out if you don't want the "print entire section" link enabled. +outputs: + section: ["HTML", "print", "RSS"] + +params: + taxonomy: + # set taxonomyCloud = [] to hide taxonomy clouds + taxonomyCloud: ["tags", "categories"] + + copyright: "ERP for SME" + # privacy_policy = "https://policies.google.com/privacy" + + # Menu title if your navbar has a versions selector to access old versions of your site. + # This menu appears only if you have at least one [params.versions] set. + version_menu: "Releases" + + # Flag used in the "version-banner" partial to decide whether to display a + # banner on every page indicating that this is an archived version of the docs. + # Set this flag to "true" if you want to display the banner. + archived_version: false + + # The version number for the version of the docs represented in this doc set. + # Used in the "version-banner" partial to display a version number for the + # current doc set. + version: "0.0" + + # A link to latest version of the docs. Used in the "version-banner" partial to + # point people to the main doc site. + url_latest_version: "https://sap.github.io/cap-operator" + + # Repository configuration (URLs for in-page links to opening issues and suggesting changes) + github_repo: "https://github.com/sap/cap-operator" + # An optional link to a related project repo. For example, the sibling repository where your product code lives. + github_project_repo: "https://github.com/sap/cap-operator" + + # Specify a value here if your content directory is not in your repo's root directory + github_subdir: "website" + + # Uncomment this if you have a newer GitHub repo with "main" as the default branch, + # or specify a new value if you want to reference another branch in your GitHub links + github_branch: "main" + + # Enable Algolia DocSearch + algolia_docsearch: false + + # Enable Lunr.js offline search + offlineSearch: true + + # User interface configuration + ui: + # Set to true to disable breadcrumb navigation. + breadcrumb_disable: false + # Set to true to disable the About link in the site footer + footer_about_disable: false + # Set to false if you don't want to display a logo (/assets/icons/logo.svg) in the top navbar + navbar_logo: false + # Set to true if you don't want the top navbar to be translucent when over a `block/cover`, like on the homepage. + navbar_translucent_over_cover_disable: false + # Enable to show the side bar menu in its compact state. + sidebar_menu_compact: true + # Set to true to hide the sidebar search box (the top nav search box will still be displayed if search is enabled) + sidebar_search_disable: true + + # Adds a H2 section titled "Feedback" to the bottom of each doc. The responses are sent to Google Analytics as events. + # This feature depends on [services.googleAnalytics] and will be disabled if "services.googleAnalytics.id" is not set. + # If you want this feature, but occasionally need to remove the "Feedback" section from a single page, + # add "hide_feedback: true" to the page's front matter. + feedback: + enable: false + # The responses that the user sees after clicking "yes" (the page was helpful) or "no" (the page was not helpful). + # yes = 'Glad to hear it! Please tell us how we can improve.' + # no = 'Sorry to hear that. Please tell us how we can improve.' + + # Adds a reading time to the top of each doc. + # If you want this feature, but occasionally need to remove the Reading time from a single page, + # add "hide_readingtime: true" to the page's front matter + readingtime: + enable: true + + links: + # End user relevant links. These will show up on left side of footer and in the community page if you have one. + user: + - name: "Ask for help" + url: "mailto:DL_64A676DD6A9245028D2A9DCC@global.corp.sap" + icon: "fa fa-envelope" + desc: "Help from development team" + # Developer relevant links. These will show up on right side of footer and in the community page if you have one. + developer: + - name: "GitHub" + urlL: "https://github.com/sap/cap-operator/" + icon: "fab fa-github" + desc: "Development takes place here!" + +# Add docsy as hugo module +module: + imports: + - path: "github.com/google/docsy" + - path: "github.com/google/docsy/dependencies" \ No newline at end of file diff --git a/website/includes/api-reference.html b/website/includes/api-reference.html new file mode 100644 index 0000000..e4117f6 --- /dev/null +++ b/website/includes/api-reference.html @@ -0,0 +1,2234 @@ +

Packages:

+ +

sme.sap.com/v1alpha1

+Resource Types: + +

CAPApplication +

+
+

CAPApplication is the schema for capapplications API

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+apiVersion
+string
+ +sme.sap.com/v1alpha1 + +
+kind
+string +
CAPApplication
+metadata
+ + +Kubernetes meta/v1.ObjectMeta + + +
+Refer to the Kubernetes API documentation for the fields of the +metadata field. +
+spec
+ + +CAPApplicationSpec + + +
+

CAPApplication spec

+
+
+ + + + + + + + + + + + + + + + + + + + + +
+domains
+ + +ApplicationDomains + + +
+

Domains used by the application

+
+globalAccountId
+ +string + +
+

SAP BTP Global Account Identifier where services are entitles for the current application

+
+btpAppName
+ +string + +
+

Short name for the application (similar to BTP XSAPPNAME)

+
+provider
+ + +BTPTenantIdentification + + +
+

Provider subaccount where application services are created

+
+btp
+ + +BTP + + +
+

SAP BTP Services consumed by the application

+
+
+status
+ + +CAPApplicationStatus + + +
+

CAPApplication status

+
+

CAPApplicationVersion +

+
+

CAPApplicationVersion defines the schema for capapplicationversions API

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+apiVersion
+string
+ +sme.sap.com/v1alpha1 + +
+kind
+string +
CAPApplicationVersion
+metadata
+ + +Kubernetes meta/v1.ObjectMeta + + +
+Refer to the Kubernetes API documentation for the fields of the +metadata field. +
+spec
+ + +CAPApplicationVersionSpec + + +
+

CAPApplicationVersion spec

+
+
+ + + + + + + + + + + + + + + + + + + + + +
+capApplicationInstance
+ +string + +
+

Denotes to which CAPApplication the current version belongs

+
+version
+ +string + +
+

Semantic version

+
+registrySecrets
+ +[]string + +
+

Registry secrets used to pull images of the application components

+
+workloads
+ + +[]WorkloadDetails + + +
+

Information about the Workloads

+
+tenantOperations
+ + +TenantOperations + + +
+

Tenant Operations may be used to specify how jobs are sequenced for the different tenant operations

+
+
+status
+ + +CAPApplicationVersionStatus + + +
+

CAPApplicationVersion status

+
+

CAPTenant +

+
+

CAPTenant defines the schema for captenants API

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+apiVersion
+string
+ +sme.sap.com/v1alpha1 + +
+kind
+string +
CAPTenant
+metadata
+ + +Kubernetes meta/v1.ObjectMeta + + +
+Refer to the Kubernetes API documentation for the fields of the +metadata field. +
+spec
+ + +CAPTenantSpec + + +
+

CAPTenant spec

+
+
+ + + + + + + + + + + + + + + + + +
+capApplicationInstance
+ +string + +
+

Denotes to which CAPApplication the current tenant belongs

+
+BTPTenantIdentification
+ + +BTPTenantIdentification + + +
+

+(Members of BTPTenantIdentification are embedded into this type.) +

+

Details of consumer sub-account subscribing to the application

+
+version
+ +string + +
+

Semver that is used to determine the relevant CAPApplicationVersion that a CAPTenant can be upgraded to (i.e. if it is not already on that version)

+
+versionUpgradeStrategy
+ + +VersionUpgradeStrategyType + + +
+

Denotes whether a CAPTenant can be upgraded. One of (‘always’, ‘never’)

+
+
+status
+ + +CAPTenantStatus + + +
+

CAPTenant status

+
+

CAPTenantOperation +

+
+

CAPTenantOperation defines the schema for captenantoperations API

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+apiVersion
+string
+ +sme.sap.com/v1alpha1 + +
+kind
+string +
CAPTenantOperation
+metadata
+ + +Kubernetes meta/v1.ObjectMeta + + +
+Refer to the Kubernetes API documentation for the fields of the +metadata field. +
+spec
+ + +CAPTenantOperationSpec + + +
+

CAPTenantOperation spec

+
+
+ + + + + + + + + + + + + + + + + +
+operation
+ + +CAPTenantOperationType + + +
+

Scope of the tenant lifecycle operation. One of ‘provisioning’, ‘deprovisioning’ or ‘upgrade’

+
+BTPTenantIdentification
+ + +BTPTenantIdentification + + +
+

+(Members of BTPTenantIdentification are embedded into this type.) +

+

BTP sub-account (tenant) for which request is created

+
+capApplicationVersionInstance
+ +string + +
+

Reference to CAPApplicationVersion for executing the operation

+
+steps
+ + +[]CAPTenantOperationStep + + +
+

Steps (jobs) to be executed for the operation to complete

+
+
+status
+ + +CAPTenantOperationStatus + + +
+

CAPTenantOperation status

+
+

ApplicationDomains +

+

+(Appears on: CAPApplicationSpec) +

+
+

Application domains

+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+primary
+ +string + +
+

Primary application domain will be used to generate a wildcard TLS certificate. In SAP Gardener managed clusters this is (usually) a subdomain of the cluster domain

+
+secondary
+ +[]string + +
+

Customer specific domains to serve application endpoints (optional)

+
+dnsTarget
+ +string + +
+

Public ingress URL for the cluster Load Balancer

+
+istioIngressGatewayLabels
+ + +[]NameValue + + +
+

Labels used to identify the istio ingress-gateway component and its corresponding namespace. Usually {“app”:“istio-ingressgateway”,“istio”:“ingressgateway”}

+
+

BTP +

+

+(Appears on: CAPApplicationSpec) +

+
+
+ + + + + + + + + + + + + +
FieldDescription
+services
+ + +[]ServiceInfo + + +
+

Details of BTP Services

+
+

BTPTenantIdentification +

+

+(Appears on: CAPApplicationSpec, CAPTenantOperationSpec, CAPTenantSpec) +

+
+

Identifies an SAP BTP subaccount (tenant)

+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+subDomain
+ +string + +
+

BTP subaccount subdomain

+
+tenantId
+ +string + +
+

BTP subaccount Tenant ID

+
+

CAPApplicationSpec +

+

+(Appears on: CAPApplication) +

+
+

CAPApplicationSpec defines the desired state of CAPApplication

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+domains
+ + +ApplicationDomains + + +
+

Domains used by the application

+
+globalAccountId
+ +string + +
+

SAP BTP Global Account Identifier where services are entitles for the current application

+
+btpAppName
+ +string + +
+

Short name for the application (similar to BTP XSAPPNAME)

+
+provider
+ + +BTPTenantIdentification + + +
+

Provider subaccount where application services are created

+
+btp
+ + +BTP + + +
+

SAP BTP Services consumed by the application

+
+

CAPApplicationState +(string alias)

+

+(Appears on: CAPApplicationStatus) +

+
+
+ + + + + + + + + + + + + + + + +
ValueDescription

"Consistent"

CAPApplication has been reconciled and is now consistent

+

"Deleting"

Deletion has been triggered

+

"Error"

An error occurred during reconciliation

+

"Processing"

CAPApplication is being reconciled

+
+

CAPApplicationStatus +

+

+(Appears on: CAPApplication) +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+GenericStatus
+ + +GenericStatus + + +
+

+(Members of GenericStatus are embedded into this type.) +

+
+state
+ + +CAPApplicationState + + +
+

State of CAPApplication

+
+domainSpecHash
+ +string + +
+

Hash representing last known application domains

+
+lastFullReconciliationTime
+ + +Kubernetes meta/v1.Time + + +
+

The last time a full reconciliation was completed

+
+

CAPApplicationStatusConditionType +(string alias)

+
+
+ + + + + + + + + + + + +
ValueDescription

"AllTenantsReady"

"LatestVersionReady"

+

CAPApplicationVersionSpec +

+

+(Appears on: CAPApplicationVersion) +

+
+

CAPApplicationVersionSpec specifies the desired state of CAPApplicationVersion

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+capApplicationInstance
+ +string + +
+

Denotes to which CAPApplication the current version belongs

+
+version
+ +string + +
+

Semantic version

+
+registrySecrets
+ +[]string + +
+

Registry secrets used to pull images of the application components

+
+workloads
+ + +[]WorkloadDetails + + +
+

Information about the Workloads

+
+tenantOperations
+ + +TenantOperations + + +
+

Tenant Operations may be used to specify how jobs are sequenced for the different tenant operations

+
+

CAPApplicationVersionState +(string alias)

+

+(Appears on: CAPApplicationVersionStatus) +

+
+
+ + + + + + + + + + + + + + + + +
ValueDescription

"Deleting"

Deletion has been triggered

+

"Error"

An error occurred during reconciliation

+

"Processing"

CAPApplicationVersion is being processed

+

"Ready"

CAPApplicationVersion is now ready for use (dependent resources have been created)

+
+

CAPApplicationVersionStatus +

+

+(Appears on: CAPApplicationVersion) +

+
+
+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+GenericStatus
+ + +GenericStatus + + +
+

+(Members of GenericStatus are embedded into this type.) +

+
+state
+ + +CAPApplicationVersionState + + +
+

State of CAPApplicationVersion

+
+finishedJobs
+ +[]string + +
+

List of finished Content Jobs

+
+

CAPTenantOperationSpec +

+

+(Appears on: CAPTenantOperation) +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+operation
+ + +CAPTenantOperationType + + +
+

Scope of the tenant lifecycle operation. One of ‘provisioning’, ‘deprovisioning’ or ‘upgrade’

+
+BTPTenantIdentification
+ + +BTPTenantIdentification + + +
+

+(Members of BTPTenantIdentification are embedded into this type.) +

+

BTP sub-account (tenant) for which request is created

+
+capApplicationVersionInstance
+ +string + +
+

Reference to CAPApplicationVersion for executing the operation

+
+steps
+ + +[]CAPTenantOperationStep + + +
+

Steps (jobs) to be executed for the operation to complete

+
+

CAPTenantOperationState +(string alias)

+

+(Appears on: CAPTenantOperationStatus) +

+
+
+ + + + + + + + + + + + + + + + +
ValueDescription

"Completed"

CAPTenantOperation steps completed

+

"Deleting"

CAPTenantOperation deletion has been triggered

+

"Failed"

CAPTenantOperation steps have failed

+

"Processing"

CAPTenantOperation is being processed

+
+

CAPTenantOperationStatus +

+

+(Appears on: CAPTenantOperation) +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+GenericStatus
+ + +GenericStatus + + +
+

+(Members of GenericStatus are embedded into this type.) +

+
+state
+ + +CAPTenantOperationState + + +
+

State of CAPTenantOperation

+
+currentStep
+ +uint32 + +
+

Current step being processed from the sequence of specified steps

+
+activeJob
+ +string + +
+

Name of the job being executed for the current step

+
+

CAPTenantOperationStep +

+

+(Appears on: CAPTenantOperationSpec) +

+
+
+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+name
+ +string + +
+

Name of the workload from the referenced CAPApplicationVersion

+
+type
+ + +JobType + + +
+

Type of job. One of ‘TenantOperation’ or ‘CustomTenantOperation’

+
+continueOnFailure
+ +bool + +
+

Indicates whether the operation can continue in case of step failure. Relevant only for type ‘CustomTenantOperation’

+
+

CAPTenantOperationType +(string alias)

+

+(Appears on: CAPTenantOperationSpec) +

+
+
+ + + + + + + + + + + + + + +
ValueDescription

"deprovisioning"

Deprovision tenant

+

"provisioning"

Provision tenant

+

"upgrade"

Upgrade tenant

+
+

CAPTenantSpec +

+

+(Appears on: CAPTenant) +

+
+

CAPTenantSpec defines the desired state of the CAPTenant

+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+capApplicationInstance
+ +string + +
+

Denotes to which CAPApplication the current tenant belongs

+
+BTPTenantIdentification
+ + +BTPTenantIdentification + + +
+

+(Members of BTPTenantIdentification are embedded into this type.) +

+

Details of consumer sub-account subscribing to the application

+
+version
+ +string + +
+

Semver that is used to determine the relevant CAPApplicationVersion that a CAPTenant can be upgraded to (i.e. if it is not already on that version)

+
+versionUpgradeStrategy
+ + +VersionUpgradeStrategyType + + +
+

Denotes whether a CAPTenant can be upgraded. One of (‘always’, ‘never’)

+
+

CAPTenantState +(string alias)

+

+(Appears on: CAPTenantStatus) +

+
+
+ + + + + + + + + + + + + + + + + + + + +
ValueDescription

"Deleting"

Deletion has been triggered

+

"Provisioning"

Tenant is being provisioned

+

"ProvisioningError"

Tenant provisioning ended in error

+

"Ready"

Tenant has been provisioned/upgraded and is now ready for use

+

"UpgradeError"

Tenant upgrade failed

+

"Upgrading"

Tenant is being upgraded

+
+

CAPTenantStatus +

+

+(Appears on: CAPTenant) +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+GenericStatus
+ + +GenericStatus + + +
+

+(Members of GenericStatus are embedded into this type.) +

+
+state
+ + +CAPTenantState + + +
+

State of CAPTenant

+
+currentCAPApplicationVersionInstance
+ +string + +
+

Specifies the current version of the tenant after provisioning or upgrade

+
+previousCAPApplicationVersions
+ +[]string + +
+

Previous versions of the tenant (first to last)

+
+lastFullReconciliationTime
+ + +Kubernetes meta/v1.Time + + +
+

The last time a full reconciliation was completed

+
+

ContainerDetails +

+

+(Appears on: DeploymentDetails, JobDetails) +

+
+

ContainerDetails specifies the details of the Container

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+image
+ +string + +
+

Image info for the container

+
+imagePullPolicy
+ + +Kubernetes core/v1.PullPolicy + + +
+

Pull policy for the container image

+
+command
+ +[]string + +
+

Entrypoint array for the container

+
+env
+ + +[]Kubernetes core/v1.EnvVar + + +
+

Environment Config for the Container

+
+resources
+ + +Kubernetes core/v1.ResourceRequirements + + +
+

Resources

+
+securityContext
+ + +Kubernetes core/v1.SecurityContext + + +
+

SecurityContext for the Container

+
+podSecurityContext
+ + +Kubernetes core/v1.PodSecurityContext + + +
+

SecurityContext for the Pod

+
+

DeploymentDetails +

+

+(Appears on: WorkloadDetails) +

+
+

DeploymentDetails specifies the details of the Deployment

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+ContainerDetails
+ + +ContainerDetails + + +
+

+(Members of ContainerDetails are embedded into this type.) +

+
+type
+ + +DeploymentType + + +
+

Type of the Deployment

+
+replicas
+ +int32 + +
+

Number of replicas

+
+ports
+ + +[]Ports + + +
+

Port configuration

+
+livenessProbe
+ + +Kubernetes core/v1.Probe + + +
+

Liveness probe

+
+readinessProbe
+ + +Kubernetes core/v1.Probe + + +
+

Readiness probe

+
+

DeploymentType +(string alias)

+

+(Appears on: DeploymentDetails) +

+
+

Type of deployment

+
+ + + + + + + + + + + + + + +
ValueDescription

"Additional"

Additional deployment type

+

"CAP"

CAP backend server deployment type

+

"Router"

Application router deployment type

+
+

GenericStatus +

+

+(Appears on: CAPApplicationStatus, CAPApplicationVersionStatus, CAPTenantOperationStatus, CAPTenantStatus) +

+
+

Custom resource status

+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+observedGeneration
+ +int64 + +
+

Observed generation of the resource where this status was identified

+
+conditions
+ + +[]Kubernetes meta/v1.Condition + + +
+

State expressed as conditions

+
+

JobDetails +

+

+(Appears on: WorkloadDetails) +

+
+

JobDetails specifies the details of the Job

+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+ContainerDetails
+ + +ContainerDetails + + +
+

+(Members of ContainerDetails are embedded into this type.) +

+
+type
+ + +JobType + + +
+

Type of Job

+
+backoffLimit
+ +int32 + +
+

Specifies the number of retries before marking this job failed.

+
+ttlSecondsAfterFinished
+ +int32 + +
+

Specifies the time after which the job may be cleaned up.

+
+

JobType +(string alias)

+

+(Appears on: CAPTenantOperationStep, JobDetails) +

+
+

Type of Job

+
+ + + + + + + + + + + + + + +
ValueDescription

"Content"

job for deploying content or configuration to (BTP) services

+

"CustomTenantOperation"

job for custom tenant operation e.g. pre/post hooks for a tenant operation

+

"TenantOperation"

job for tenant operation e.g. deploying relevant data to a tenant

+
+

NameValue +

+

+(Appears on: ApplicationDomains) +

+
+

Generic Name/Value configuration

+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+name
+ +string + +
+
+value
+ +string + +
+
+

PortNetworkPolicyType +(string alias)

+

+(Appears on: Ports) +

+
+

Type of NetworkPolicy for the port

+
+ + + + + + + + + + + + +
ValueDescription

"Application"

Expose the port for the current application versions pod(s) scope

+

"Cluster"

Expose the port for any pod(s) in the overall cluster scope

+
+

Ports +

+

+(Appears on: DeploymentDetails) +

+
+

Configuration of Service Ports for the deployment

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+appProtocol
+ +string + +
+

App protocol used by the service port

+
+name
+ +string + +
+

Name of the service port

+
+networkPolicy
+ + +PortNetworkPolicyType + + +
+

Network Policy of the service port

+
+port
+ +int32 + +
+

The port number used for container and the corresponding service (if any)

+
+routerDestinationName
+ +string + +
+

Destination name which may be used by the Router deployment to reach this backend service

+
+

ServiceInfo +

+

+(Appears on: BTP) +

+
+

Service information

+
+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+name
+ +string + +
+

Name of service instance

+
+secret
+ +string + +
+

Secret containing service access credentials

+
+class
+ +string + +
+

Type of service

+
+

StatusConditionType +(string alias)

+
+
+ + + + + + + + + + +
ValueDescription

"Ready"

+

TenantOperationWorkloadReference +

+

+(Appears on: TenantOperations) +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+workloadName
+ +string + +
+

Reference to a specified workload of type ‘TenantOperation’ or ‘CustomTenantOperation’

+
+continueOnFailure
+ +bool + +
+

Indicates whether to proceed with remaining operation steps in case of failure. Relevant only for ‘CustomTenantOperation’

+
+

TenantOperations +

+

+(Appears on: CAPApplicationVersionSpec) +

+
+

Configuration used to sequence tenant related jobs for a given tenant operation

+
+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+provisioning
+ + +[]TenantOperationWorkloadReference + + +
+

Tenant provisioning steps

+
+upgrade
+ + +[]TenantOperationWorkloadReference + + +
+

Tenant upgrade steps

+
+deprovisioning
+ + +[]TenantOperationWorkloadReference + + +
+

Tenant deprovisioning steps

+
+

VersionUpgradeStrategyType +(string alias)

+

+(Appears on: CAPTenantSpec) +

+
+
+ + + + + + + + + + + + +
ValueDescription

"always"

Always (default)

+

"never"

Never

+
+

WorkloadDetails +

+

+(Appears on: CAPApplicationVersionSpec) +

+
+

WorkloadDetails specifies the details of the Workload

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+name
+ +string + +
+

Name of the workload

+
+consumedBTPServices
+ +[]string + +
+

List of BTP services consumed by the current application component workload. These services must be defined in the corresponding CAPApplication.

+
+labels
+ +map[string]string + +
+

Custom labels for the current workload

+
+annotations
+ +map[string]string + +
+

Annotations for the current workload, in case of Deployments this also get copied over to any Service that may be created

+
+deploymentDefinition
+ + +DeploymentDetails + + +
+

Definition of a deployment

+
+jobDefinition
+ + +JobDetails + + +
+

Definition of a job

+
+
+

+Generated with gen-crd-api-reference-docs +on git commit 867824a. +

diff --git a/website/includes/chart-values.md b/website/includes/chart-values.md new file mode 100644 index 0000000..cf79b4b --- /dev/null +++ b/website/includes/chart-values.md @@ -0,0 +1,67 @@ +## Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| image.tag | string | `"latest"` | Default image tag (can be overwritten on component level) | +| image.pullPolicy | string | `"IfNotPresent"` | Default image pull policy (can be overwritten on component level) | +| imagePullSecrets | list | `[{"name":"regcred"}]` | Default image pull secrets (can be overwritten on component level) | +| podSecurityContext | object | `{}` | Default pod security content (can be overwritten on component level) | +| nodeSelector | object | `{}` | Default node selector (can be overwritten on component level) | +| affinity | object | `{}` | Default affinity settings (can be overwritten on component level) | +| tolerations | list | `[]` | Default tolerations (can be overwritten on component level) | +| priorityClassName | string | `""` | Default priority class (can be overwritten on component level) | +| controller.replicas | int | `1` | Replicas | +| controller.image.repository | string | `"ghcr.io/sap/cap-operator/main/cap-controller"` | Image repository | +| controller.image.tag | string | `""` | Image tag | +| controller.image.pullPolicy | string | `""` | Image pull policy | +| controller.imagePullSecrets | list | `[]` | Image pull secrets | +| controller.podSecurityContext | object | `{}` | Pod security content | +| controller.nodeSelector | object | `{}` | Node selector | +| controller.affinity | object | `{}` | Affinity settings | +| controller.tolerations | list | `[]` | Tolerations | +| controller.priorityClassName | string | `""` | Priority class | +| controller.securityContext | object | `{}` | Security context | +| controller.resources.limits.memory | string | `"500Mi"` | Memory limit | +| controller.resources.limits.cpu | float | `0.2` | CPU limit | +| controller.resources.requests.memory | string | `"50Mi"` | Memory request | +| controller.resources.requests.cpu | float | `0.02` | CPU request | +| subscriptionServer.replicas | int | `1` | Replicas | +| subscriptionServer.image.repository | string | `"ghcr.io/sap/cap-operator/main/server"` | Image repository | +| subscriptionServer.image.tag | string | `""` | Image tag | +| subscriptionServer.image.pullPolicy | string | `""` | Image pull policy | +| subscriptionServer.imagePullSecrets | list | `[]` | Image pull secrets | +| subscriptionServer.podSecurityContext | object | `{}` | Pod security content | +| subscriptionServer.nodeSelector | object | `{}` | Node selector | +| subscriptionServer.affinity | object | `{}` | Affinity settings | +| subscriptionServer.tolerations | list | `[]` | Tolerations | +| subscriptionServer.priorityClassName | string | `""` | Priority class | +| subscriptionServer.securityContext | object | `{}` | Security context | +| subscriptionServer.resources.limits.memory | string | `"200Mi"` | Memory limit | +| subscriptionServer.resources.limits.cpu | float | `0.1` | CPU limit | +| subscriptionServer.resources.requests.memory | string | `"20Mi"` | Memory request | +| subscriptionServer.resources.requests.cpu | float | `0.01` | CPU request | +| subscriptionServer.port | int | `4000` | Service port | +| subscriptionServer.istioSystemNamespace | string | `"istio-system"` | The namespace in the cluster where istio system components are installed | +| subscriptionServer.ingressGatewayLabels | object | `{"app":"istio-ingressgateway","istio":"ingressgateway"}` | Labels used to identify the istio ingress-gateway component | +| subscriptionServer.dnsTarget | string | `"public-ingress.clusters.cs.services.sap"` | The dns target mentioned on the public ingress gateway service used in the cluster | +| subscriptionServer.domain | string | `"cap-operator.clusters.cs.services.sap"` | The domain under which the cap operator subscription server would be available | +| webhook.sidecar | bool | `false` | Side car to mount admission review | +| webhook.replicas | int | `1` | Replicas | +| webhook.image.repository | string | `"ghcr.io/sap/cap-operator/main/web-hooks"` | Image repository | +| webhook.image.tag | string | `""` | Image tag | +| webhook.image.pullPolicy | string | `""` | Image pull policy | +| webhook.imagePullSecrets | list | `[]` | Image pull secrets | +| webhook.podSecurityContext | object | `{}` | Pod security content | +| webhook.nodeSelector | object | `{}` | Node selector | +| webhook.affinity | object | `{}` | Affinity settings | +| webhook.tolerations | list | `[]` | Tolerations | +| webhook.priorityClassName | string | `""` | Priority class | +| webhook.securityContext | object | `{}` | Security context | +| webhook.resources.limits.memory | string | `"200Mi"` | Memory limit | +| webhook.resources.limits.cpu | float | `0.1` | CPU limit | +| webhook.resources.requests.memory | string | `"20Mi"` | Memory request | +| webhook.resources.requests.cpu | float | `0.01` | CPU request | +| webhook.service | object | `{"port":443,"targetPort":1443,"type":"ClusterIP"}` | Service port | +| webhook.service.type | string | `"ClusterIP"` | Service type | +| webhook.service.port | int | `443` | Service port | +| webhook.service.targetPort | int | `1443` | Target port | diff --git a/website/layouts/shortcodes/include.html b/website/layouts/shortcodes/include.html new file mode 100644 index 0000000..1ed3ff9 --- /dev/null +++ b/website/layouts/shortcodes/include.html @@ -0,0 +1 @@ +{{ .Get 0 | readFile | safeHTML }} \ No newline at end of file diff --git a/website/package-lock.json b/website/package-lock.json new file mode 100644 index 0000000..0322333 --- /dev/null +++ b/website/package-lock.json @@ -0,0 +1,1611 @@ +{ + "name": "website", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "devDependencies": { + "autoprefixer": "^10.4.8", + "postcss": "^8.4.16", + "postcss-cli": "^10.0.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.15", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.15.tgz", + "integrity": "sha512-KCuPB8ZCIqFdA4HwKXsvz7j6gvSDNhDP7WnUjBleRkKjPdvCmHFuQ77ocavI8FT6NdvlBnE2UFr2H4Mycn8Vew==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.21.10", + "caniuse-lite": "^1.0.30001520", + "fraction.js": "^4.2.0", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.21.10", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz", + "integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001517", + "electron-to-chromium": "^1.4.477", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.11" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001521", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001521.tgz", + "integrity": "sha512-fnx1grfpEOvDGH+V17eccmNjucGUnCbP6KL+l5KqBIerp26WK/+RQ7CIDE37KGJjaPyqWXXlFUyKiWmvdNNKmQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/dependency-graph": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz", + "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.494", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.494.tgz", + "integrity": "sha512-KF7wtsFFDu4ws1ZsSOt4pdmO1yWVNWCFtijVYZPUeW4SV7/hy/AESjLn/+qIWgq7mHscNOKAwN5AIM1+YAy+Ww==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", + "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://www.patreon.com/infusion" + } + }, + "node_modules/fs-extra": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", + "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-stdin": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-9.0.0.tgz", + "integrity": "sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globby": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", + "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", + "dev": true, + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.3.0", + "ignore": "^5.2.4", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby/node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postcss": { + "version": "8.4.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.28.tgz", + "integrity": "sha512-Z7V5j0cq8oEKyejIKfpD8b4eBy9cwW2JWPk0+fB1HOAMsfHbnAXLLS+PfVWlzMSLQaWttKDt607I0XHmpE67Vw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-cli": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/postcss-cli/-/postcss-cli-10.1.0.tgz", + "integrity": "sha512-Zu7PLORkE9YwNdvOeOVKPmWghprOtjFQU3srMUGbdz3pHJiFh7yZ4geiZFMkjMfB0mtTFR3h8RemR62rPkbOPA==", + "dev": true, + "dependencies": { + "chokidar": "^3.3.0", + "dependency-graph": "^0.11.0", + "fs-extra": "^11.0.0", + "get-stdin": "^9.0.0", + "globby": "^13.0.0", + "picocolors": "^1.0.0", + "postcss-load-config": "^4.0.0", + "postcss-reporter": "^7.0.0", + "pretty-hrtime": "^1.0.3", + "read-cache": "^1.0.0", + "slash": "^5.0.0", + "yargs": "^17.0.0" + }, + "bin": { + "postcss": "index.js" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.1.tgz", + "integrity": "sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==", + "dev": true, + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^2.1.1" + }, + "engines": { + "node": ">= 14" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-reporter": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/postcss-reporter/-/postcss-reporter-7.0.5.tgz", + "integrity": "sha512-glWg7VZBilooZGOFPhN9msJ3FQs19Hie7l5a/eE6WglzYqVeH3ong3ShFcp9kDWJT1g2Y/wd59cocf9XxBtkWA==", + "dev": true, + "dependencies": { + "picocolors": "^1.0.0", + "thenby": "^1.3.4" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/pretty-hrtime": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", + "integrity": "sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/thenby": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/thenby/-/thenby-1.3.4.tgz", + "integrity": "sha512-89Gi5raiWA3QZ4b2ePcEwswC3me9JIg+ToSgtE0JWeCynLnLxNr/f9G+xfo9K+Oj4AFdom8YNJjibIARTJmapQ==", + "dev": true + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yaml": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz", + "integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + } + }, + "dependencies": { + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "autoprefixer": { + "version": "10.4.15", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.15.tgz", + "integrity": "sha512-KCuPB8ZCIqFdA4HwKXsvz7j6gvSDNhDP7WnUjBleRkKjPdvCmHFuQ77ocavI8FT6NdvlBnE2UFr2H4Mycn8Vew==", + "dev": true, + "requires": { + "browserslist": "^4.21.10", + "caniuse-lite": "^1.0.30001520", + "fraction.js": "^4.2.0", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + } + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "browserslist": { + "version": "4.21.10", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz", + "integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001517", + "electron-to-chromium": "^1.4.477", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.11" + } + }, + "caniuse-lite": { + "version": "1.0.30001521", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001521.tgz", + "integrity": "sha512-fnx1grfpEOvDGH+V17eccmNjucGUnCbP6KL+l5KqBIerp26WK/+RQ7CIDE37KGJjaPyqWXXlFUyKiWmvdNNKmQ==", + "dev": true + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "dependency-graph": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz", + "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==", + "dev": true + }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, + "electron-to-chromium": { + "version": "1.4.494", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.494.tgz", + "integrity": "sha512-KF7wtsFFDu4ws1ZsSOt4pdmO1yWVNWCFtijVYZPUeW4SV7/hy/AESjLn/+qIWgq7mHscNOKAwN5AIM1+YAy+Ww==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, + "fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + } + }, + "fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "fraction.js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", + "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", + "dev": true + }, + "fs-extra": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", + "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-stdin": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-9.0.0.tgz", + "integrity": "sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==", + "dev": true + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globby": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", + "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", + "dev": true, + "requires": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.3.0", + "ignore": "^5.2.4", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "dependencies": { + "slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true + } + } + }, + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "dev": true + }, + "node-releases": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "dev": true + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true + }, + "postcss": { + "version": "8.4.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.28.tgz", + "integrity": "sha512-Z7V5j0cq8oEKyejIKfpD8b4eBy9cwW2JWPk0+fB1HOAMsfHbnAXLLS+PfVWlzMSLQaWttKDt607I0XHmpE67Vw==", + "dev": true, + "requires": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + } + }, + "postcss-cli": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/postcss-cli/-/postcss-cli-10.1.0.tgz", + "integrity": "sha512-Zu7PLORkE9YwNdvOeOVKPmWghprOtjFQU3srMUGbdz3pHJiFh7yZ4geiZFMkjMfB0mtTFR3h8RemR62rPkbOPA==", + "dev": true, + "requires": { + "chokidar": "^3.3.0", + "dependency-graph": "^0.11.0", + "fs-extra": "^11.0.0", + "get-stdin": "^9.0.0", + "globby": "^13.0.0", + "picocolors": "^1.0.0", + "postcss-load-config": "^4.0.0", + "postcss-reporter": "^7.0.0", + "pretty-hrtime": "^1.0.3", + "read-cache": "^1.0.0", + "slash": "^5.0.0", + "yargs": "^17.0.0" + } + }, + "postcss-load-config": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.1.tgz", + "integrity": "sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==", + "dev": true, + "requires": { + "lilconfig": "^2.0.5", + "yaml": "^2.1.1" + } + }, + "postcss-reporter": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/postcss-reporter/-/postcss-reporter-7.0.5.tgz", + "integrity": "sha512-glWg7VZBilooZGOFPhN9msJ3FQs19Hie7l5a/eE6WglzYqVeH3ong3ShFcp9kDWJT1g2Y/wd59cocf9XxBtkWA==", + "dev": true, + "requires": { + "picocolors": "^1.0.0", + "thenby": "^1.3.4" + } + }, + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "pretty-hrtime": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", + "integrity": "sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==", + "dev": true + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "requires": { + "pify": "^2.3.0" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true + }, + "source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "thenby": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/thenby/-/thenby-1.3.4.tgz", + "integrity": "sha512-89Gi5raiWA3QZ4b2ePcEwswC3me9JIg+ToSgtE0JWeCynLnLxNr/f9G+xfo9K+Oj4AFdom8YNJjibIARTJmapQ==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true + }, + "update-browserslist-db": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "dev": true, + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + } + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yaml": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz", + "integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==", + "dev": true + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true + } + } +} diff --git a/website/package.json b/website/package.json new file mode 100644 index 0000000..ff8634f --- /dev/null +++ b/website/package.json @@ -0,0 +1,7 @@ +{ + "devDependencies": { + "autoprefixer": "^10.4.8", + "postcss": "^8.4.16", + "postcss-cli": "^10.0.0" + } +} diff --git a/website/static/img/activity-tenantprovisioning.png b/website/static/img/activity-tenantprovisioning.png new file mode 100644 index 0000000000000000000000000000000000000000..99e49ccda5f2f78d6fd7418e266708f97bc929f8 GIT binary patch literal 113312 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

^>ed56xr-d&$PTtL z&v&L@gLKSd=~Ea5kGbrI_t34%Cv{lB=d%|el|YUrCV59`3baOgCO0ofi?%Wqo#$`= zobQmV1PSM`N!#JqhiI_%t&f`L*!5AIKu-+)Zqz2oiss&3qs?fj=Y5%c*~z;+wX>Ep)@0$0DHlue~>XwgQ#VhTT3KX8LR*tZQ?y5mBt__WQS)Qxr${UUghHG}j`o~=hpaABrmSrw?9=F}gb9`ljxgGN1v zU5~V%SC#7z%yxx)X%1*DhQ-%gdT8#;Q{GG*T(trUO*sC3T=KL=8N~FYqDs;e$2&8B zw3JRfgx3lkcWiJKUwyp-Wxvq;XpyqC`Dwq$15rR)S%Ky$CTM4S1D5XOS6#n*0g@mE z%s5ASi-AJdP)}6CZiDUE5*lx%&3G_>V8un|910O6!9ys^L8sYnTjvlHA*8OO;dBnOuR=q{q_sp{L*^qK^MU)ae4vB+ct*D#JQ% zV@hmjqzplgT5Y~Rma*UX^osEAKD@?;o?!n6C@gYFI|CZ0sw^;PaY!d3&wC>l4$%${ zqc%%>I{)w$&Z^Q)XXaWn)oC_r&%ET1ggF5>yQ3Pk!XZjgiBuu>g~UPhgM-stz`866 zebCGTGno&{TP_|c_l%|wJ@w{@fuG5baNkiz=EG3c-PxsElPf@iE=dD002518)*KJ_aO553hg5!7oLG~@_ zYhxDPLq$TwZtyALQkR;}q(@TwKvEY8vx;kECh@!|@Q%3)Jikl^DG%BrapP1<`-fx& zzQ~DMb*rIxDPV`YrcE_E%0vvMaa!FP6)1!&LtsPRc!Ih_7_fyIf2<*(1X7qr0U7%` z){Hwxp1N%}bcjf4Mci%Qd&Ro}753UWvfas={3qJMe)%k#x529R6#Kr!?=3Chf|Rjz znyM|FTKl3Dk}`70n#yK68Ab3g!Gv-xgykn zJdRa|Gvs^W@J#w$)aXfOH2!8Brgwexq%KQ0%GxA7(Lx?5@>H%iiX2l5I@hr~P%O((-yjq~(va$uNp6LvQnQsD z>JZVoUA!ZD8m~G3@NBfN+k-)Yr=PR&^@I$B_3Q55#V&lgh=M4`G+rGXJ z^__os52Mk7`4}(WKUhY<$-5mY9xvTBm*r+tGV0^?kkjURN0=A$L2)Q9i{;U%@1M0V zoIpW+&7@&fUa_x8ebWb|c&5PDN2AvyGrE?Q_GtOWxhAB)KZiuYUlA{^V&4G*92`vZ z=d6wOWagjkbBcFKohWIJ^k;9&CjF2+5@A<oJB6l9#;;W149z5g6NPUHan%-oeo$$Q^$?vho9EjK6O((Zti{@U_fW9wJj7(H zZ|@~o6T%^FK2boGijnnfi}j5z<*{4sN9|teL=|D3e1Wblr=~1a_0J{uS$V~H?t+kw zX`t6v`27kZaN~D`P}z%pahCb`p)+6-mA+ZxST-ZySf?hv;aT%1NBq36KlvzBL|n5Z zoH(A5%gucaeR2V3P7GnJ{CF2{RnjeadlaK+=Kc^W0;R!}En6`eQ}6=8Py8DsO(rz{ z#`OYCX?-D|?TQ^I!}PIa0?z8uNQ0^)&WRw-zveqS8aC|@`NC1I zGmsBIESHjT$DHzuf3A~BxJWvit7DdRJWd%Zz4T$h%~Ztm!0c6G;i5M4DF}(yq|lN% zj9VrJsYgo36}&T1-cxnMN4;G)hWnjD*XGemQ>e4ekGG(Zd)tGN`qUdUX<_w_mdrpu z4^#g#@@oIbuK&bOgX4+z`7FW&+hLMPZ=HcKzAvfwFH8^q^7gbk7tbH(E-d`p*E(L( zKvPG)KprSiobp1M6$N-}_s8J5?!M}xh28ilBo*ey{MdqWDK3;Rk z5B;#0XN;7UlQ#4}TRk0%@kebd>#`710gW(ccTG#Vwy5d_W{N&lVz+N$v)<+-oOvBd zy1i`Z|Nm?QrLfR@Ny>5(SGa){_?Pi_otf5qD&FF{ypOe+e}?4WmME!y`o~aiz20GB zSmdi^M^N$H4vVqx`TH4X2YKxc4gT=`F(o_%`Kn&Rp#ngRm=xQ1Oj&3$qYX+e)Q}2( zRWUM+%i8Q;^EG+DLfUNxGP=G56uIYQu7Tl*!;8tusx@P=r^JPxKj)s+yITm4MJbk= zl_YCoc5!UQ-2B}F{L7Zk*0Tl6NMGNH`3pB%9ZZQp#;fc-QDsEjuY|c8?bNB?$#E)g zU*|7%gb7jD)f(4okl?)#_s*LxTlQf_jadb!U90?*_G%$outrk9mHOY4uK!9B-l@*@ zVFM}lyAwk`DX)5k2wfK1TX;nfj-1Ef#tB5;mxrBFbQ<%UFGpk6M~$ECtJ#NMUJw%) zJJAJmKm9FUHFU;FZhdtyLpJCm6PSVRB6PJlZ@XkH^rc8SX zo5(t&7!{&F0p>y?&v>abBW7jWvA)V%u_xjEJ3{-cyfS@wUMq^>G^$EVxx06Qyj7g+ z+gZk>G7ZhU#P1A&K_r&p(|F}M5Rz45#SO0i(bb|N5ecqe7?P`B`k?dHk7TCI<(__% zvlVP;{}kf=jcO_>K?v~Dc7X;yg0#XGQrv=W2JX=P^yZYGKM2>x8nkO?!KYX$+r&Li zXP+PhW0qCv${(o2?`Dv9keF4ONIeRPRA%ka5*f?Wd7=<_pz)nk@iGI<<5&0(Ft->x z8&`#o2?^q;T%a2ZuprAQajMSO5Vfky>*rXDdApRD_nq=6O5+3~K9vy&lGJqVH1mXw z`j7-###|8wGC3sMw)sKoOD@S$nXAO__P&(u{piPfaxvw7P87;t_27K5 zFC~QTnY{{hlvQP267mHyiqWYr!#Z|?3r&;f;aNi|TAV~wk_eJ)9znuBBsA-Sl0`ai z9($WWzV%1K1u@cp-%69HFwNV7kORgYpv*)e;$Gr0D{XZ9C+h8lo8}f(iX%Z|wqn!X zy_jCDf^A($rj6qD?5Xi6Q-Pb&7Fx%s(y?Kg>hY4gkLdATGu27Ko*8wOl82PmV9IrI zyk#!LPTRu@)JY#bf%;0kWDH?GD&jyckVA)M{D~~iw9)fnd@a{4gb*t!EvFJBCM=8r8qbe+Oh^-w1EQ3TRdxf_+K`UZ) z4@9DQuZ>kPia{_sxF+n-4Af?04Jj)0H~-LHYl)w~1#YYS|Gom9W2zM#xRn`y9Orrd z$B^nJsh%CF>8|)czFh+zYH8_1`D=Zaxds*Jx+5()N-w^xbhV z>3*zhw7OvfoMw4tV>k`on|205(T;;Co1K})#%a>pB~S{l+dA2qAqN%g8Ah#cI$^U3 zJJ5ub>?EhY+EOOT^7cjE_%P@cF25cD&c@_)S|*Dms2-xTw;3X4>m4~iEXAK=Hl-;7 zMV&FK$ghfx6(vTlfI57mH=6%ZCe}>+gT(Kn6C+MNz?VsuM!a}OBi(cw;eqf&szd!t!@{- z=ZBlB9&*2cx=?!0Yg4qYltBMu1qA`;SzB5kE!mf+ne{9yfg81;CwkDNFtZPh0=uF# z6`n}2$ADl{++KS36`>1=hkF1-m~-;r+83zBl08KRN7@&I5|76o;jD_>8`%{fCVpJJ z4-}tV?-OD4Au?j=M-dG(8ZTRMv8Tk|kp%St!ZNAwCr1V~Ohx-oVFEMCw}Pq*3Y#bDktV%X~ zRz>kUA|G4W!J9ph1-c!X|B+()v?-Zf*7?Pm0cV-(Gu)lpFn45ZiWh6vhSs}Dkbn@0 zt~hc-F}vca^7d6A^BsWq=sNFra%c2Lfa;b49uujFKpu!`U!UFWKd&M6k$p3>M^fy0 zVVAD5`H-uP6m9j(=kLO46T&7ZfoCd{7<|P#Z_w;!Rre{!xgdljY|&F&UXEQ!J6c4#L!j9D=XWBhTJy2KgN`B8$0(K-F4>IYe7)>`S+ZyE}aR(bo4 zUhTus+l_*M>||{hyIu<&X?sC!#D74_@0YK?fgJO&-ml-Cav`!KP1E4g>fXVkZE@MS zCgV6%#1p5I>Frk&cmZN>tI2-R&7S~AsL1DcW0Ip~9P_XZe#554h&BJrO_T(f5%2CxwkP{!#MrkDWdIUE(1%A4My*G7?+fhA)WN2CqKD5jM6I@RWUpPo~(YVMVpq*LrR&bbvuU{`j3v z(sX@|iA6U4>`~*Sx3t`)URV|?R^(BUik&ipeGF>!0I$StU&msmn(&Wmo9O>6Spn_?z@lL?|3?f}JJ-$DQrN;ewD)a*rK`nO#bUX(+ z_oWrFJ*Y!w!;!)_S5m{KRm$AHDtJ8l45*kPk{62v-wmE8IcjeW%7WVeF+)M;7bEEwHp`c_j+@WZGy`Yp4Z?>}u>{WR2}?TqW?b5{b* z+L|Vh+-c07bTJ=`kqgTNdECy^sIn~CZ+}>vHUyi5LlwRy;mGYJ9S17mR0uoWTvH)iXqZ^(;wl9()xbe$~o5vAwPA*qVRxZ>9 zIJGV0I%&k1{DZDt_M2NyG@f&kAOX9u=%M;7zYyrw+zP;IU&P%r{&1&Cb?+)SN&u$? zXtUoICb-gVi_#Cipob5%O;<`c!A2J0&{YbaLi zrXdL~CJQxJg2T^883ix;bgjB+s=}vEUb@G4EGaMy2qSG>@T*oeh0Lqkj=RuJ89<$q zw}j*SAU5pz?9-c-z%*y8(x^qbP-|0t-2SoKy9+j!A9uY4zX}oD{=vK*00;YjFeK(N zPE=<{%HV1*dy;Ysu)sD0ya{xr`4*lcmFV@?hg@dB!CCLUBjv#A{&z4LqeWaPAu}5u2EYToms^$eD*oz5<9q(kr zX^h#EticQGHmJANYy*MZL*Ke2I+>p?so?6_fOFrOI>-IFMnkC8@ZLzNHoHmgZej0w zBC~HxnnXr{O1{t@`v>!FS)=1d1KB&|4q1T z+GVy)?4vaYHX$6E3F)AAh53V*Qi%uIDaTK&)iIaS8L7+yq$0>T-_8R)xb=g0M&%Cd zQqA>MpFg)pojqEU555*9?!7D4ZPrY&@JE{hJoeXL%heMd>e-I~LB+*%viKj&pr??Y zi7S;*c&ZB+L2599f^j2`dqlb0NCB%SGt0)Tzl&bJ_)+o1L9Jc)Udn_Hdy*zwGY>=b z8P@|G%i7JRb+}+GY`!E>2Xz^L!?^!e)oSa~lCFA{+cIl!M&^=~aI(NxUV_hGp9sVU zfY-G$4;p9**Yo^elxlw{)35l&)d^JK=Y*Cc?~gXRn1Cjoo1l4`b;k@I1c-H%50g1Q zE`5&J16?J{`7si;OexBR6JWJCL7W9qi6%TsoZuz;bKb7@4fh)GVVCk7Fm`bTSYJb@1|hC_C(n2iN{l2 zeaXqrvlyi7FEcrWtsFWB9QQ_DImp@ogqd57XmFl2M^4LT2_)(%K+E5#G1w8C~X?>|wXa?Z`w7TNA-ep?qNbQNj$GTgq;Zk1@ z2vub;BSHGr&6-J?tc4lRz)wnQ0%N$_LxQ%p7A~Z)(yWMrBtu`wf7gRtCC953SmjF1 zM+_srZh?3)udZ3CUulF0V3CqLUVQFaGB-^mQdq?itW4<-SU6VLr@=;|Ul4q?VRZ-( z{VOzl8B2A9Bbg4FFuvhidKr0nz7h1xyc|E0O+4=574#5zHifv3eJLs zLjfh~Mws!LZ)~ZMrZiyl?m-lGk|KfR!Bu1zy-2`|sfQ$zOXT>_x(4Cvw9nsy>%sl} z4ev>U*vKZ2tEO+X|4q_eT>;vMr3z$CY(;J!a5hASo19LGFO^&5=eKwBjSE8jRpeO0 z4n1YxUacUsWX>6Tt=C4fxdJYU0@-SWFsON6OT{0=mP%;g6WK}!7GPxs!3m+x14HK! z@>`3)nfw=jTkIS%#umg#vM3<#lorjJ$E_*MpSG-`2#FsII8E30i$pJZUdT!gAT3`# zQo{q_wMUUknt7b0T0zLba&F2uZ1T3O)>V{-|G`Fn%u?pF;kbYS&C#_ls_9t~AFyFc z!!q>{BJOC265evG76VbA80;b!$3}Wd;i3!CIqjvuP%4~98~A)+m3M`G36x}usB93t zfZBuFBhl2CWX_9zt9XU&aoFwuCu~NQed_>#LIx|ZV~Mw{|i$(iI!%tvqXa`{cu=?DKSh^XGx+0jNyw@7X}M_5%Ai!O;q59zI&h1E;@w`yU^6z|Nmk>(mwOYhmGe|2WIHiopk7nG zABL)2erqbtZ1w4-l-7X;N0tTSXBlut?l~HNqBFn|zFP1W@XM$F@Cw>z6sR!Cgr5j3 zoD51$TyX=BJXRWDuA+fc@%QgZWtjb(aHu8nhoU%DZl9@6-^ejcrBj&K91a+>*lv6c z4h(tw(+1K(LD|4KDo{9d0K=7Azx1Gu7tEljNb1-vAK-xIt#=(koiev#-LY~t??urnt9FBX73CRSv2hnZgiP?+@wYp&?v;1BC8WT_Wx zU7+TkI~tJrah&UE4ZQZ|=%b_jHvnobzW+d<8fW1E!gS}{6`}yu(|;uM>qEkb$b@2S zE?SX+gz>ULF=|f#haBV{q`I4|G&e;KPKA`6*#!fE`OtG{Nmn-K#t9pS=ol ze3N;q?{H99G%2?r0SD(R!<#=3`yRgoc1LInf@qzJmG#bflK1T$-{aYaX4K<9{*ZbH zW+0Y!F&tUV0w(juZ8@O7<{hBpd?>RnYrm200dY-{$1PcAmw}X*#bIYaW|yQ?4D}er zDof+2)TfYA!K1M@K|0SL?g&L1T^;dj$-oIpS-kseDBCh=OyU&aDa(%MN`9piz4Bia zg~5sDJK$g!Ph}jpZybG){k(jMeabdEN;$?8GWUOgEl0}7qVg-ZN)aQeJYA~0{=kS4 zQEt-FK4l|Ml^-hwNAod2tCxJCR@`|uw3X2^v>+-+j<+<*W%Rem|C5-}=}J7u9X#A_5nUc^;RXs(1K z`Q82Gb}R7^`z zeIpt_48Vj1fyuk+=?@ZntWP7P}D?9S_h9sF(UBMxOgCWL!fqdw_{(Kw6V>NIr z1hzKQ8{gu-K#)6|^~EA|=o>-(!aZ1)hy6|GCJJZBX8^hbyo+GD;K`%d<-6g*akXxVaue!@vEE+^b$Tts(C@{XxmK-)Os@$+Vb_vPlbWwKLyLO+yD1R z-mKz-rvs&EGWe5%KqL!5*er-(z%Za)&JF$qF-EyHzkdnJm#@saL(I0c`(2!V*g33@ z5!T9F0+7p{Z##}GY6a)5xBHt|asWvk-d?f<;>S9FL^hF3@;dvjiowb!zxIv0ypL}( z^eUfO-V9<|&)v?ERlWZT!pP5S!OpIXX)c4F-6mSyoW(uAFut|Kl&N5%wKtIbI@9rC zX!q+xbdTGlVPz>Io@&oxm~2@gXN-orK^_EK*YOYUeFVb)8#WVz=-*DnIsUfRGDzY+gRV<1jd>hXS5uifb1gWNIKiE9m-iWu&{+kl zdYg<09`yHX2`O3f6b(C9Q|BlP#g~(Z$MF<@r!QV+apNlhl_fJpyvt(}VY+W*fh&uq zd835*I>qctQMRsyD0jpt*hKVa&|$LtTi<;p!0qGOI)?S1$k>D8DHwGy^m1r%=ZI9- zmdj6U;nKA9Pa}Y4sJ$hRmc?Jl6zWluQ43U$ysxi3kqM?WBBOMi<+M%+CcH2ody6cl zG_sRk#0EkgCtOChhx03tiT|ZVZ~D{&2E1pVV(z3a0w1sPO8tg1R8iEdZ&qb5K`9~~ zL`AWT2x(Krt2bP}i#TK-5xPxV1w9~0%=r5Sg`oQM)uKS+u?%9Np6LcME@sW7c#;V9 zV?xDlu|#oH$y0kYpF#hd1`m>u8m5bpifPIeC<-Z||8VzCE=`~qbe!=1k%x`(xb z%4nT^wMhf+EE7-lvBqL(n;|w?cu5e_{`C73n+MJ{iZjb8D2*|y;~Dh=lPyv4ni4fM zqhv#`-Zt*WD**Jq?d`rdPq(*tavCa1cacbwG z0feP+$fYF41{36nUOK5a!lcB}i%c>+5U6%cG}gA}fvSxm-!&r8v!P|}1Ud?nx(S(y zSYQn~?|Uun%AjPA**jbTtQQYbLy}U#UH%`aWao^(3--3~{HQOp;7{(p?caDwk}<^i zp+3uddWxX>FET-*o#C=l3Lrea%!R}IqNvpUH{+bdXZi9h7o&`zfpyb0dW5FdU3k=ru#VR$Pbwl-GlZ%C@Xb-p=6NQ z11gI#Xr%;zVO-iboDX3&7y9OPE@3mo7J0D-{i^G4MOSBC$gBb~K_JjxV*&?d9Eeh6 zDP6Pv(Lv-_Timx5nB=$G5e{YGb3&fz*n&LiQ~eOpweh_a`ghvwJHUD1HSp9}s_w>* zDBDfU;K4NZ-n;{B_9%efxlFU+oPMb`pcS`7dc4~D6(%*khL{e82ZN|wp7m5um=Qpf zzpYt)>ap_qS{JRkKEIaco-H6#idvYALvhC#X%l}FQut5QL)VziF6oq%G6*!1sL-wXVjUnXx>eVueW@!YyLOpx+%UOXhy_7Ruq2-eR3AZj>QeF(UFY~6lJ|8SnL(LhEu-%8E3C!ddGb?n1u)%jpFz zy+@#;XmWmm*m?G;ac~vrG$l^``x3l&U&Z| zV09d6;3bX&-3hAB07|=H>94_3)9bl>W^zWn=wJbtB8)vm9!LVTfdV1bHwdoPJ-pqV zDP^}@LJAQud2*js^q+CTG-SfR4AAhjYO@xrjYlI0nXM4jU}h`-4Bz}SON1*`ph*<9AU+ARp3nX_CC5`NErC^0&_MlMRfiD0ya~bsrE~;IOkkq~o!Ag> z=!PMt#cFE|HIxJ6H!jeUyh=W}s`bfch#mI|6<~rzgBhA*K?p@M!1aQAs{D`cW_nKB zy;urkaXsJcvWuPky-9Zslfq{lT*{6?gG{Hrk^Hr9q>KefXrVOP8N0_A487zh%0a@i zRW6X&>uVDTHN+9?_dUqR_L7m_28%+M?|l)v4o#%DWAaW!xeheOcKWU+z)X6e%K($j zq0G%)V5lML<7vlUn4W$q5*zqZ7g?(5dXrW0q&Y2gcy!~dv(xQxJYwp%A>ksuoH##I#Kuh#EMV+YPjH3<)_Cyl#$|+6!+$-w_J(jm zB6+#eiNmDsp_PUn){!+nV&V^X_=7ykj5t>>rwtutsL_1hq!`@8^mbW*w=fpP4YRn9 z_&Z8Q3X+%+&DLGHKxZ)*CjRIBbj(cWqVIc z{=jgru|!2ow=kfeXqev61d>2a!x{3@^)FZ8z9Uia^`k$KQ|?UocV~3^esHhr-!8Fs zWaUiOmFA#d*QBN2jvhgmKV+59FQBZM{ZorC841HD|Fsl~af0`=S#Cw&X>p+g%W;jT z@Y5lpb12v=gYeNos+KwUa%z90AI^kmJk{&_!lEjeFXbF#M|0Lcp|>vzDQl82M)b!? z{@2GjFn#~}!o;WGK*N4kvlXAuD7p*;Oo;*gHB?xsmkJxtJlyrm%1ry~pR`I*U1^!L z%rI5>3#5Re?Wl#hEZC(ZP3uZO0m?_0*x@IXSW#lC3#O3uzSDfyQFMX)qRKEnNsu)h zbqD*!!vl*- zuI|O~pNCqE>sZz&9H8UsWDQPM!{a^g-RI>H`IJGl}6+U|OtF_~Sc3v-ExvVpD z7gABHTkK_`9v>*{-yT{L-sMt?%A`*H!C1QZ?WWq#>4u+~z(a@t^LOyb2M#sNE;J2= zcRB$cxF~BO(=8ITw?O@_}b2jyiDUgTI1I!3As`nWVPT9!LNfkKw zD2RvpgatTWf_Q;ZF-%SdaA?Lj-X2`VqxzbIrT``k^$nDChe}!8Mh5`gFh=klZYuDd z@2r{0+~)RlkCeX%!f5WFy?0skR?y0KW#+PclLpY6slU*hWPvweraseSSNhj7U7tj+ z_UzR3-bCVqC`hkL`;I}=TpS8LOU;m@gwU%|{@~CXNOagC+o|PPMz}!ZFh}#>-Y9ws zgus8lsYJ+f5E6lBLc)^GAH9aG>m95y6rrcJFJmW9!e7e(fU-7jo~dv?S@1M?2=ky& zbg71@nlytQQ>ZsxP$7;-oB{~-D@>Xu8GNDR#XO>b{QTMHa04`!)j`F`bB3yqF!-LF zvU}0r=e^g10i^yF6t54LIlNn3#!ELEGPB>~7$q0T=MIewlRyWaz13*KzN zMDf zO7?uxq4Vp|pz3FiblluO-XJZu0Im{8@bX9xl6|w92G|Rr;iCIpk<&I=4T3ZKAx~K3 z&mH2Z_C?FK?!jDxx-(qcBIB?9cb*sF1-3Wz5g%oh;~aCiBx_sFUtEL)I;$A?WgFyyclKR=|)caZUxL$Zs_E&K~+3Jy!^d?L)Gi; zoq-eesp+H|L`=D^963K(amz+AGV8FWu|`1P)B3V;g4IB=lD*}Cp|$`6?90_IK~uG) zd#!M?0{rGt->13a_pb4;RVP_kOA#N$oeUs*oD zRpj&!veaIVz4BasMOl38Vf4_(s^ue=m{{go;qjw{^K*S^qWfCW1tHg(=_x#n65~pY zKNG8PkjKEyHw}4o=*Iq4i3rY1m)OBRb=>go)O+k5kQOw5s2D{6*&Z^p#RXzNJvn^LtlKmWDseQ}?7T(JV90x6qU6zam3AcI4 z2Ri_nkj#LtA@`9NpEep*&o%6$qs;4z_1-@c32)UX2&t@^7MoTgUt5F=1|D!*Z%gD{ zaWZu&-_(+c!|`Scj>S5EC@7j%-1CaoDGN2)@h;`AjEf+=lxTQKxJvck_QiELcup$N zY7pumu48x^IGNA0?bzy_z_=1ZV&H63v-(C}n(#C5r{$k_^J3vg&EPb+YFv|LG`Dfq zblfRlkMbFJl+o&*Ppts*h1tR!s!F#+d(*!%s=I|pmVR?dbv6(UGmFVY37Y+I7^9Sg za?1_&=d#=yS0XK5X57yxcOmvTlTK&NE1)aQO+eZolt>?s*F1`+T9kLC>xq1Xs@#9f z0w51!^JqL}k8Fd4{^1tAif{>}2)HgiKf?D}wd-$SVQO=i|t3Qz3 zD?Z}a854?ljXXn`ogO`4i>@QVEG#5J-}uy>ZGtS&OEG^D&Rh$3v-mttX>e&TBEFgM zjhYJE;Bl|=3TwEyO~_W;ObuxVPFHLD>b~2BEGK*$C(PUBVK8cxyy>Vlt{|kNW~-2I zU6PCSvjOuq4RzJ~Dcg#^)bmTcXTb4z-<0j8fqLY|;juwm0U^O&!Xrl3(vO)`i#T!Sj96`|h4Kw3;x7P@MFfl>aD~P3!B+K#N*S0%f1rqR^#u_io)%nzagHz#AyXgH=cLqtTkx329CSS}=q zDo$8Q3dBs$7kOKLlFLG8G$WCDa23Lo#-m1kI&anzV!4Dbr4>FEXst4D{jdQ!Nk5sV@%(;PT*0XwKZ%0ykc zj*zGX9j=`#5D~_Hj%JcJ=VfyF&Rn{P$CAt&1O9Q43N(z-$saw$7B7cbM??BZ9pm>q z&{AsFg&C0{EPmrtj~axNr0dlXaS#V0wr4Nn&P9pCKRr^QnZi|bmp~1G`oGQXo}uH( zH-+(^{!U`9{>rkbbu?f{6?kL+k-al1JN9`bzJs_cLkI+vFl&RQsCr$Jf9F@Sp=1B> zD4Fp{M5kPvB&(ABX*bY}254C~O)?q|rXN3Q=rGgPF@j>nZj1?eQ#j zT8B!J5h?yW9YQ9fgkOVuQq0vXiGK=GRMeZpEq$!*ILI$Zj$+hZ=q#=&X`n%r!E0C; zC{%)*-@W~JP`h*l(MPR)udZDx+igN9W7i;bA2;6MM&9F|#3$;bfZ5S?P`--aD!L8A zLAwUrBi=Bed2}?E$T8{iefW6*Jcl;00&-l+*(%B{`d;aILAeMVURsmC=7Twai}q1j zu+}W!aaAw#OIfk8O!FNqsyNPI!&}#+4t} zBW^u#>~k4jARG%Mhcv;YcAFkp`p7b1z-2+tIRyHc;}%$3;pTS732>Bx6j`zh#J2FZ z4}Bn$>5SU%P$?BKFrnDC0trH}H_%fT$aP%j5W?+Gc=>e^28Dr5^hpC^UkUvKk0F)s z#{XzTBH$q-Fbas&e&TNgOm=|;Gmoa@QyRPgog9_k$G(k(g&|C1zMCe>n-{Sbt7K1{&wNNZ|kbo>*D4v|PB0kG_J0W%aNdL$IC*%Zj z3i7DEM=4feQgcM1_7`ikg2!Cl;8y#p_ew;FxFi2szj)mXTA65Y;>F9b*KWaqvedJX zg}ab7*b6LRf_J4k_>AN1dri8ILyZ)nqV4B-r)@5IA zi*|?L{fR8y6ZP0aJ)iUcy$&XZ*)eX53T_sO#zk+Ir*S^{IYs`wcJ@RFJs%p1rBiRF3r zFHq*(C2V2&(M>QD++&aovG2F9jUbtNIQdC}kc$ge$4lTFK z_xhoLH6O(=et;a; z2^+<6a6>w(-pt zbyKow>24*IkVd*&kQ4<)Kt)ixk#08K-69|&9V*f&AT0`#QlgZA#5ddfdEWOuzUTdp z-|_qRzVX`Yy4I|jSu^K(j%t*bn3U&yq%0=lFdoP3-0XwGsbZPq6+2LoQpe5(2Zb_! zKwy&oS!W%p^~yl+12z*o`tuG50l4i=+Ely!np*|%TKPxwn6a0?VU9s6wp#2?#sLVR zO(OO**;zZpRVj<{4STDFxWTG;+GjY^c6n9_aXh#_x=l_-p;x7Qn9#fXnngzMA2?q_ zgf&anq? zMo>>35_WHN;qY>NyIOrP3)_l{&NLlh1J-3rP6r#w&sT!@viI30IbYLXMs6t?vv9Mk zC2GG!JwD=yXzLkczv7OF4-uDF(|cD?u4`yQ5(;I=1{bNBvq&YV)0czw^P0O%-h~FJ zF5jzHZ<8%rfZ`SaV4Yb|11rH&`!JTOSs`|+i6TWtPwbWgu#xWt$?}nng{PVZ0n%Ai ziA`wuPSMDBM=6A=3tv~nK3fc;%VA4IAlKjQ^m5m3X9_w=l$uR=>z?jj7oLAT(JyD)@e#Lr zT}1PSvI>-h>h1X*T`CufJQ3GQJkS!djxXzJmqkHQCze^ zB*l}kt^2*v$t#$DtcitR>O{}%%ZiR*%(@1u7ZDuS1s86U>tDka2Y~hrC@xoGv8mWe z?$t9sq_5s^YND_fb?x``lWOZ8(gd|a89)WcBOYkQM~@UzBjxOx33AvSA@nirq8>YD zdXTXfF?q8JexH2&fn;{VNl*YHm#1_S6*bby`#YZ8eJm;x+>L`Zk?!MbxZr4{p!@uh zMFb(4f2)R8>x#XypI`I0&su)5aBB^)8+!S%_iT!5_nnI!6e9Qwsi^)w?)|RM9^E`5vhuiE zulBr`IJ9Jq05EN{DYx~c`QKQ8hGhmb7vrjQxopoLVrIU_yKbObJlN{(X>_AN;~&He z)xO?ukUb#4;!0ttvgxkE$IlLJ^nkzJV()UeJ>&`^!~@!up8r zH0%*o3NwapzV_TyK>T=xcIdWx&wC|5tB3-nYGv6so(^`pzMOu!*rC%5p6k^xO-I#^ zOK$!}(+@iqoWKVYuWa=fI)Y!Z0v5?Ct%_t$$;ntjwQQeRSfRS+gE&Fk{(Js~%1?+R zfhuNWL41hNDaxZG;F+$f^@}6T%xt~{46SI-gkCUJ;}b#BnL?^2g~b2rNfvSV17Sfz z*LJ+&FmEICCzaxWDSZ!W$yh18Y8L!x@l`IYs&hC7I~q$rA&5ar&~Z)yRh`Qsyl{Hv zd~b!DU-*NY!14KHqZU0P_M?9OxznQ|Dy_hE& zu4VS%36P6j(IhU!osHS(L7_i3We<{$l$&`LoAtNAcNbU77!C79qOI4O>C9Uy_=H88FEdAg^!BV) zH@PDrTqRZkmfYQGcEV5lMA7cKaZS;rE_vp~!IS+@fMnU8RkiasX%nN~2vl5HFk0Uy( z{`h^(I?2>J_Z`ql&2(_>-nmaGk(;ONh{Z-3c0_~|R>vy>XKY-4{6-v&i$n%Aj||D9 zSgZlAea36I{FY*=!C{U`g%4%>`Lhg+wyWZB0LD-fr>Wziq}u`DC)}$G4txehu|Y^d z7Ck$`Qu-!N#}fDLz0+g?|Ln}iZ}BLHa^mr{(j5{9_2Nj)8=u-zM&&vzfga97Mf2`o zc)KO@^soYYUERg?F45`r@_4-6sZTL=p_in3inTD`yX$AkIyL0^>Ch21$>jdDJDk zqv;LMt}vZJGTX6;kwI4KX(2-pbfDGW(QI2-eQdWG&imrNi%`eqgL^HXRo1(-*HzWm z6q6@dl~&^vhh5EMetc=#bUaev6fWQgy5sBUzNcU^+#kah4)(thS4ie=BfN@psj6h@ zAWDXwx%>J$Za-6dR5>->Hdjz{j1q8O(_7M5>0S^PPQW6vos28~N<&T{H% z{}F&Xlid16H;>6#6i9gFI8u5`bJrFVo^@pD%WoxKTltot|IX*^^V!|)?9*oBtTHvT7(0#1hJ2nl|Y!{ zbZ3112ZD6wyr&v2lqRYdNz^bj4E}ej-?CtV4?Pt5t7ZC}Y^(HrT$h}1Bz`2su;!E$ z*?IVonkS*T0LqaRqjmnL!i39B86miF%gL+YC@_hD!|yof%t3?U9X^K>tC?~Vd0v2- zOezPXz?6x4{J_160FZ=C)hf9p`#640=ZP7nFQpdKlR!P&t0POnfL)!4b2omPmPI-5 z)_Pr?8Kl7}Vo!>^yv6)H-@fiT%*B#PY2@&kkk!~qf=J2Lp;)M|!4=>3kF?*%t7@!d zd-<3ZMXTsb0-Uu%s5W_w3UeCs4MwG{j+M^3Vv4Q{=~5(vrODbE zh^fF$_1X&ZGuz~OP4u1i0Le<!)2PpJ{fV@({!myMhlg z5QRlmQw}~hizK9A>j$`XMP{r%|4rzfozuIazw}EVM-tD{rc8+7MSoXqGn3Q1z8OR_ zzU7n0j?byDBHgJSj2TViqFFICf-yWI$tdJ)%H1pwFqtX`s#75nwSF=%s%BIDzeHTbAc3sS<_+U3g{TL(08_Y%pd9OtPAD_ER(tL1ODiX< z`OpF-mq;zmXRPRZx~F#$Ye4o1gaay7e-5GH&@InRmgj^i2rhYuxj5x}dR&d2cJ0aJ z*B0y-bYlRRn(qjs8xsTozWb0%P#bpY{)2%{Bupn%^%!#5n9BA1{p0&^A%VEddMHri z??5DcbSa9R^!hAtx!%HmsRFs;2NP9qJi=<6*9@EZ+HeGD+dq=(VxsVobPn8m&q}&T zfc{RKT}Ca>^kC1oA@BJ-sKSRs%6`^x6$@C$iiLe`@ie`%KAr*yrjbLfxO1SWS=Bp` zdiDHr#*+*GO8KfvTQQt>+4SVH8lEiAfgE;wv-|dA$VW`GRU2S(wu!IN!wCuIz&<~m zGgThDcHMD=&c~%_)S2@aUWM~EV0D^?-vjEI(grBH)p<+>tqLYswQ$3yy>KPEWPF%d zap+q10eB>7(DPyp^g_3|tWWrZq#IUsKWRPZBh+Ud2d|SJ+Xuz=CO+}P{JnzH24+Hr zqY&nfEiN~)hXB2;9PWoFde%UREc1c9hG!QNDXiRNZU{pf6(iRJng0tFc44xrN1rcQ z^af9&=_1F$+W<1(F0rq3UDGNN9PsssSUGZIGgt+v+Wi1eP6qm%L?bsQ>(^)m>%SO5sIv=l|J+?47o*-dcG26AP%6_qCn#Q1u?`!d zYEVo19CyK1{*b*(N`Z}zy*prnCdLKSjXFTqX&Sb*BL(UxFv;U4!ZPXpnN~Umb?c#qy*%X2yPD|w; zyObKF8B`cv?rc_$VSp?wcK6RHlxn&OufOSGVd6KnK4$5%GJ1aVHn%=cqD6&axs&;k zFMGN93)5w{zBu=H*rO-rbk2qS3NG8z9UIP!$H*)Ysg;cq68B)yrR8A^0+LnK$`!*3 zle;S{9YG_bJ?9k(zb`xszu>56;m~A}h0ocrr$S^-%+D77eLhJ-Uix%uyfqj!C&pKc zPg1T0u;^lw59Qkxj}`H#7vm-23}WhVgqe^Q;CO%G>0NRC0>oRd5tu3zNhd~Xlj)pf zbjg^H6D7GfVU;53ekF5|Tfj_j^ZfO@^LgEyp0GfxxV^O?pG8<{xm#HfvW+QGoB7@pM{}E1c-=Z=MulipS?o1ymn3;Z z-fvaE=TxQ4$_hJHqWJq&1)Z~Ro?#R2@N23{cQUVki^txi3f>YL1fbj?pr8A>2~_oJ zsM)+Qs?h%3C)}BpIDPXy!??&E@NA5hX?%^i(THqL=~i z%AQ(EcWWqLl|0S~3M=z)IkHppD_cH7x4c^z+5eP%QlR{q4rv+1Z}swV0yaL9n)h-b z?~!ncmk#~kB<*y?Oe7D2pb@OtDvUEGLD;bcj| zZCs7Jr*(P4ptPmYZOq>m(@HWP+UFaC*CkL5djrn2a)KsA53L47Ofruxcmks1&r=-3Mff8+etT? zhKVR)=~W=-O}&h*x89LfvfX!{``#B1XshZ{2)g$YQk-$aHSFkg_>z;sz44nl^|052 zQoY`7^*S@d>j_Ux9QNbpZ%|b(9=+NyFuX;)B3c!cxK?`tBN)PA9s}cLlUHt5P&DX5 zx((QyC&}}4(Qc5+Aa^?53nOK=TEo;tEm zmgbdp_Tzjj z$L|pg{al3ZhUsv*TuXzllD%@6u~JS+L1)Y&`2sNgZkpHH-6#dBPuPTx$O=!{Cx%5X z(GYXz(HmpB>SC+hdx|sfrMwgG25w^+w=Q9 z?TdW&sc^3m<>k~ZKRw7kwNpD zG$9=q;*?o|5vGkAbF^(Qs@BDfm-tDA9sY8PNc z#wvE&B!=$IZ2TkKg`~tv5NjMYvT#>ik~AhcX8J-yWe5nyB~1qpxZE+b-=pwn!O)yX z-sC;!33IH5DsQzZJg|w$)9KRs^LvxHr%4!QUf7fID?oW5{kfMp2qbp=Pl7WFOK@IX z9*(IfyHDJ+$-Wyj600n;ROV_@S*|SKz4<~7mk+W7f;V}bEL1H!P99s2>(+jJ$>Sp* zVio`VhhN!fr(r z2Er(do9-NYaQAr|kiHk)22(f2-s)qru>Gho+wq3>LqOg;9C1NXZK8LFl7x$;lzBYc zTV_QPE~gsnVQm`)erhL|fgL0vrJdw++{B{Ac4r=j0osOVRY{aVFq;{bz|UW!2poA# zdudO^S#sg50gVdLkon?S1k-pG(TBO&7}aHi$hwrn-g{mwmW&W(ypY{7u*BsVC3X2jyF9u!w#(nMb7$>mli<6PotG_- z{LKogG=isvrM@HVVz}W-c2AK%8{01{bC1MDv(47sBB*>?<*>R@(An;PCcTum%W!@C zML9BjB-R&@<&+pUb55zJ*Z1;y^*+3auuxhpvz`MK4)dLisiEDf`Mg`q*&KTERdM;E zwKl$pbj;8Egu2wUUrA0G_~lvN;J~<^^OBd%>)t&*{ddWDpE-o?PSCx>fofeY-i~SG zo?OAlv+b*(xB4$liCt5~3FkfGi3}NB>9eyBp@A)HuOnV|@Ua9#o}`L>g-FEX8oFPx zk`$`ayDY-ZbTfm_uIi{V-xo7aIU4w;88Q`Ouc%Qm`v=CxmR#_B5ebnNsL_ij=mrl7 z@rHy%6{3}AbYw6;v-N0K8BxTxqRstKLU{gi;l9pgto2_{WaCO>h1Tg|Q1!?bi2i}2f!wXOmq!V%tYjNkK#k(lYg1$F zE(!Q^-9`I<>EWz+5(ncdvuaCX!-BP&QQdhP|L$4PByFdh{P-GWboccbmcqV+Vu_{4 z4Og&x6`$|8=tPZq{PM2474PGKpD~eEvOIKgmBWYN){xRJ^dB76Gq86CpzFJz$);rAO3W7*jVmD|U z)R)t0?6|BOK7OlscyU>&FTw`P7M1%6k}KRN)Ckj-5nh$lk*K;V7)1YOrzD@rN!89P zAWa&Q1l(U7W6a18C_-rV=$DV$t%0Ihu{hM1u@~v{4JV1(uTb5>70?&Tzm!(S=(C?o zCpxKHVHnb5!ZVe`{Y3!MDy$Zr0+Sr}o#}{YU9nUc(068JE+98&`^)~u5@8xIb>U`T z)|;W-5gv5Y*HvPH*l$dtA-AKt^AY&y=_KxYN}TNm(Nq!##nQ=JtDQO6i_WTc#hsZ1mvOq=HfKbiFAnw6OQZ@iNOmK}a?O?!E?hqaG!gZcj^0Wu8D!qA>gUUex?S2k z^A>V^Tw;3*?;2Y@JUxa@M_O1f8CI76~+-!Dp+=Vzc zy8J@9TB5iouOq?wT@kj`h~J~V!1hmnxd_jnC5~onw0mpp##fUqW+VqXcA8~Qf6f%a zkrJCqM*Yu=@q|RaRiw{^UIcp*u%5cA3I0S$s1+#~q3~v(M#`~V$Up^n6m;e(4kSN6 zj3XHuWw^_dU3#AsV4Rx#bth}Oq*{RivpFyP3>zmQY{)!4_@A0+hgCEXM+7;q@!xR>!jRZ!zrfz6sDXcp2)#K^rPp-~bsYMcCP0Ig~Rf!Gi&yAA94T49sv=khVD$9GzKVRIT zJ^R%7Q22$eM40z8?u`{u!S1%*&#ZZ4$_wQ`y;UFF-wGedY24l;Gg?4K)hS6AM=O_T z*7$h6UEAP%)Wt<(UTwCyNA_okEGYcb2B(J)QG;0V&<0DRg~#dIL0gM?VfLZf6vbZ^ zG6hAzFccIz`tv=7|ZZ4M~#m_#pk zvyQ8l4WxW>_bmSTu-GY={I~Ap3YXt;()nY}rzr~l7Nf(eM3v5YJar(u3vrwVgm=^S zL3mfUHt~9cQkUtuKiY{>EIBc(OCvu~G!b`cunS7B{9hJy=NF_d)pWk=|EO}h=>OQ1 zZJkxHycS2QrzORsVS2KmWH=$Zp|zOtm8&m`Xluy@+XZ|A+VDXqhU0)LXSqb~I0PDc zYaY(-Nqpw`VxIigQEc)F-_g!b0zLXF4hw!8w|q%F1)X|&7}omw(KY=7ZK)Q;YzU2K zAmqIwl?jg;p|zG0EXdlg0*#!37xCo7Dx1%mNnF>mk5gpPoNm}jv>(JU_lWusLNcys z1h)=%w+6PebNA792WNYTV|Xa=CsTGm0yAJ^}$+w3iJoMC1sf5Wsq%U+uMV1M*R zl1HA*km=iA<;aV-wD=6nzdlNG=X-;)@^YhObqX2kqX@7KD%F5K4^d!FO)%ITMiu&>v-CQ)|Cmtdwuhyg}>pXM^ytHo@bi>=&sW(+dkvq$cxxv}SlqbEm4~ zIm3T`pQ`0$E0Y_q$olGHH1_A4g5C7vcSKP3H~4fTQ+(wqOvS9<<0mX-K2#S{-;9Wm~msMie4w`HQg6$O%& z=8{o)Nd0+;3Ln8T-!GBMuH1hwHF{UbK4mnN}^tEi2 zgxuJL*V#f|71phggQ%2n!3UV7oVFDX0`Uec!yceW%oqs6)=+q2t$ER}CsZ?(Jb2-Y zl#$3Ou?Pe#jR>gPJ(y`V-%2zXuQ28OQz?k>nfvS>Zp6If#>-%R@T2=yCnebp}rh!UpyZ?uJ4>(}zo-G^lk1 z&h&wc^;^{trMm3zq8asyb|~r`#-KP_Ihe1eV2+xceZ68781TLDuyDqbCA-{;S=^Sj zM-?z(_Rh+fq%u}!k1rk`YxVd=ASoxdQ1QZtH++HN*ONbC)A;$J)O9LU{L(OA4O@!U z+o?lnZRv&Fy@V?lBRZGDh&zq_;ztuHQjW?%?LPKAuS#^9Qmy0$WuL&VK|X)1bP2bk zC{mF__yS-@`d|bO)!T*f99HiDzj53$>+?#-K*giK0EJl4bwo+!)4rr*iy)Z1V_#m+< ziN%KQM4(X%sUz1wfbmy1YfKJ2|5k-z8{TXKT8E;Rr^{6L@~t)3d`Dma&tA}r{qaP1 zb7t@#Ny%^6ZXknv6E2qHSg3%Z={tv=40e zTPrN_F*xCSq--33E<))*vtZePC$aL?{h>S$7=@h#GgQ?MHQtbbh_b}v-BBYWn2`gu zJ~iJm!*VL*zW$rrfHGYP|#cB%(Os|gqvl-J-pq#R|6X*cF8dVq*#dJ{i zudal1#eI<6TM-pg{AcFby@9kK3SPsTEtit}>;dS+1rnd19u?=BVvG9$%=B-ueTiVg z>w=|N)qvx4WtFHF9F>l9j4Ws>_;&rQzzYI`EMTW6D* zy#MSeW=nfN27HuA*~d-&Xspm`B)fW&<*&K-Zb!emehn!z8q@r@VxqP?)m8yRm#I>P ztrO(_-6do?4_VNH@KY@3Qs(HvD6V%gAt$dxsv7lh*BpUS|Hjq=dH@g0L+G4TO8;92 z*PevYYdFjgO$q<+9f-zN-+p%vz%}Op;Ze)`hZcyHN0%~NxB?BKhbQu61Z(EuQQD)S zvH`GFD(EAmsHR?id#BAf@F!J?HaY%~`hjZj|I(~P?whLZUdl6}bC|4dtj6xXgi>*l z!hZ=r>%sGK_|g9BSkc(}3UOp?GwsU*n46t$!caa-`sv0KNyI^X^0F1;GfuNHD?f0x z#rQLMWo5R6zP^-v%|Z{mjxT_jRAfOTVBdkAnK+2GCL-puX#WYUo1z^_wGVBL6N}gV z7Rdz7{ySV48tPw`WvaOU<|c~~z`uNoi>*-9*MEqvm>WxIB@vx_P{{@DWhhF8SJq@- zz_NCH>##<9bqocJi>RL$r`WNCdv5k0N&w{Z2%tso5FMT)2|f20DmUatS$GfW?^s)t z>2%?t9EXsWg(?g#`kWI=0}O(*?X8OIUJBkzM^wOcRR@{2*>HrY#nTHcXrjN^u)JTF z)CO>2cHtjUvj2y_GBf)`HjU147!(bK>ne7op`A5c!erPpJfnfhbz?+OoZi5izfz9# z?L6^P>Btdqsy-wo6(x@FS{LjOjYmWSz6tln|%X;J1|H=zN z```bIH6n?!(Y511Tkf59-a9pXyg$ZI6vp7PHENe;IW|Ta3o4~mqzUf$3!A?q!Z|$W{S2k>>@QsMzGQ@6n5-5$mhPO>TA)S}Bl13hrRn8L zB?2JT{?yVs?0r-K3c3ACXX=1Qqo6`vE&){>l6Ra zug{JYlbba}fGl0Go-YS77VQE|AbebLzm$vM^Mg0p zgpi~azuFKBCGj2$`}&?rwt<~V$WU1W*jsvvmy^lrGu^F3lh2;A{9Q(7mTE<)Y%h8+ zU-)8S+?DwSGkPhLXSy_Eq9RSn1X$A#qGnye^=2i#$(?}u$@5T@k!wgSR2B!e%GXyZ z>3$R=vOcas=m@84IQO5Ykl^>;ON0`^I}9d`wLOYg>h1s7oFsx5pz7&(O-+*uiCJ*M z()6E}CaFDSGqDo)yWL+Hr}k&Tm^*CjqCt~fN`p}UO<;*q({bO`hNTo4fmSf6?MN#P$=0s*>kP?$dpmFR$<)CHFb{5I(+|*TrAYPT3zd{Tt|5#^y zd3CA~3))(jKOX!zs3*$Opixf@gu0KCgQfLDcCdSV_%IPIJ-cJLqnZCOqLl98dSKc zr}#KL64Y>~dm%5MVv4g9HJz43uYU2bOTlYmO8-a$&i>^+!ButKtq9qRdaP|FRW_>F zH4Rg$IQ-d(+L;PYy9;n^0RK+zS=up5z~A2q$si)dVQHVSJCqGZ&(4Xoy{3l`%l}4ViM?fUzmY^LTA0;gRT%s1s(^)#<5$=Jt~sPb z5HWVftwhW8{F#fymL=Mm1;pM6!t+0ULR+neL~+N%UQ1V=6F{`zGON<@5op>w4wA06 zzg088i^wAI7cHke?|&(};^y;rL3)=2YT^j7)S72X`MnBE#i*Zk;>E!OZgt^}^@g;Q znh5KwJlYP3m)z0MCcvE!w3C=E>Wb`s4&R5>wv|*j-@*+S-xTQxBPa|;SIb_9AgHVJ zuI94>QxNu1K}xx5T&r_aQSF00=RDcE9BV~?+08c*wWAQE-22$rMt&IkT&-Et(2(y(jTgkv~B z3uO|_3!C}W7oL_LKwfdJ)Z;o+gjJtm?)0P9oE(m0%VdWtMMXxz6) zhUa0Uk-SaRr2ct)FIt+1lCNai(az-C6$H^)eMiWmaG5C#M@2ebmQu{+Hc@Uc`zAZt zU433x8c%b$EEk9pW7+7=XOt;5dc@$zoF=VTSJI_ju&EVCBklurwY%~XrH;`qk;PXL zLDgs~ml>TC_`C~&2C=J(nXa^HqWyBExsNHCklfcT%{ID@PiudR=(r>;w*2O<|FNGbZ$8WQk&*M;#qzo#eOU@Ip z6z2%W)%Au{h6&{csB%+k{?vKci0oG(HN#Wo@cioRu&3qfve$p&k+pnsxKZ25p1Ryn zxps0~*|7BWwBh%0%B`Q?L`#9CLrh$J&k(qJoP!@WR}Bwut&*|GqH*t-n~ne3S6&n& zScKf<4x{dbiH_%aG>G&cEFDE{H4x=z zo*C*@=CY%xiK*3H(}?P&zr5&fxK)1F&Wv?M9TWL=@rib?xQt$PP5w)fNbZ1A5(QHY zZ&K-#2Za~j}zOsm6C* z>_)_os*j4sQfLyF`6BPx8CZy>mEM-Vr+09rNy5kQ-AIXZW<%wc<=})S(YyRHEuwcb z>#qeENqE|LTt6H97~x8%S(LZ#8^6W5@MJ?Kz?YyB(np&4zk*^{` zGHJf;^%U4p+Rs$ zJ1sA6XhbP#M)lTE(unEjH>bB}#ONOUBHR9z*!zdKl9!Rk&V@5UM(uWQ$>dex7YR?F z6_m4m;n4ALsNWOpaNx7aaZ zrXbyZK$)XR?TYui4!a%az=Cl$3bu_nA>CmYRV(%=Zu;0mrTk~cjgMZ%P~5n3g^^W| zKmYpT3%YHG(csbDLi+)!#h6+Hnd1&gyk*af(%Y)er*Au_U6pST=)Y^b!!f^v_xR9v zyT3kP71t4%S9I|cdY2SULmh($*_eUD3*=Qx$+;Q>T zQE~E4R`O80as?N|E<5vnMCEnfMIt2HPnHP&aqcBB>6@s*%{#h{zuXNs$1IbT=GW^< zkx}TJ%{z8NdYoowa8R;2m$54)U=)={AQ?zR$>8iq zYi0zF1n&M_s}KoYW`zGb-r(j-n;-HDHKy+*{ohLx6=rtn{t+;83X~ng{oa&9V?0jx z3l@OF``O1KHmxW!nEy7@5{F6wN@F9C@aoyvjbHXqo3f@~#7*oNev>lEpKJ{cz z9`D)e>$E=z(8xNWDtNA?5$>}bNce$W&=txh$1ue4N>QiX@=I1`)V`))YeeZc*R%27 zrnvpvV*|Hu(A^xRRC)wjO>GZo{5sfENpnu`hD(%L?lhGwJi7n#342BF^7iT)ug{h> zL#LAbbUg|gMf&%1nDClWG2u||;)Jh$PuRaZUm`$Ktauu6^C+QMt4*D{%H|yvQ4}bg z+Cq2Iq{X9lY2a$sxz1o4amapk87o$S#MEfszrXWhgvI?PQ+wW5SZ6< zcMrBtpcEb!eZ$ADE10)dg#iqy9d?|os$K)E>Y5+onD1-A{a*OP&^nzI_qo5%4x^$t zBZokuOE}v3Y3-0mbZ8L#9c3YLhK+jgxlW~w_ivw*asIW==8}tOuueqLIbj42szc51 zy|Y(u-QD|^4?2tl`mZ@6cg!`SfCqsCriQY#(>VhRc#eD1tf}%STQ!Y;gB^3-FkG99+Z_^_|os`9cQ zKd~FSSa1CI7w*{yuVH^dNYn3@h$id&IXG?l$Q=pSAPI}%WVTGEmCu+g%uYZ-1qT0N z!GKnj$gI1QeMmtMMuTV|Zbw!1qk3bJfqa$*>x9q>So#}EQL=Jv_`Yjb=fbT~VDHq? zkr+Ch!&^?5s8?j+$)db(FA@{0HyRFCa2HG@RaXOybs$~zW5L}M8)y6jLF+BtB$dYN z!(#W7B&Ru)3$Vx2_=*_=slS(PdEq4gB-L<7O2iY`i}?(;Ax?$Uh*n&^GvE}69`>C{ zZ+np*gI{pGRKsp{Mn3lN1@J-bq>GFrAFO1lrzuZRc;P4gyEUb!M6(*L8a?fy55N21cB4Up~6zn*W{d;B0Ke#tXK94hv5Jq!l zl0Ld%@tM^&uV4eysYb(EYC24ttCi*Me%MO4~*}kx?D04^2OjQX-5fKs*s1-17y~DI}t;ny0u*eZ%Er-ZQfQ6%d z<&-2M`^`vH>aXwuM~Z3})z#a{O6PX)bYl?5l?=?SGd%Fj3XJ-4>x^0&_lnFX>ee46 z=P%M;q;YmX@OZd->TKV-HbJ!tYm=|@VtCvC0{Gv@DU}CvWTK&nedR8Uc~35 zOkZ?Rqv7xIq8lD7zyDxi+kJPJf>ET)%Jd<3oW=A7RS96+F-2kdyJt(EHO8mwqN$~LaK;3 z%etU2O$EgZ$kLbwNpWgS=P|>dbi#e}=FUQpV6ub-eIccinjU6dcu*qjx%!qt|DDAy z2{oMe0KdtKA^x5t=Wxy}1!l4MK-m%Op=Foxs4rCRp47458wrvs3T|0ty@JbxX7i`U zd|3bSL;48_=XY&PH#`|092{iM%*^!cieYSf2uK9}3xtG{96eqSQ5Zx##J@Wn?Xcy$ zL+>t2$$bbmsVU}z#jf9AjH@7<-SJ%`#Z7E)FgG_JcX4q!84?3%dADtGrptFc&Ws#j z)Z7Pt7$CfpfpMI-ob77?{egua!1#6Bgx_r=zHkAwo!;cA2v9SExAxNM`ABDM)>KIQ zAB%tKjC5CBvL%;{d)r8E!6}g6)bxiIAm?Ht*&nq{sw}U+i+6{f*MREJ&+t=Q!|mZ zO0t{7pFTY~?oH$+#!eRboYf6b==_82<2`W$<(SIg&nNZ;$~gkkgpGHo$eMd)-SrpdqiezUXpym#rvL*B=hUOSJ~V`6J^ z$JR>;JT-_sN0qJDaIYQDG~%AH4}SS_T=Vto*Z3(h`TVcX2dHUVT3SAW4K=rYt@UC= zUWO!~78U(@mDJMFoJzNdDo44fKL3JJ#bi9=zupn;=jV$1ArsJlnK1k{Wkh93TWU7} z=z}Go+-wih>}k^8jWE0DQ+Gw0pjGE<=qLIlb+w2DMno_na9mMU^)AW;@p%7XGhpzu zRQ@ksyzp%S0&z^`?};%iWYPq%%=ke8U%j>m=Ek9%Z+Nu&{rli>p}I4`oFc{Dx9i6N zL$e?B=FJuy%B5Mrd03F741M}ka}P?~ zAu$QNt=@m09(F6JP#SEUkW+C6pk z_4Umzpk0gV`i%{**W~=(*Rskb49gPN?V!YH?FOp7k^r!~3hIj&_j6M8L*&XD>}trlO_4 zW$8iGDwTw=$$XDF5lPOea9~)32~+!A4XsHtq{m;FS8NLejT=mA{F z9G*263o1+KvsPr%WR!lht=^A=+>AG4`{d zKVG}vmdg2%Xp+t}E6vI|Xe zX&<0I3X^4DwTT|9FdZM%*Xh^Sh8`39_E4e;|MpP)|DV(RmE@W$3>$EL1Z-)&ihh`0 zS`2e6&A~5SbnXhC%mx2LCG8W?h=i2_E|#A{%ed{pxolu%~N=7&|y@szQIiC$uL$ChwBzYmFee+k+O-(ZJ0|JS`|dcoKdt#+?h*7KcFQ?1@j z99kJAE1!z{ycF)f8plQ?P)JWx|Jr3tV=;DKRi4QOn`@LI{&O$?|Zx#JvmxZVu zldLKD;U`t3q*vQkc+?mF zzSex;MS`_Yr5XE*i0WM_zT};|s^cn+?;nLL5CP!1=3Fzba92W zmBnfxd+G-y5govxKB&UeL!P?0{GgZdIeHCH*w$-caVc!uwaoY|8pCiCmZ7?V$I3}L z?&2&d2e&$W%$`abmu*u)HKtQ#HEAdK3wam=3de4Mo*A1||1e-pwbp_g*;QpNS?~ZP z@BrmNlmHPW=tm2OX!0jme|>7$+W+l4UF4%%*|Ar0#-?@Rv9$yxcFb8^2zRVY020cPC2xuqU8xdr(nuOVwjE_sC2m$i!YV@uuD7qSns_>MbyR zi_)S1%m`5${qW(#H^_gNK>>*{(_^*N3Ddk6k28K;bP|O>Pv5>G z^khlnvRS=O_Tfmekk8Tn{h$H6ISJPvBQvMRGi$(A8v>AK#BF$fWpGGHW#W$S%U;&} zYa=E~Irc$#C1Y~mn@SCkMM^w3sYe8{zeX1L6n?EAt~6KV>CKt4sm7Nq2Np4uG@}rg z3d>(KsxkhV=*lA-O2qSp^k{TIbmloEc8U6p#XAaA$rq#_o~5cC!>=XZ{M9_>^`nWq zjOVspL3r{TdS(ddB+M#jkm&9V*w-fuL%2K6j>lik_*_qa{`~3L6-j;7tM78%0$_mpgfb{%PxY+(l1M zuT6y?)B~fDa$&xN1*Za**%&Rhlrs0GAgcM?`SZ1IzwYer?X7;<^!XiHC*)zyDmWlK zur&F-KFZ7$X2h+u{`v6$j`U3 z&(*p1e#m6IbW`LO;^DM^>CidJM?wF!q&QOSlKxL~Umj28_V#T`R5CW$6tRs_WGrHr z!Zr_4iOh2uLdp;#3EQyEQS7{LcA(p7VZw&-1+R z`Kwd+zVEfJwbnI$zt^fDFeU|J;NoOm+qVv18H_V+j%W5sZMxkr&#|lg*$21U}`L%cYL-}$^=>2s)s(60OBR{pEEZstJ#ag<}{YO1{`q!aX z(1Sr{70&3;L(s^n3($1#eI}mUn$~;EYfAk#HXSw)ur6W_t5etKu_siN5>4W ztBIcSrrzQB2`(ZQU0Hdj_&K9@>)`3P>iKEVfTbR8*^vGmZE{b_bjitE5i2@~%X>y8 z=@jrwh^$lQu7?e}_%pj1uS`*=T_z>vVQpTotRuua@eSdwE`Uj&2gw{NEI*G5g#06iw{KPh2LQ1$WZ;(=aph-=*wl6eKd_XG< z9z7&8J49Nxxe0lgyfxX_j=m`Oa>DUETE{Cc4*5s)1C1PODugGeZhbD3dSvL_*kc#R zIlp*}dw*W}-1B$(Bha`NN)ehc3Tes<%ONzzrpDUQb6ot(p!r>U0;OWQDlT}hLhd1q zYd&{nLHS_2io#6vNbL|bhvD7LX7s_5>p2v>nlQu{aa`(oub+u`HEBCEL})d>N`f#F0{ zZzroPlB{;hX_2+5Zb{fHC#9i9%G^J+*zQSO zU**Er;Ij%cpWNg!){_27p=R9 z#?LfqYYewU-iTH7vS?2i^qb3^!3vR)7S#F5 zEE@Y#e~0HBMb7}9?{@EjyGIk2ootvKHazb6BQ$-Oo!Lw`o!Ra&n2dIabPoz(#poYY zSFHJ=S11-@i5N!!>-(^hv)jCzq;fm#b*AAM+_N zFMOz7bM&1}8Ilz8GwM1V@0QamvN_7;Qj-INz9n0cxK!5)*6iFbZ;qcbK@+!Mt#bO# z-GVMnnOAK5t0A^uz(vuO8S=Tm$E6hgl1kr^vhTKKX>eXZ9`lqohdz?H{pPp!v%2*6 z2wwTvlFDJewg2t5?Ey74H6uyf%9dE@mZg$^dOLU9RK#;sHf^?Y@>u(eZ{91CQMBLcENzQHtwhq*eoADk*CpwtJ7+_vVH zHd@Wj2sb(4(oi3Jre@xS5Red>?9Te%r7@P4s`lb(faY+mtC7a}Y|GFhqz@D{Yxj_R zGSVin5*idpK_cZzW1AOu(@oW97xE1hX)B20*jH_%p3ow0jah2HBL55dO$*dq3z%-b zGVfaYigW!3fCK5Vv7z8fk85``pG)93kwjw*B!W7S4596pr@N?_?5;!Lw##c!4uy#J z2ENS0-Q5n_j2)WmR7x}KN<<}?TugGRUA_2l`PJurB%>?!jv6?_*XMeZUFfnKfm4UdRj%=LE=D*H{aSJF{mx&p=HEDWyAwK zapk@@VkO@dL#vimo9l~?D^)+MjGfifbLS(#Q)42w1K1 z0zDeZw%(Q~4|n?MjyZ+R>{J0?U@Z;R0o^3m4zZ8FZZT0BbA()nmBYQ-+SQV$?-Tx{^x6x|JN{*LonO6q4C&jTEFZ=*Nd z3l&;=X3bgu0~jm|@#_*n2uWZ zn2dYr{rUQ9H^ujd8)oxE5?@a;C;amK2h;V+-24YlfZaXri=($txN-nbYUCE?;Az_B zY3w>^G@8H=t;TBJ5-ZejNO;J!eSWj)!s8>0Nk$@lc@Ns3h`TxT^oSJ^ORZxvSB8|= z)wc3?w4~$IuC(=rW|oq?hR$zyv*-g9N-#cvlOMcmb5~WUx2nd0cWqFu%Q(3-;uT6* z7u)o3VI*u>vG>hw3sCgB3i=sKZJ+J9baUmE9#ucOE3bWP#@lpt;-trbea^PFt+Ov) zUfaCmP}|e@HnK@Fue18N)CNXEZi+r1P_=c4^(d6CBKmP;KK_^}Cbw0%)|t3@mh^h( ziR{;{l?jd+13iLnckgA*XoY!=ro7;f5ZAqYipJB zM3VUza65y?(zcV4Edc&r1e|n1Ud505M3ibLNs0){B9mVgh zENEFPwDlkpQ5rf=>tWGn@$VO@e#GkzK~p&k=~Q`WmMD?NcCWWdTe!R@rCzJmd0*?s z8}R~qtIFj7$FU&-f3d7WZZtPD;=)>{0CMAE`=Jl_W=e9f10NF;HT#V2XCp2{cto}h zq%KR~s@YXJXJxM?EN}L)FIrJ8CW^ShA)*$}P!kue5jg{Y&|qkCY`b3LIHtm?U%%<% zRU4$>FF~s%3$6=0xGuH?x_!*1+xynLS17#zNZ|$j55umrR(^H(s)3tnG9aF2CmF1L z@LH@>opmPdpJ=QQv)z$c8~*YEl*> z1+$e&PwDBt+Y}0#YJuVIktJ6w*V2n71^(G%)O}}p53fF{o|be*?@f2nz4BOTaqwD8 z&h11uIwGqEv0JF+9nT2`_J%>nwNTS5EzPQa4W78j%U*4swaX?ky{vdGPtn>tKB$6H z8%+jQTS87u;%9@D-K3#Ap6ZD?k%=esBhwQD*;(Iq+R2psA%acxx-DiNLL)rFC*Ywe=LpGD;E~2G|8@8||Ak?0g*ZH(8BE?3oq4zd6&#&h9AjC8{ygXg2?I zS(dtljHQF5QkdgU<4!@dg_3JW-#WybqgB_AnvV$Oq~3bPJUgSDaN; zn#wm_drXhiu4=~O&s+em&}E&+3-U=XovJjEl>HQxye>R2o-~Er$o{Cg{WsgkAHx5x z!JOvx94CcNopyw8#Q0gU|fD!*rQu-%}+~2|Z_)Cfv{hT}YNLY6GASw#SZ!Gd~_#8ifG{!vH22Fn z^FUQ^=L&S#9oe$igo+e>pLAm<^e>L=Ei^UV0*Q^HJRLzmHm(ym4$nSQd<~h7EZ8kL zqt;a4>FLJjrSYv}SES_See!Kfw;vtS%tB`Os3ju;{NB%r+ynr6St)}NWuJIYb?r|n zu?FZxe2i=Ly^2R8yY2fPeeLJouV-(*f1>I57-@W4V$+6N1(sLOeC@jLr&m?wJB1Tk zSDRp4^%48C>>Dmx89Rvb?xD&rjaqWL9p|4HEvQB777O8u%C$YB;8J=(xYEO{!tpKh zXVvZ^`%v#WbS&Dk-kiRIorF$2y}By9vfyvnaExa&S;c}IB6i8mP24S<7)vnig~AC^ zf;~AjS3y|jTmAeL-1X8MtO65I5n?UvHI_QjH&DC!vX{@CM3i0qSLxDtb3a9|h8Pu*>9^vKBEI&)&Oi1bW3q$AcLSAU#aZ}q(sD6FEp+lgjq(f_gC}s6%MVLeEU43KGZj-^6 zsh2KdRa7V3@3FMlUVjkV8%2;oKQuhBAMi&82`uhnkdKklwa6jXc~+=VENp)%r2?dboX(~MehE@Zx_bNXDoNj-38+JI)ap9_wpN3NA)kUU z@ji{h)jyE$vH#_J{f7do2{W@Vm+#pskw0Aw$=lWlk8{yb8_AOWcZCcLJnPQ~mQ8(_ zm#!7?A<=(_4;yQ8)>NYpRcRP(UUy$xeKI~y0nP*~*sW%(6j?nEkB^r3%|1XK{eYhWr~1a@=e@5D-m-TX6Q&EUP%O?`Xs z?;*dpAC|$o=}?&}>;*l}zu609Ak06J*tZH9B|Qu_9Dya4L=||K(JaAv(Z= zUKKKwe?X*eOYcKt4mC2q1OM69`o}W(Ay00m|3KzH%`L`^46YLV%iw?M2fA=XKhPZi zizJ}1^Iz$QUH~vd<^7)t>oWFXR-wX&eq<*ABp8;V*mBnyG;A!Ph`(&C|EVK}zzhD7 zn)$!jTK`lA|2;DQp>HwV|6l23=pUE<|xI8Igj7%@RCNPfZ>({ckSb; zrtLStqJ{pgMf;yPZV15eKhfmHi>D*U2LE`_ovry{sKmb80(X2yFRBg*jsKyM$#PEvVH-Tsrv9Nr;aD66T`WLz;FkH7z4qCJbqzcD9oL0WsJn*w zT>xw?CBwXJ1eLLtTXqQ4KtX$jQVxI}~$6#y^d_SY^}_deUjk)U3w z+Z9F7!g@XzlQB9V*>WM=;#_Q7_+8-<4$Dz#nJjnyz4w(yX|elKtlR6<4l*N5m=?aq zseQoM_*{JGEiL7rz_SHwHci0?JEv)|L-BJb#tT23;oHWk>YYNkktI)T(h+XMF_skr z)K$x%u~A!1PNKyoSX_qaSVh2YopKSnwiO%0mxM~zKcqOi*hN|LvGb2Yw(r=H04W5) zcS`^O-DXK3vtkO$>$a|+YAQ&XRI_|(JmV(#*JlU#%r6#JejV65{>TzCN`X6!T^>DK zthP7#^w7)O`%4`)V|6t&3Ac3cG45LUsvJHt`3iqFW^ONlIgFZbWoV6S0fWfp4mXzn z41DeWn5HDSt-HX%_AH?jW!>I)ODOr3kFcjhVr~`smB_dwzIZTCTrf|r%3hAk{vP3Q zyDhutRb&k+{HS=+4~H#1-RuunAz6S%32D&2JRwq`Ps{m!YD_|^(IAOEPFtyYMgv!< z>fm=qHN!$&^c{JJr0!K?X^DWi$hIk26&(CCuyEk3>q(0gDCtR5JxGj{NgNQrel0nT zo%irSDW7F#b56Q3uRRx+db5@fPbrxZ^z0(D_)kxV$-nLloKk)Ie5MM$u25ZKc#Al$ zsK^0!mBZVj0tOT+ZZw5jX$Q74EAEC1BaH}|F>y*z?kK`)A#eNuF#v0B)j!#1^E|5P z+0|Qb2IQMJ{tJ!j`?H-02$KObfjj}woqa3k!OsZ70QmhUg5q`QQH&}t*k zhFA+tAZ_q+UrSQsB_C~E4}ITn$5axY^PrBZKgh$F6L=gQ5RyMBt;E^c#lwITPATlORW z&prhZA`1hB)ywxQw9>_ymH9ENy1T?#GGOu+_q<8QJm0cTftwJgJOs7{4~yJrUU5bH zPT4nGx#3}^Uk``p9bteEN3+)Tgj7@Lr7DDdV0&}G-2`Tc6gjymCjfL)S8St(gL2G} zpb)|jISTRlW=&mBQvJxlE>R0&&{CML2T21YI;GDf4qi5MSVzwu0E#Y|zTFTtGI!cI zhc4mP2bPSr37(Ow6!bdti73i=DSZb>6D=uYU3UR~4av>eg;q*%0b9k6*^ib_Lq3-EsVWl7Cat zJFOJyd_Qn8!uayPUfM0KpR;zZOQ1}YDiM{4Dszae__32P+_F>%Zzqpwkl2;hX^g!) zvsk^f6ne8$J{*@F;7N8UvOmA21p*_5KDB=R=S}?HKYhbtrhsp}p0rcy8mYyF)1-ApF;B)=Rnx_<&=?UV2*{b zNvh*J#^0t#xyKN=4yI+vHSaRBi|u~=!Va6ksJ?WYz*y=oBXgwAlstho!~IF+nsq*t z#pQDi@BCgiHdBE5Re|%f`^MPmJFRG3v^&oB@=Mk7Y#q&&`=v|}OnzAm==Rh8`!^S6|ef7E&%m4Cy zU{AiJV+UP26$@rg|BXC$*&BaFWZY1DMw6#)FJFGtoHBKwl$BkH`8m!#_R)khsHc3f z>hY=vxb7~8>F__1=kBDTL@KcSE!WhQcmb*)c|3^`C{2Q}!7>RI1>2~a64s2aXdTm@ z64Ul=##ZWgd1NMXXtJX;Q5=oLg;c^__4$SwHm3SHbF585=jMDik$Fs=8(ByQS*u(L z1}QgJu>%H)*kao+D^DM;D9;Y4UOZ>KZ(JFxgg?ah%AkJGuDR2$?uB3q`$>Z*GL4Pr zuf(dIIi0)S5y_8%j%X{Clp7Zsk zs|05RY?&|zY{Y&1gEm?((V8MSbA@0%dncbiW{UFj8}qz`TmI}&p& z%Rf&9tZ1%!#)X{V2bU5EDSW<~dhv2nw0s_YZM}t?8`TCzPqtWX2DGA?gPDVs;kn;X zb4+wGm_-eotAUO3Shr5Gy#DB3JhA^^HK?{JdgSpS(=j5MRr*cGavM;QGMP!ZYX?Po zJ`#71KPwPr=PZd@q2O7>gr@iKoP@{>3w{*Z{=jw`BLSW67j>O>y97Otavvp6 zfjR!LKH`*Wi^91pf^*k!M}GEVbRA_QZE$cHOEfD@DnBynH|_y=<4T}f=Cmd}10~@J zN@DPNFmVK}BPsn&Nu`IR3D^^IQlDT$PD5Urml+xUa1O5p_=LRM)AN+LFTg+$*G>*= zHVII@#e9I~t@><%TaNY!(N(CCRl)NCbm11LtoYGJ(Nh8y2qzK{te;18qDF5x>G2EBo1=yQHyhpj+p{bJwuG2Af@n_u( zyt1S4f}3>5lbKvXD11RP-@1T~yN8&c-X}LU!rlNN!n;tGx0C_Y^i!mu&_B&YfgAXN zkPoZzCpDZv>`)Kb$-62cpOxyiQoc@o@kZ+e-2%jruncaicyBQBJ>~RKFJT#+5nUY@ z@q}8SE+wXJgN+xHY5?+i-fP7cs-~b(=Z_1`1d;mytg9yrjvnD=N9GaCn~CA(C(k2s zBJF`8)OxhBzi)sv3TG|jlOZeI!=O`WvxB`2u*yjY5{lh{3T7PvKGZ>ex^Ya#F_?BN zRKPk#kmr=|OVxj&L^P8B6Dwix4fVkaXucV-Kx#-b0!g>{nP{#;+nc%`y6nUxx}Uy{ zzyA=;CAX3CD^`q@9Ax=RPedgCSR7;>WJ6#?f|~%v225-a{|_eik2Q+g1cD+U{w$Lu zc_f`1+TZ>(=?H18E`=4&i$1avh#=KO@f&}AAKbl5ZY-B6#eC^XFv!0@JKFD}E(oVo zk&Q129z^D6djAP*x_)G*NvGbR3W%cXlo69UddHU3g@!@|B)v>y<}VyX-kJ{U;$s*& zsKjq}Mf!A1&^?#0ZYgqbb>bQ{2NjJkIkY@=jOb5ZslWrgL3d$AtL|*M45GCMUp7lK zae7ZDDnN!0mvF-F)5XcFb(AzDH6s9cKJPm@*FrKsM{{z|F8oEcLn0MPd}6nZsVv24 zXp9lUl*hHFjG$D)K&glpLr2}ObSs8k8rb#z*J6VVl-vHM5rMK<>GVc?uez6t*{qt? zp?`yj7o}E$eh=EJmDsMx0o1h=O92=ETe`skMa)*GeZD0bp7xg>6w<%}46|wnOvrl= zD;Bh)5YVi$DB||)iJ}Ow2F^t#e?ZYr9xW;T;3(08tH7c!IXt(mQV3J`w?HZ8T3m z{k~`H0_wgc8bk+MMa`KZhTDX7mIONa7MyfAju$P8cUPx=;DVI~3jF~;!a~@&$BR2Q z=+}SK%io=QOr0NM_b0rzYTubMB}&_x-3+#To$xKmfXm*36SlNG)up16CuM2lqt*31 zYxz=(a>{Lf_o`OQ&iCO>5ZstWmbcRtlwvYpU;Ia{a=N|iFbT=a@w!Pz0jXS`5B*mnR_K7%w2KhO3j3Ev2>&#D+zxt&{<}OXh zC}+S!{BES%rs5^I`iwKJBW0se>HNf}PtV~B(5V@?r2{T8cg2pr942; za04>VIep7C*oTXrd~k$9zn)>&QyvpAjNN0c@4;KH0zEnDpk#y0wuPo3w#0%>A`^c? zzfn0bSh@5;k!qLdQ))_6@NyPzvt2IxizA5u75y>isTkNfM0cX5g8dU<#MQAl$OzWm zuEClaZ-tty5`;kEu^Ke+DCiSqDd^Xb8=na8d_uTb?KU>IeyP-!#1xVSs^jY>vo(0( zQyK>p2cJ@X>a%HGY~1y$!NjW%j%!fp^IA+N9s0E;V0R2l1yDnm28O~?^x}5#u1bJ4 z{d3EF+dZk(<*M*HV6XMNN_LGBFz+D7}*0)MD`L(uEK5^W1{}9%D1rEK>m+{Mh%t*v2YXtmk?29N=Y>I zWYF>;0m6YtN&9HH{{W7Dqy;?5&BRElfxA>3?5yG zj(aw7yrKug34cA(nRFUdy4!vqOKjSo-|*sFu+0<_MUkx>bX&suBJ?uwVd>-h*qc>>{|{T!ur4!Kv#4+Dva3!3#NqK-5-TcP;scuhF?8a$A1{VXkZBJhJ-|qL2#f?_Xfb zrajKY2hRvvM>=(-u5qQk)c_Az5Lt>%zfb>vZz)8kr&;fC(}G;Ya}nJTU<88f`HLW# z*daL)P2cVg+5-_iGFTpDXWe?Qk%X)jC*mq>O-tgV;dT)NLbw6=t{Cp!@ZM>=HjV!8 z%lDhKr;_X9@J>cVO)B#W6*dbs^M8W+gcq?dGiQUY{9+)}vd~e?Vm@RZv zl&!EnBAYRnet!;rBFX|u^1b**gTY#LJ;o|YBXrC8*Ra%6S_fEu6@bY@`Xe3l#fjmf zy$+`?JP@KbX(!M+zWz+D= z1y)?Dq@<*~D$ws=PmeY^a{v}P40y1S*aM~d68*xS+-*XIkXXIeb)L{|MvN9w#`Nnfi!{TD4k@vr?px?lcl zzmH;7uz@^2MGx3EBn^!057O#KQ$mbe$Qej$$*&@{ngTgYrH5Q?qunPTdxZkIHZxD%)ISxaIq-# z2?^fqx8jOpm&hi0kX#Fa$uzw=Ffh<>LR5?;@+O#oIXCZh!YLBMo7*z`1R@Rce+PnL7973$9Wrb4lSx z%Z=FYeLQLx>xXc36ws6oe3e}2B&!ZDBnM7RbbylF|1F$i+7JN9|MYvOibi^(6}-Bh zKhG*F#K}`o&}v|h9XTG^OWR=EY1G4qq>szJXGv$>%9Qg@?mOvo=BammZ|8*sYbv$^Yf3s8 z&Tpz6Q^#bML+e2iKmuV)c7)3ztN+`-qqY3MF(Q~jBZgqCvVS34ys_7K zR@E~6hko^9D#MtL@;Yc^Su}0lZv%lp>azaUY37)Dd+I@>awiid?IYsahm)in5w>k{$%(!YbJ;a1VG$Q-JnSxyV`erpPegLLE!DQUM`@s}0 zkDMIcA#zH`t@5|?6kcgr|FILuuD{OGDTLm#uV!In2XJNAC27lu;u@gA%qLpzZAF{i zG_k(vXI(?=J#-d1PZoQiMs?ht1gOoRW5a{cc4FuO-28#f)NJu#v+ggp7G~SjV8TIa z{YVru1&pv!rTcgY*Fm93IY1kq2Nkq(kFZgR>Z$Pv7&7LB*G28|$9?a$NI27Bpw|&n zI$@fM`l-4q-{RFLY@8L0id{#Q&%@;lK#A#@8}GJOxUKgY(c3`lB{$Pp^Xi-Ng;XO5 z#@gels1R6@7@lTXIHJzrxzq?L*L+h3VoF^deTAllG1>|TUHLoYXTuRq+UUz=Ymbgd zpp<~DUbMK&F1QmK6zAr9`5r3 zGb(*O@GSp$%@bN1ZUAWd=b6*Xn@hlPQP3Zxfh;E{LhV!CqKEi~om2sQ@S46t;|DWS z=!@qPccEtAb;txadf=VT@8NO^@OOjrmKqZjq{j^=ObBDG3MOnbn6Uj;JXmBaXR=i2 zbUlQwEe>zPanZ^LZ0$tOR=@CB=DBF61D72`A_=QWf`AMW=XX~rNYzXkEP@O$WG|dk zNKcpS8a?Z%IVfLNT3G1(5(tiGQP37h($+5M)Na=fdVOA})uh9JG6KVl0d7^5l?9(Z zeq6~!ZpDl0){^_w$*LuL1iE%TgImK&;O4&b{l&*S6xXW}Udby)#g!?fQI8)NCuem) zQ`r*lfh1`-R+$IS8Z-Q9c$hkA%31f=f9jQ-6Hxa7tN_GZnw8=I@+M@?7zdD`o>FVw zkLmv=i61}vv|yz0Hm9cpkm2K?u#;4EmLH z0C5F22KmiV?4ZJkI;>Bj@7M904IC{ zvUz^exvbF2fxJ58M7>pTGG;sI?Q**GPT03VOI5tPKo1crdFYq{m{1o#abX_lYn@+bt&n8z%$~X1T=zBi3~MsY4pkW} z)&K-C01hYVGx zZ4H2vQ3&)*L1cHVrIge?FlNg zK@VDyVbGA7(3vPW5<3%y#e!e{S2r>oHWS$^AYZDGn$`cW@tkVgk^s{lAYLW?tK=}IJ2A8ATR;MfzHHYo}V zm)l7R24%b>o4Y$5vPdc77&MkRi6IdYf>g0^f+Pu_E8tRyMne>j!$?xbC$dOl zv%oCVs|m?)0#z*3*`;ViqLqqC5;6=)2%$~Lj)JOH35iBsg2tkfGjwP^uoxZ>C0Y#H zI9?=L%9XKHcs7ek=ZYhFR0@rnz|_j{mJp&rL(vkG2+7pA5E>1o3L(h&d_Ez8NJel% z95jwfrX!-rA$EAOh{d49$7zWo6W+oUGn6K^nIudgkzh(H0i8sT(_!gytxg{$G|Kdn zcnm7i5l>*EC9q_!7S7X~xpIk8oa~T?U=ow?7Kw&{#nEk0jge;Ls&w3>5G~!Lz$XFk z9%<5}8hAu5>I zs8mPTMLbrBDS|GtS+Vg9t(C?FdPI@&3^fmkpDZI-_>@R7Q6OQf6KM$oy@eHthneG9 zFt|MtXEU0(Vj6%5GFpNmi3CwPGm;UZ#YQHX#9Smvj7^NAk<1)%BpP9dbMOWt$sDI7 zAUPZjPfSpo%p5s`X0XZNO1uNFmPJbScmmQ0v-9M91&JgvP^^XsEeorRBig|b%s4e! z$~K_of+P$LAtgYWNG?pwgK9p;fj?2?#KyvnM{EZMexjA3f~rip+}hMEVzuHm|&Jr zNVFssQcBX}=mu#>T(UwTQZR%xCc&mo!YHw1k{E}?!VoBE5?3iviIGei3cwj2&DAIg zG>VBVWAiAEI9Q~E!jN&r5g0B{DdLlv8VXT{Gm*q-m9w=J9bw`#)iN_9gl5E%gvvw# zSB`Z4fMQvdIyjXVk8ng-;y5x9UVtMp`2bk)XfZQF6Ty&08iaJK18$R$%^a$Py;M6D*%3`=S}}Twn8>BE z@FpuwOam>zoORHd0%j5yFGEO~NfwD(Mlspw28te!PSi-^HFS#vL9`GO=qNiWSqVn~ z8Pz(dLPRv{Y)Fv=7O#X##B81rE;T2LfwD*nR!!IGNX!V(y+y&HM8(r-B)e2#VAw4N zd=%V3P!Qwd;235kQA!{aNpUtcgGA-ZY;+t38DUBy#PM_pJ{sd-MNx@ZIu4{hJX~cV z+YKDhV;mabJ04h^Y|=={3UyqZ5Y50?Ehq%biipxnG;wmO3MoS?wTUnT1qo#nloq>8 zhNU^MngqK=L1mkm4mqDF7ecA{5VHYi*F;%yb{HM>NX5u>3^dumGoZ8*v6=`)6L=bw zAyS)2Fp!88FfM3Bjx(v~4l7S$Bg13@BV0$~VU=nu9*Z;c1v(rm5xAQihqtoSA~PJy z6+&%t10exVhb7@{k#Tf9!VpKmszHi{$EX$HA84o+r4C0au=HxkN;)C)!Q2!RkhO~7Kn zI|mM?4BjjPZ>mJD1^^@8jJ27~Lb}oF93N^lQq74-jRB=HOT|eTB9lP3Bb-k@ zS&<^9l@o_HgPe`$NkuZAl}i=?)`MsA(ORjHu3=KJdSLz}tPu*+bEGI~l*B9+k^n~{ zQ^i^$+ayG3)rldh__$=Yi6ta!X)+uuQNjSzT?N&^RYVj|hM`DcRE8Lej-rcbI;e=q zMoSG*W__~UL1U8$~ltzGf4w6u079~1Jay)|0uRH+2cmMgUyk=iO0Q6&s29z})Wu?DT#7NUu_aOv?p5-Kr~ zqEC#cuwgVjActl;j)ue2_)?wSq{opIT#J~9b+F#jzQFl zoeUJ8h(RJFqL|dAL;#Uyn-j_yM6;M|hdMJ0*1{6X3?c**XcBLw5uKDrDFQ%9COHr? zC`KL;0tO|sSah4tBBC&q5;50GqQD8sP)4#C8>Q4D)rmL-g-+!wm{CL=AB_W?+P4pB~1Ga|7pktK;>rIW>47J?h;;FAz? zzKU!BlN%qOptcwoBqa}NLF?kJ&SxVNg~`$cFiXkFSePIr(m~c?xNxDu%7}<$%OasH zlSV_)TclE)DMGAOBC&A_30f8@B1_;LjUhzqB-dzul*SPcxIK&)!K0Efbc0!-k4MtY z@fK;4A~^vMMYAwu9+IOJ3+O0{S*0cEpi~e)2n^I7fo3IR?3`p36$|H3IaZlMsG>w@ z%mR*5Y6H9;&jNo6P82W?S)fAOHI{e|RcN=!c_AToiPB7RpaJOM?cfim4$(Sb7K2qG zLL=BjHcA);j7F9^<3?k40vmuAJk&}>e#Wb@IDM=H*17$Q| z9k37?2Mb3i5GIveV1rThNW4}lWs~$25*tHFM#NE=z2opBNn|P;4V8otvFr$DBq!Mr zVoOL&Vj80iM0uRXkc>em3F8tq0C+iUvw{NzP;yaxECMF6Np&(lMUkM>a_msF(1xKB zk(3B6+w9~#PzAys7YABvz=aCHiC!Z&+AL6&*lbrviX3_*&z9tX$%#oaJVn4^$LZo= zXc{cUDC4RUVFI|&X*;1wk2J<%3|1`9Y{n>1N#LHDq9z%nk%UCGNJ=5u49<2UezJf~ zQt_=E7B#9#OoT8qQFsH%ic=>tByfJb8K#g% zC550!N;nFsiZW6qb{Hu>MC^z_^GPr$63H@K?N+A1rcX{*qfi*AgcEOY$Y6GA1SdqO zXK7$&48S2WPb-D-2~FQOo8Hf8ZD7;rO3oe zeo~al!cCw+85$K#s0366k7B@u4h334Nl=IhG91tfYeqX$2OI9}Q>EwFr155j39c2= z^cWcpWu3BwpHG&@$7lwg(cI3g!$;nVr(I5tfO zLs`vy2SK4q#xl`3nsX+?$Z-;zMZ{CtDI&avoq(pI6Ui8@SjFM<>F`Lp+JKY@;}{~o zn1bLi*!n270%fqF@Jt&Y#k5*r46-ztX$=vWcq|?lNjF$REGAUER1`&(8znG0M}Uy= z)nt<}gl%HLEnEkfwPb=?q|lq`Xh*V{NwG#b?2#l%h&m*RNP%hSCKNB;4pmc}q)#fL z^B6jFJei3_!5xSwK9NC{S>g5wJ<^6{34yH{GHL=tw3SQHcapq7{dM%T*dMDe*>_mM&o;;9NW(#Zrk9jHF~O z#w>G46&4~Ap_Z7$IEzpwQ+@eP4!@6Q8p;;Bn)%=N=YiYX`~Fu`@Z|r`)&rJhK{kY+M*emm zK4=4E(1v+TSB`t_)q4}B^j<;+A$?0B^9)UQ@EiSHJbYX{irriWU+(V8_YZJh20*jLg!4GNy@Zjo;u@FS$N&0+ZgQkBYVJpd;fxyjyzu^AurH(Fx~+xxWLI){hsb= zKVpAZHhES^$D4HRd~q6YOIS+T6NY(%)*Sed-l$kk#~NkLhAaI zpBa(oIp=ljdG+dvK^}`vNx0{xjW4{EK0S8?q#|STy(b@V54Ia4eU)58QDUHbP-&)Dv!*SvgiR*p4%ZShE)1Agc)5+zylMCPZA*fu zY;0i9Y24E@8K)wq(sQbO7Wdzo( zAGU0YTj9n+`QU(5C}L&S&Z+9$w=1@WTCb623gdUkh_$bk8-MQ|fqxJ)Dt#Oc{v;IP zJA3Pt?i|vF;xC&_L%bh|elDv!d34lKzh>&xpX=HwC#cyoZc@fqe@>{rQ~qQ3@-GF~ zpQ4&`FLbF;fAY(AAsch_+kKElji)7C^~Ac5xogT6BnDT0)b4(4n{tZUP}X+rXsTs# z@T~Kns?Qv*+e5Aw7U#_}ZS00NOT2J_b+A2 zuISmI3R`n9mv!sVOa!WC+-1cqH^GGZ5z3dNry2#s-1g@?Wv2F72YN*9C3!ySBjJ9@ zubcfsI~tFFtNV4c{I>qwo@3Vz58hth!XWM(dtUqP)fKPux3&|{JV-TZdx=3cfw2f& z?iBgMbG!1!_EmgPAL=kXh&i@@?53XiYu=q*uz!MbCBOyI&AbVNow%@P@@_Xy+n0xR zYxb#fu6k-}c;u#jFO@F8QvFv}AxYih2`2^|&HM6jpl}hjjZ^V>4dlSxhdK1QG5vRw zQLkS{hnX%z4Yn#`Pd{pU?Cj{ffS-)nGEu z=}4OC)Gue#yamLX1(e*#w7YvoMyBr*hX-$YHTXt7NiHe*v*}cI#6eR_s-!GCK>qOX zxr1)y!9%M9*H84-T|2b>#EgnZv_8*LZW;ny%OZEkOodgfWjZfk#<-QwF6_bY&ojJ* zDY$tG((3%4BjT$k^L(ZUtBFU}AIns#4F)jRzAbt@(p4m-pd% zraeB&-^uj_Ibf7|%4)n5>VgK(fIKhi$m7u?;Dk7 z-ahu&LtBcPM{W&N^VXG+J6pHn7-MJj%vja8bP&hg%<6^59@nd+E69y|-2~lVretqk zG~Up;w@SWXGq>I4PWshB;)*Jj1Q!?=bfBu}@#=k>{^?usDYo`-mFmRYo3Cn*lqCn| zROK|P7zMFoes0SGEwa$u-H%=KuygoHj5!TlK!c#qg7Kp96O@$Ex2fPHpAA z{W1B)GD!b|fyKIw{}O}<^3vs{@xzPGIFtQ`(O|4|)Z-JKVCuxvS>U>@J9CS3?0NG6 z+B~^@V-xK!=h*E5uBW{u4`1%=>iEdrKGnC=mH!St?~!YoKz>rA!%lK`SHiz?I^UP=2Fed; zoivW}f2quG04LqDTU-7k_w;^I@{`kd7VhX(839z>zU(;mKPvmWd?G~Q_uq?m-nh8} z$UVvP@qmA&$k_(2&-74QoPAa10of6=cT)cyaI^0`T-4Y|vrlio8K7lIOz3}z*g-0= z^SYQfxlXk9=?@0=+>qq@Z?}T&pz*bd)i1N1cJ^=?jK7^x-ESXjM{;9#XUkr{iQwzm z14oYf`67FPzMjBS7tSYd-w5Hi>H5+i+SO9aKo#)$c=D|Ot zXHIq607QDI*X9Br-7Um_ceCfs^xy}y?vLBRP^W;1+Lq+u+ntenZp3b%j#a1b58Fz1 zpD8)%D!Y++?ML&jSoLb75!j^D2k4R9l~**`8K{0haxYqiU+U%{dCu}uzfGA}fO?!h z7uHpkRs}YXaR$Z191s!AiH)~Qqt{iffmXCtO)E;MJ1iba`uOeR;}wqfw=dI1vMSe& znpP^mwdGRdKPI=(FA&z;&WG=|=OL7F|7;8x6PAM6Q!;gDQlFOElJ%M4&D$-7cF~aq z*w^**4d+LVUSPbmA#B^@ulMP*Nc!hX{`6qpFRT-)GJ=f6L{Nq(>;y24J9 zooU{=sIcki*$=_hyTZedr*?jLGzJ#e??}a`*^Hf`*zO-6AD`f7S)QEI=Z5Gd)?aCZ zM*2Ouo-uxO#-GZ*Zt2}Q;SKvccExA?x~;o2f#A_>f9rY&Q2HS%5llWvbf5e4;|W5)!U?l3@^O>@cyX0v*ZGeNTA$qAJ8T4#_AL?Ag$p_Twl+*} z@`O!$b$?QGZOMH9ij(KdO)<&u_uk9h&)zjDgV5brZfbhbh1S)(54w6~_?+n<=3hV5 zbM>+Rjc6dtyVll;o|`vD%rU*XO1zC@g^eTiK}~%Dlb?JWk4S4VoGfxTvb_gN*KqhbWX0D5T zUDzn7-Lu*&E&tPj*OOa$w>Nk2PH@um1|PVgY1qGR9&;K~yLv|D$?*B^`R})wGxyoo z`5^n}29fo+TqH})e!#VTJXLkRi` zcYsC9N3ZuLXmF}^p(P!8p1#OyMcYS@?mYhFM1SR+5%`SY1`HFXJQcq9SJx#glh&g1 z%GOSC-E>{uaBn*PV5qVte{;hmzWcuS7rTP4PXe>}l`B-9)83nTs6GZTg&iYHaEpM+ z=a)Pi8Nacrs~T}<=n>_nUGE!a&)8GgG61lr%zE`}wtu`mjdN8sZBE0ewEGu4H)NNk zs{PZ<3!Y*E<_i6a&%V7ALnzjcTe<0o@5%lL)$9rEA7XH9!cVy`z4k@iTlZVk%1G{5 z(bySHy4^LsK;IWQe1cDH$=bstc^dn~hn>7?(zFfvqrz%BZYY8_y*2Sn{IK9LBu`qz z9#M#1K@s5sxj21ncTVvBtk5><$xrMsz-R(yAk^Dyt{X&iA04CZqJ2|I^UDvG(0U%~ zV=U(;b36dDz3c-ypAkNUetEgV2~&_kujc_A9k|A(y-aey4%3Ks?@eHwNZ-0>nxf#E zjJ8|SFLYlz&t0TFXhimk>de9#K`3WXJaa75kAiy_vOm!ES^3xL9>slJvPkkk*F#Vo zW`Hw}jXogFLNfOI@VR5#{w~LFI%#XzgvY(Kd_$$d3Fsl^z`NFh>wjUJ*@rP!KhlRi zwKq4{gZ92tNs2y>z3GZixl{jQe^+qumEfZ7^PK|yTTn=ubLIA-V#b^hu)uasp&*yo z<)!;DSe^6A$C(Y6RfC8^_%{veRbz-tR#4HI`Ws|tu2|ZyiUL4BC9I|V8e?p0`4E*8 z!I^_V@bmeF6GHx`*#5rFONY34Z8rU{sGe#1KCj<9JDk_&C(z>c9wSKL@9-JTPC@5w z>z4~}m@;oC>u(0c-NfDvbJ>@E>duB4&fk~%yTp>@QSyiPd_yyOrC==`aryY7(!SqL z6f*|yK0$X1pR{A?EL{1#hJ;^YYW?4#I-M-RJGMFGK7hHG_^|n}2ZT4T@|wOuSy}7d zU5YAVP0X%c9p~LsA2h4%6S~BEuG5F7>?8kAW)Id@6^EvM`&PAHxft)~6Gr>#9^t*} z^_j+ojn`HOyE$1Gx3QdDR`vl$8sQESQ?NuBte@ zkMRnU5PAdm#U?;f*=I-Tw=FMynz`3!`sqJclSg~Q@nd#2M*n&T0}KRuE(rf$;USs| z)>Nm%qbK3ViU;5NAK@=Qmu=<(gYR>l;RJ;L*xS6Lw3x9r`a`b`&E=-XeK^m})& zu{n_-=xm)Si<OCaS_!343p*((?COSeOk zGoyPAK4Ibp0^e!IYL}GTOicH^NN(N(ND8Df(M4C~DAr~6y2@uE7|UhORKWw_K*60o zCvNpxXyTK?O!HSD6Zbi-F{{Wsb6smeAVV2fqQpr zd_wD45Ic7>%I=(4;55|3X3pckXfm%7_@S?zRlE--NB)0}vfg{HXe`r!$#QZ&0BW8f zRwK@wD7_>&Qd}La^}haVNW!>&c7v=j{72NU)`qc|uDg8)TOt}V7fiwvMyFkT+OADm9OL~vzMJ;5tLAOn zCjA|m{PV=}U#qfSzuI-~3B8Fj+WYMc*CoeVE`C8bAHDqjv%?RLZQZ`L<=w8x@U%PA z-(T9c>Y{Irkw0;heR%HgXKh{_5XfDyj+A^HBSm@SqW$G^4(b>j8^h4sGpxG-ye{lp{vY3Quds+eN>BC1ETZd;UC?oVPR5Pg`LvBcUDE!K z@8#&cVjs+>tFZFl8#|Ml(;6pwbY^H{{luE4l4%!5qxN#+=Pz7SGAr~a@6!nAgZ(qZ za@yP5iqe0a#Wq&TZ`~fBzGYk8ec#m+F_YUAF>dp{9aGl1Pr|&a>FTz9AHH^a{>Roe zMPt{F{`N}y^~a%Et=G@Dgfy*hJkzi7(~}9LwPj15+-S38Xd-?hM(UWQ!hu!qcu_Vo(axq}tZC0L1V?8@~#E&zs zeH@%!i{FpEcX8K~W79mcHx5<}@#4Gx{;WpFb<34QyZQZ}-r4oT?dIp1vA^%(F8Q|7 zJL)`>{WQqbsD?+L4?CxY3>#b*nyNVc=GB$Z{slpqP4^G!vvou8BRYcXJ0*FuTaXJD zeVvi}c#&u_y!FTK5sCVr57z$)ZCl%YBjte4W%e)3`==xQ8sDdE+&b`;taED~Jory$ z`}x~5UQTV^b^*=55&f(69VK?T(PioL>$_IYY z&~I%hr0PoR#e}D`$M~+9Pbkhl6k11p^TxV$^^#g zQ{Igujq<*~tA1)9LLs*zwnsiQyGV4w{#CkjIj0rjuW386r}#ikefsZ%l|LT}m#mr< z^P>cZD^8tUG7OpOGfShxsV=y0c(->QlV7=Qx$#ErE;sR$;#un-exH51`qXS+*T1>{ z?3b=Zy%-V*rr^5SwaujmbKv)#Gc&uI$uiPrnk%1=LX{4t5$q#o0f2; z|JGmJoIYLQcVd6*d4F8mun|oU| zgvCGn>q=~@zsDkpj{0OYF0j3c9sYaq$NF!fir7^9%xjwb=qoeBI(%I>rJV{oyQh?g zxThcOxTj^t9{lzFQ!GinbIKxhqu07qKFA>h_l`(&d*0KvVe2saWM1q5tHeEddN2X^ zu6_L`eQ*5!i=vhLg!_sa_s8L*5rNzTkJf+ASRnPvfEBSl(|*)E+_v@C5OT)EwaU|< zukK3u*t5a2`q|N6=rHJ;B}Dx615s?=$(!k84sI^FKelGgv55g1FW;Pld;Nmf1x|UW z#;GX(=$9#u`Xc!u0dMZ7yH7cv!;RHe?Tf~p+U$e$Pg4l$QFtmYx6yWc%U?2i*$>p6 zwuD#3G3EUVAnb$i;8C9Ju=eu$cWFPCj-NV)VBZ6b?1wMD@a|$0*SgD=lG7d3=U)2R z$M1KZJ;LUb_YRAmTB{pYFm^7yD6;j4Z_e);oACm0joY;?=YEa(?sx6!7E<@|x~2mL z0FmSH%vp~{2isrQ#+*JhOKu%r-Lu+q?DzV{XHzpG*H`7X5Zj{&W7he^R}~*Uw63+c zI+5zEeAIov?SGV4Rg5p?k0^UV;&tF3Q1^DNIXv~@YN4{Y|IT-QQ`$T{N>-_dTqs|& za6v#7`tzUbsRz@qkD8PHdmG7BUZboKlGhhQTY4w!N%?1jDpLDM8?pFz^9k1ab;nd( zuM<_pXP(!d8(y>PhA`<-aFP9C?O}NGi%VC0h0+>NL)rZ68}?SI0+(~vU9j40KLWci z2N#tjaDL70rHsxB25Dl8B}y0^+_Z^btgl$Jwy>YHxEg=r*0t4+A(!vw=6*{%S+@|u z%5*Xpf;~v@-?C>VQ66gO@TkOrA(~8tJoIEgH@$-25R*1b%l1JqV z$c6qdVpDC`?at!S(te9UwP?eMODpH?T~}2O32evy<&atbp($k7xe0UbspnmROc?Yc z3bMa1#;sBY2^;ugEo=OZ-@OH>D!>d5eT-frEAr{MQM>4tvxmDC^WnA^PP)igGXz_I zEek~-yJO%B4TSIV%i_zDPIZD9hX8UN%9>n3I6|)~)=CGLzz@v#t^c{d?AFC+?SJ#y z8p>y1Tvofc zcxeBZhURoOZp`U_91V{(2G&k%j0l&zdd;REmY)ZI!YnU$`O?z%P28Wly=cQty*ECx zqUm;#&$+(m&!3MfU5Jw$zR<5=%-ttlO|tpxPd2Te!}43{7q$Q7gWkdqphIf_$-9}C zoj32@a!w&L_AKwih=s=Xmh#l5GdH0=GwC3i;No1{1%n|CTm zoAle>I`mw}t3&tRK4B;Pc)ua__C~+7vx(BDUt=$P`12}!=aR8Eb|6C<4$UyQNM}DL zF}gg`6q9Gn44!&n0_t0-YE!Z$YxcAD+o8=Ht_)8*|MHw}>C78Dt=B8Qe7N;z;<293 z&n}MZC^7zsjqDk_FE*L%IU;9n%j>R{u@y$+1B!SrJNi#KRO5SgOJp&drzm`e_s0Bb5Zz`x8eCQvj%1+AucR_cAjt~U>o7- zq@01?$&mc$#=4px-7=AFg)3e?$^Wa-)+Al&Cs)sne_7U)&c127wq3^x+-j=Nf$i1 z=$_+SCE~5Uc)`dS_6O*xk4Gt$akAwN#12yW(W%Z^=qg)-pU z*)BR7y0z)i)X(o1Q(mLr$M(AdSkVgC@YXwC`899XI4+zcy8k)oqnq0OxH=2ukN{Vv zH@U7FglZdwb#>rA#+s|gQ&<3KscBPdmW=qczBa}DMY7S7Q(%Vjli$p~cdGO3BTL$u z)YKN6cCY{&^I-WLTk6@adopg`-Tp68keI&Ei;w1J_L>G1?!mSObo{JIJ+qhjtgo89 z*R3JE?$7$bq`IrH70c&Mg14?&73A7^DK_)O0~%B6`|6rkTAvzE$z@U{mHt`4Rs~IV{g~B zJh?Dn{n@bSUr{n#*9}P6kQZ+4XEW>no{*?)0WQXlT;SR-$hFm4wr3!Dlyc*`z~y&Y z2*HRrifd~VW0^AC^Tr_j+U2?4%tZp1_j81TpLU09XMn5TMQ;9OZ+v^n%AFIyxgNJK zxnacRfu9#o_*46*V|Ol+w4E5wj4k#Cn|^z+$CJOFA0f(6JmR{4=$uSiwzF%=lJvFv zv#xyql;o*oY+Hk>`!fWPfDoc_?~Xsg*RSrk{CVx|e_`V({eJZ>MEjEK`yef$Ti1Zy zfo*YLm2+Yyc(yBo5AKWw0c@U8i;;JfKqkNbwPw(yp*PO&?63UE1?!&-k3%F7Mg=)YhDq za#hjJsjV9M6VbPT=!!hU&e}mU>k2(n-+%bf#_b4r7+e&={1)_d*o&H0bMueC{PI7$ zAMR%E*y#EDNPfteM>?doHeJ1y} zGjPQuX?#fx$kz++BJq}Cpir{+*`vB=<4dO6*1AV{p82G8U1^(q&MRfe@o&6?2O~V~ z&%zU~p1hckjISvHrO4)sM7PLif9h7WX!kM~f#*MMtaaCz#(;mK7(I?RU(S42K$#R; zKIPHvnSmd=t!KREf(@p`tW0lsW%a(ypGyZgi#QuVM()4k!y>Bhkjr~CD@qQgNBDYV zuvEx-D_4KX2`)k}c{%59Q}sal*5IQ2OvTap9pY#~^U7O<;#nXQ#O|7L>RLiio+!1a zot+ACd)nEx;qqLXko@M?osx4t>oDu0vWuUCRqZz zZT>d7G{yr`el2;a;nHjmhGk2EcV@p87Z!OrJB`r;zg_u^8f8%l8X|Lk7 z>)k&bTb(sKe$6}Np>!`W*%zz_i9dCT?e)E||J{{y)|$bQ+L5l{-+$WYX_A-siW@ld zi*W4;)%WXb{lIqK$Q_)t!sWV(GK$nvicv$G-clj)tkN+bbNl2<;XlXTz{r3d-;ZIUp^T1#&i%M z*T81m^ey1bagopTV^cw@4w`)VKXs6TsHRtxed)6YT_(6}@B89b?esBIwQ%`*9;D*( zHs8MmrgcsNGqgFP^K#BTbH#(+ip75wu1IqHQF!pp1$^@AzEOQ%h#~7C@BXm+K!;VH z`l1;4w;1!vS^TM7`hPlJ9k{BHKWa}Ej183F53Tge}NeS<6u*c#2B~wNq zyq`#3;~ogzBN~JIC_{#xfmAkxrpJx%r6P7fMdaTX4*a;^=Od(JLWR}gy4R=ro(mTF z#>rwPR5m|N1Lci<>7r=&gWuowt!Ke<+v61$@bF%+Z5yTeZo8fYiZ`nk#hmYDZ*NKA zyX*@*P85R6t&D}Zl>n{$=>K>3)ds57Q3Gc+hm!|i{*~udTX>5A zw)mug~g zm@#(P<2faIi*rz2P1#L4p_5=&V2_!=Cc`B$|fi17F=M!cd!6^alprZqfE6YUC z_s!VcswurCzEBX8x0((uoW9p*==D*oRpn0hJGqAZ&V zs+urv;j*n`e-77N8oPE()$_Dp&f@euy>p3B+xe-lvZ{Dah5PMZ4-UO#xmb4T+G@`< z=?~7~zg~Fh(ok1u?^kegN{efqOYU7&{-P0EF10wPN97z)dH=j-+GCgXRXOz~z115j z_UYf}L1FA4@7BTi{Vaz^FWcV%oc3t$M`em<+TwDr`v=$Of=O`|bzqLkS$ET%TRHJv z??i|kJEQvO$KJwfmGkI?FHuj1mK}EcIZ&zKfPlo7px^S3xr5&Pehh>%Y+70XM@_}Ju+1ZJ>dhV6S zKYW&rU-WsN!7%IJ0zKF$56h=`;(QLd>2XnM8Li*G{?R9U+-a>}8QydC=M#N!efx>- zd$KCkqy7a6b4*i~9?PgId}{ZrpSEHZ2yRar(3#pW>isR0{G{KNqt(y_{iN~%vDwSw zzZL|!PJ{od8tUI$IxIQvGhz6`K}#1ywz*923msTVgghAkeosX(q5$LWtOnc!t0bsD zbkd{|H^Gz-e){?n^S9v5rT~1@H&|p%;)kr0VFFxKNVw>c;gt9N?(F4v&lIPR9hMVV zaAay|S$3G@6n(|F-UVd>h&c;Ed}tMX(B1>Rr3LfJcTbT~IdjI6wjQuN2ITqv-b)=I+eJPi`1MAzyI5N`;*Tk@TJ` z?e8qML(GrP-joRTMKm>EQ9I7l8aF>k?3gcIUOIH-)v~JMbY|U%+sFZDJt{*Xi>jbW z4ZWu-E;x^F_|1Q54|2_eORm3XO1b2tpcMNp|3=O|IKw>>R~+}{G1zu>Jc0r&=4l0b=}$ zsyz@e`0{K=g`aG1Y2beU_ooXM1Lse{6&D)Jn;tK5`|dlv`TR1c(O!dXDroqYGWFV^ zB2Zs%ir-%vSX7q_AJ>Ee+lkGBeUF?d;yxX8yD)m>5vj9SGUn4q#;Nq>rRgysw{YBN zLco&h1oZCXjkaOF{%Ol*Z&ZMoN+W@er!UF5JZ?nV+_`i2m&9I-`BrlI;Q>Cnv$Ipb zaY=jgYxfT+?>>C^FzN}$;MlRHXK~r$p6&;8Q^W$UdvfK~DZOjgWZ=_7&HT)pvz8q; z6!%E3)ShtP{rULa!Ocp(fhr_#}2g|ZPG3YvbVuX^#zeqBZhrtRtMJ)g}4v5L?!A69-S&~?4a_a4#E1o6nh6hBqDC+zv%IFFzb`uHe{SvfI)O$Rr3ja16j9b_wRu(5 zCFdR(z|^wlrJ?B@_qQ#wx`7*0N)~Eo_WRWLh3rqr==ldzGF|bGpnzFv9lrB_&+^{7 zmEap=G0D=~54heK3)!>q*3RHL=XW1FbGv`f%#Q<0$5z5276_vs^jZF){=-&2>vbgt zDAoRTW$4EVPm=mB#`*UU7u$tJj-I@4ir=3ek~>%zOBEr*@9(;`V)eO|Tkbd@AJMIE z&$^y4&{iDtPkX!V=Vr*;fY3cR3DvV9BO&ihQ;`=iof8n<%1KARO;ivE9npolPU)N# z8q*(o<|`$0^iTPD%AD+#Dd*;#991{a!~0ZejjrFj3f;K{S(zj0Z3xE~*4gH(odK_+ zKjGSV^xK;jEnCxl*t`8g(RaTyOKg3|Jh-{Lyu#GDWzh!59oT36{i9E1HL|kvdw!np zn34Js`l%^g_0+BD_K1r3YJbSh#AS|I$hx!FGX7yKdD!YjTxwvBnJ>!pozQq2rK1e8 z%`I45w+b58(7Ovf5fHIlH~SC2N%GK-@2;#0y_%k|=FsC|FO@?MJior;^7Nu%BVuoj z@55iYIYmA9eiA0R);+3wL)pbL@@22IS>p_(UB?<=LAlx)lh4z=;jfonPucyzH2qZJ zhPK`C1Jc;-D$n8HFnipv?98oS*Nu6$Fci5qb!K*d?9t)xwLhv~eK>vp_Lca~_pi~H z?fz*Nmj(CB6B|VToNq~n(E0h=!T#qzQwN`4gUlG(nt5jR`}3@R1y2VCoK>!qoS|qv zAF^-{%QVSw6|~ z%AZMmvjQ>~gaxHP%1`iZuG*X~_fSetW+D=GJq0i{)X| z$urhuwXi2GeVtK$p@5wXrT%OOhrT^f`m!e|?{TD|hu)p&udZluf0BwRQ@WB!VRh;B zY2(!YBqzjV?>pR6+u2pV6{-%#eyU3vfh1L)|M~1QIWZAU&YZAz$DG8%ix<7K=6=hC zpPyKH1`?xn!KzDk{kYGH+4s+RN#Tla+B%Q?u_KdXxBT3ySn=-1INC!SScLy9A3G6w zCwoatYD3CD`)7S6R##2^^ycYc?T+melw?HHb;YYg-C=3)(MX|ygndA2N#6RZ9L(ue zATd=d@wu0)Sv?OYP41*zD1CE@@VqTzB?26jKGxZ+_P4Yt>W^#yM^jrjJ~6qe->%qr z=Iu2v>$mXAViazq9dtMQ+a~3nApNe; zPo32KA0E?MyDvR~Ft8tri%PBUd5j&iZQfhJLAx7I&se@bwcPpVGtqPBvpz`@aE89K zdP>d5{v&L`SF2qM9Dm?!)CI3)%SJvKi!vxCwLyuR>j?~*I9*L zX|$I2n=X7#n%JA6OF-&;+w=*88x#2$@t%G_mZgL)LeT%?;d{|R%ojhVq+e-bq5r~zfZ{+*7HQ&Zvo{Sq)m*q+lU#SCS z_ak~(-Q3_ob4tjqn{Cju1IUx8@zXtOa*Fr=7|>uFR=~!owpM-?bW6fclq@cJx8h*> zg4(C2)_HWVK|abl`*Tgx{gY*w^@mrlf70GM^|+$+=+RjraMd=y?8`eRosr}Hl$6s@ z)Dh~IbBE#lcS1}9NnYI%8Pw)CYx16(kxwPc6KirkJK(3cQ6r*H56RlQ*1akfeWW;j z;hK?+G}(z`La}>Q)XI`0cXB?@d*c@xT~}NY8xXq5S$*yUb{=xG5U*<6^0dU09y@-l z)JKlgi{wa}d&DBs<_{Zdr{}wks3B%Mwu>_aKQ~(5+*t;DL}&_rx%UA41K)ksMi6{D z5PTjJAF>Yx796GWdx}S+<3@{V023hw4(uC0&n-lM`DWm#YI?&0(o#W{DCd;Yo6RX@c?y^F4> z$zS>AR=^Su5rWWoa`tYS8zvO zY`1czWn3=_h~E=5`t?e*$J*chhj{Hi9eHx`*{lf_MrqF6?Nc%^(M6%b);@>ZCY)WZ z@&>6s$jQp@>yZuqF6#zo84)2R_bwd`sg3> z_s?VPy!U8!X7R!k;0(Jpcr`d!aMuBrXFI;T%y3-~NXwP4HzLR3?;Z(0ctC%ubm5+$ zp^cC6F^4zqO7-*2zp)i6?J5~60;B#{r2szeV;+RGQdv1^+y30|u4vhV@BzD@%zSsi z?_=YZyTj`iEBY;Xa3u)noHQI95v>GA0r3}89w#>sf~Jr4Fqy{g5A|)jy(kVzvmy~qZw+`O-b3NDhzOO(0q37(g_F8+-HRqUPj0J{T z<6eIl_{s=-N?r5Bkw;uHW9q}sBd3|z>h;i_L7R& zC<3@`-bhv?Ux+Lxw*87_v~S}0{Z2!cyk}H{@?BR4WRQcsX1))LKL(iUClu#@rb4A2 zTj4_@=z8Wac<9t%(>p(adg3xa^R=tF$~qDkeZ<=SgP>$-XUN_zgahUMX>UY~PfOpo zs@^vm*Di?(4@&Xo!X9wat)K;O+m_%hWEPQT-0>y%5YcX(UbwK^x9<@y37wl2%H;GJ zU8>+(TKs9t6p&LyyY~1PJ=5+$MqGZu4W{xsBkmbK#Pw#>a-U` z5K9$Z@o`2eW|b#+wxtc6#$8Nw(qbO6lYBcp=^9@945yS{8YBtsrs)K{6z1|rOKK_2 zyQq6F=(UP98B)Pkg`4~*Is8dIZm+1$?&;4sU+Qi@^=8hMIwqvFB`trBE*lH&t}k_M zW%J>lML#qxC5y4Q`CfXl*ak3tSpBhl7dChhDXDwmxfxz+ZN6>7Yoj+B>=pA28IIi- zW2yNr)-n|wN>z5^cfXY%(jSH=+DYv5@SLx!daHz|;B$>| zXK5Do*H8_52CHfPpLJ2ue7Z4y3d^qS42f3Qu3NqRhceMUSZsJt@gFDpq{v{d%08{) zhd&YDxojZY8TDrvmHL}|ApFJp>+uVf*eYH&9T3q?9Y$O=U`6{+;t(qasOdlz$kl@J z9Fs-(Slg$*lJ<8$I)*AvqD^))(RgtddmUdEXfWWL0Cw(pTyDr&y|JRZe2 zE=3`_o!{uJd|m0rKHf`RT@;UDl2$iW<_ X)X^Pm2qD>BLr)aWEx3TrY@P5%)_dw zSU=G-PUhiCdMM`fnYMbW14HVYX+~GN`z#?GpFR0e%+^P2Ow9J}Dz3k1N!5v`rz#$O zo_89CmuaA;@V_5U0Cy#NyhD|Qs$T`oJvXBu8L6Tcsw5OpAg7J4Qm2QzE<0IC8|!Eb z6&iBB{Sln|;j<-~GG%NH&@G|y!c*0sYoZ6dw2KzI9{*UZ^8319<&cod5H0szBIo*+ zvc&moW4H!tZqaxK%as`F^OUma(j5N0>5}?>e?%okqX6RDdHoM!{jPwiB@FEoFvCW>Y8IiF{Zk4z*6yUj4K-y-R?hWY0`pV6PGNN~@^@f1lxq$-IZ zw{&xgmk>bqrRnKpE6CqyjBiSqBfel4E%S&yGO--9oT2s+FQH-TWAdK`{RfWE zpsoB$-SzQR=971qSBS{qeIV06lX#Hz2K@2Td2HkRt{(Kv< zCx9-kj$ZMF)*xVH(tlYj%)|2^b2$*;CGpyCP%H5kzS{~sqoq`x4_S#hsvUOTFsyMe z-~{P6`mg3kn(g%v4%=-%`?y^)NaFnlz8|fH4{DukEvN>QM+o;2;vI<^5UcfOS}06z z0KkzhKAQKmU^kL3%US%$s97ZG=^-O@;LrP$tcOZ<)PXpVqHe`q|I@~i+1Z!`difXY zgIRxece~8vM2mK2?=KibJ~Lvgea$i$n-u;^nztQ-m}w;tSt=M}^WL?=z+kz`yB(l@ zsbq#QreWtFb!}llXBte0yqaH<|0xgtHG)*+f7ce6|3_^h1AB(EfC*`gZ0q@lyZjH@ zEV`Cv?te9=tuAZ2sr^B1c5GtetK!Y<=&1WNLV=`ZVi~|;PEJZ1pNCaL>YQ>l^XnJU z`KR8)Dy5s!@7Eo_OifHoyh!{K`r74FG(SCJm9^-wI-ffeg3P|5vt`dMi13%}SOAg` zZou&G*lqb=B?E)skuSs?Ng+8Nh!zmh6iBXUKLle11qC5_*_yVf>+7dN+b?jMkm-xS z!z**)NZoSmjSu%yhY_W@F6o9WzDAPaB>Xg_ZZTkHYHI3WF)U_47XpB=19L-d&zCAb z={&uTOa~`CGYFOgDuCCN>b^`$g7WLQ8T?_!H~5N6t=6~YdQqO)oDVR+Pai&5k0S1R z$x2sTlV8)2_v}x8ToKjsei!v;drT%Iv*VGm6;B!j?(H&zF^Gh|N@_p%UoHSL3JOZY zc*o+lTTXk}=)px?GuwyVKL8dapdy;*QTi#C+Mmvk)tAbtE^2E%lpRCFrq{iH$S}*d z*0K1LBk*14uAV^bd{P5F)g1y##Lx=;*R(?bYsaEpzSZi_7lTW01vb8FVYQz_$}6pU zh#{j82jw);5v;DeeEPM^lyhIi!!8bEQp*p$iXQ2uk!{wW_~9?+nIqfZdU_$UydQ{1 z_jj9EXjif(rWEyI@9PXg;%1CX+Z=!L-Wm&*e7d?7b7HrIC5~BDnY1`)?nm0_GM@A! zck!fmpYCAe_aJPbyi!@P0&TU0`*~iM`0p#2*FV0WKiSU!91}4>Ho|CkjbWbqfng+Z z_Eeg=w){NNJP(8q7x-{c9Qr`Qx$J8to8tVS zeO^ezc8F(;IS_1uNbzOkXi>c&$9GlJHb@?^Cxq8di0F}l&bcKtF|j1Xio$NS;vaMDr@bOHm0Ho*NKP*w+cB6WJTgE3`SO16XIbpti`I#c0yQ@M$B?&3 z3!O)+O*nTW9RimYMJa!As6qyq+sjQrKb$|o40ZgR!eyyL4}X#j`lvg!$`^kaA#z>) z3)YbENu#HOQ>u3ZqrnEd!1J?Q2R*>go)VGssVDvfcQ;$>e^h=#`pq#{Jx9z}F&=nn9y}KA%GH9cL6cO8q=D4o zh0yv`UyR@!DgWycS(4Q$hpCwZMrdEF>`z|f9Haj36%5E<=WwOj&^+wrJ4~KoAtZ1s z=2sNALp25JfeM*Poy+sb_hu_9zkVe-2+KLZd%xSeUR7$)WEM1L-?P39xYg7s*R@Nm z&;gu3KibJ3B*>kA{XLv?B6B4Cp!QCd5d`oX><&Cj9(q8Cr-QOo^se|#18au(kig&b z2ZV5{vS=$Zy$`XD@~~2<2zKjB$l-%6d={wis_Ka{z}m49Si7+%m9#Iky)wLA_-`GG zb=E6AuS{8Gu(VI)B=Ua}!t3i>bzzZ69}H~s=&%_bkH!dz)ZNJ;>) z(!aRPXpbE788V5nFpngHu@EBhsfY%9jGHeCWLVh9jD&ElbGh-+o*a!bKT9I`?=-RK zz92pD+Mud|?4P-PHyC0O_dbb0AP$E@x?-)Ts1aA-MuKAg?RpPX^7)&yH3u73Ucbvz0-k}af1JuMUC>_A6j)jx5jpQ5co71blr(n-xr`(Lfgf6H#Zx+fqr%zeaUavhuW$?E$DDaoceON zB|tIElnODPm>T#X_T$vHKx9~^EdL85wvT5NdlqL?C042>^A;3~vQp^GW&B|b;Xm0PCY$&}Z0TP+W) zZl`v}Wv6ChDL0JjXO$jO8qPv0S-S1I262jQ7!^WCSu6sU=zlnvT444n1s8oG>nM}D z=ZZZ@cd;5dNbk*_C#3S3WBcya;I7&J(vLLZ*BsC9Nttc@EIjMC7USKUw*UMJ{1JV^ z#l=P0nva$yFKGa=@Q?(n@C@-dSqAQ;^RSvnMZtQ9>bd>MMAXT_ThfGY^AM^|F5ocf z=yUp4^lT+by%U6maFEuFmvK9Y2nQh~s3ayY@u+!!^~IGI1|6kI}RQ*Zb#yQjONIGahei&WYKoA+bdr2RzsydDT=AWUb1bWnX~XTYwY z%3pq;dW9AGW4jKzs6_m3qe6ZP&|D>8s7t7eGbc07ExRoz)AsVlo{J%M8$}s!xRT|n zAMW+2`{SFQ_v$lfogIGjIql7k7kBa-7n7%W#L^BaF#bpg^Y$@!?#YKlqwblaQ@+%g zpbewIXK6LP-qiOz{-wX4F@JSWp2~|MR_wy2X_<1j)wPQ{SPxN$<=puuJ^*kv00v=EC@qFxAERV)=!aMqQH>6=mPVR z9FX=r=|8Q>L;R7!cH@CONuJ7c(Pw&tKO}l;TphNQq%IcGMIAr^B%!gG^D60l7`x!z z`Qk9eqfj%p_&sd7>2NUWnZTL1P3g4^l%loPafZu!Fw;gYnp}{hQqNz&ak~0XNKgSO zlOHkrQ}`G2E;*vusGFCSyLnx`H`DYBTXUcj%_PaplCO^vU;5i>Kdm6=>$-3DUF=|_ zH^XXgN%(5*9B?^vdNb2?#C8jml*R z4_JOZZ(oqs>rZSO`Llq7waR*k%6DCOzHMFn*2`h467BXp=a%f%IS46a0mu3QAt?8S zAb{vPLhYntnsaWCB}Ehr^0_XS4+@JdE9y+LhD?{`L)*oj3Vs+;dStIaB2cn&xo2#S zliL*W6x|HvYj$xbf0rf*#dQQ+Xp6!S5k}Qnul2061$0^7J#9JU*R!uk#Ex=tOE$Xr z=zP{8X~+wYAg7Qz^470NSHsVvOMs)$bs?u8M$XOA2U<;;L;8{eXP=pwylxm;ZZL_G zZzKy8S9rUEd74R6k|-D<4cGhpEQV3}tqh*!RIvNy$<7EjU9ZK!rs(N!g6sOt(z1K> zkq5{x=9;_=>b2h9Lpq%g2pB7>X6h2Xy*eBje+<&;vxC$Y3QBtWr%Wnofv>j4apc$v z%5#2DGNCI3e8^_~ffdw~#3a8?wa4z2P5I^13yf}*uOevzVl7w_#G~s6 zAUX}BhrQjoUKYPGum0815kbHW_L)C%GH9nDru;DmSpqxGG7=@@dzQRJxkuD7?IoI@ zP;(>2&W-ZJs;mYc;M@9n#9OomlJ*od+6hdLZ6&iVu+>`904pON$SsYn&VfKyjvXw%Th7!p z2Yc-^l5y%pG1w6G{4Tt0?0#_u%o`%JLOC0^EB{yqk%bWSJ;ji!DvoA{GRBNug-<4v z;8|hXXXu`;enOH?oI*<&~}OE(^Td zCPM--rKYO>T1*KUm+BmhW(JCE_cVJSgm7+UEQvLG?DSGF#=E~{Pbp}x+00MLDNy<- z!vhh4zV|AO)r%o(8A{}tE;R%b_UU4ES6I1~gVkJq+o8>>=uP_VTg^YWj8Zm5cIouh zGxm&7#!}#Z(+N6eUGMxdwy1@bv%ZliwoZJDVaZo}qaj$7`oiu+evYPRTB_>8CP6y{ zF}`?-E{t%RWWV)i4+I36s zq`1@Cx$<%jYFSe?lsyDnqN0bFYGQ4MoppJdbLb8@S0aE(GMhp&3J&H@TW+sw@0Hnm zAe;}FkH;QOn(1FB#r9+RSrtm~&h~yBp4j}uAA|jw#?$vkEnd595>u}ajj_Pav(9e8E&RJehpOk-X=`58Pz|FYiBlvT)DO7 zwjPft1Ion$GiE!FXM_MqWRhb8q3vXlo1rf88&}`sGE+x;$OTu?uL~WEo_SZ@+oPCo zSI_dD9^i1zcV}n@*J&L!{O#JFP#OK{h_e!N4eT=VxQlA!1qbxF4j|0rpX{F)YUxUo zg_+oD^sUXDq%9|D9kV`5$(b_ML-$)E$Z4HuKH-17!wLD=>+S!0ALJv!bTf#Gi0409 zG1>1$I_RsGHjEggUVpAimC%16M$kPZJa`V;-s@CT|Hn5sySL5kT&&ODBTvwEX($$!Z`fmAw5*h?z%;F&jaXxhfVS_)#acb4$io;K;d(LhUOA0sS^ z5-p@W1|079A3=5c&6A^yruu6J z49Lhi8I7zmNU)}u5m{|=d?08Cu=FRE-b^J3#=MX4cBCrAndXg0uH>sa!s!P4=*ORb{{-#x+_7@mgGHbrs| zb+40_SQ?5337-NycraU3nAN~skE$9ix3%^%3&a_a>J?oc$l5~(SmBB%z!cx678p1v zkt?1!%r%i+Utb5O5cO4r!ay1h_m&;y06$6t1wWd&)5e-#Vgjrz9uVPz2`lDB;?$K> z1^wt_&W-h@XaEMY9&PsqyT0KnNiU8PT|>3iwbUuYwG-D`)JFX%hp}TG@o})0HU1sWQ|426eBNC1B2_iGCZm zQs2~y;+~p3QUh5!q1HL9!_7sP|Dp&+=Z3c+1}D2t@t|&x9_XZ+`+Fg2dIvzX!qBidHrF8n)e%2U)UZaLRF}z z3A=D%|A!X$;=v?IMa_|f45`Y+t`>3io#MT46{sHRe|m~`-aZjF2D>~%lvD15kL6W- zV67~nF_%c5u)A*b^W;l6PrGSA;*f0cX;_moxLBGOsMy5~~KEux)C=0crN>#V=vCj?n_eq&#RQ zrC*$pw2;@qib6q;SRtYdV)VUO$f1U*WU|07&1kXvTJuzjh}*8HD!t@m6bWZ61rd`> z*}7ybbL?I9IidpE@{bXKRUoPs45TJubI=7&M*hvB3@0U5R$PpCtxV$QioUCCrg#J` z*b8}~pV*L89A(f}>}umXor&h7__vUDYv_5v_eEHaGRUr4F!a_+R5g#S7$;4Ca2 z{Q}p2FIZ?H5GAkL2rrlr&eEg5T~YTh{uUKy40ohmi%`|TF zd$KtN@C?ni=4LUQ1{>Zl4KIE~wk@-+@!V%(1bdnQD7-dM{?U>c;$s6@pz}xzERS~v zg?FQ=CeK-zf*SueU{PNThmCEaFHgl7hECg$dgD5 zk0JQ7eBuc-zZ+k3di?zzfrzJPph9~?4GKn0_&R`hyFlP@IU12)?jTU_=wZr<{`=SI z3WV<)F$qyIzLZff{;DS|j5=QnA};+NfT)nGkGAsBuqwVfte9+Ll)oLHTVyGme(z{h z-MllJOp{oamz%Y#%;n=d<;TD8A#Mm5n_xEsG=;2522ngK$+r^1UtG<16Z`Lab_w63 z@>)qKM^r2?QA!)z5JpTl3%EBBbx|VQ%X7$$KxoedqNA=K(;|rMX-;2wgbL|GiYGyp zM-i(0(y1{{(Yx=tOg`_~fk=!6Zzck+Nf5-U{MaX~(|XH0&H=)6=Yy}8Om@9V7<%3J z+R(qg4C@Ljdh2%>IEb&ndiA2nM#=k&J$(2P4L$uYn*xU@`jNH*QLR-2RV;qC45fY2 zDwpV{t6_e4!^EGeAu;kD7FHGj)?{UwX8oO=_%;A;?L$K0#=I;n7KP>qoHPWOQELU0{CbvG&x+9o2iKZh0RT_2ogF_~ep)^rm(9q{T)GOtfx!qSPv zlTH|7qF~c6v>f@%qSNik$!NE)t~RUDKp_mkK#vE8={G=W)I>VV(KyM+WB#^VI7jZ) z@gY*QBlHK~^&#mU+{q9mBzac*{ClWq9l&_#>$5A2#U@?_e-FjEJfHjKqkeC1nV8-a zx%E?w!i&weHBCz$;ltzMWN$8!c*N@^0^57#oKMrQqqC*~(MPG1N<@7y(l*x_rE%VJ z?Sso3q#tAYskMF-uy{Ehyzl z@4N?cY1@l6ZbV#LhzjiTL7@8o+z*NOD3>C?WhIX^lH2Adak`3<(slpYj-JuR%fal10>m69E9_7H&az!jF9IaPw3<3K*?B(T z_WIhOOAj4md+wX?^r-`Y07tBL4}gZu+YKOw#C^Bg3B`bFt}wj~(Af9U|H;luIiQx; z3+ZX~9iNzbJ%dK9z#a`7=sFb~o8t?; zT0j>MSNc2ev-_=XIaYXY%k=hX$bvd}a>~(ZR;#dX0Y5u7lM<06M+2CdB&@z5I+8Y# z&Rd$EvKBpX`y1Ov-r8HjXV_N%HmZ97Sk>q{=7x;!eEVzR+_1mgW!|60Ep)Z_o%=;c zxB?%d0t^2J5?C_Mrafffy%t1$vloWK*>Y96Lp?>WMcmQXc|du|Wm#?hXNnt?M~a9H z>Mm{5v5qoSJyJ2>l?2T8o?{ta5bCb*Z2$^y*@C_h3{gX;@tKb~v3L9QG~gbPVgE&O zAqfn?wKxKbpKy8`z*LY(LrhtO8^9xhu3QU<&v_(61$pc)kR0w+V7F302*^2W8MAaw z7+3D(gm#Jm)3yl~rU0sya19{Orh&9*D>qEtyS{_Sr(pi-$ey%D1R<0_3P5V-0SI9l za(AF+>ox+^tjdQsWbf(ZT%No|Vnw9NGyrjr*I`o&WXUIb%@qbrC5Hw6lk?A*bG#4o zKz)w1_5};^4l*-=HiFy5r+hu&{3O3$XM9P)qPbE#L%Del?K67a3i&0w1nDgrZQ8tClh&Db|pa#xp8h#Y>c6oK1U3 z9X$27Bn+hLM$C~_BqMZlEyHVWM$ zfsWHqGW!LPi~q^t_SsVhxVa9p9Vt-&oXn+!(j>VV} z`l>0(ImDQmlq4s`zEeB}>A5@CfDZv6(-bUtqu=LVt7vQ^@CI#hPh+wm;*$7pH6NzxZ8x0LMmZd|HvcRuoqN z(aHprH*A*vSwEPnvQ{7K>`h_IzeNNyg!SVIS&QHAbbfo6!N=<|>uNb-etQ%f$_|f# z(PU^fTUU(hDh+JG2%EGakggvo1@mrau*J|_+cwDe~-<-QJi zNLPyfGqRXAin62EZD=Gi;yGN;kwN<>%yqf*$3a4PDTO^1(|XTYwd3f$FQt4 zL<`vm1Y1f3e%pxRoUF(6JrvkI%Yy*F# z(T!tKRw)f(BV*x7f;<%&MO?LYw)qL$?b|QPK5oCOZATqrX3PJ}1#tOWzkWZ`c9p(m zcXu~#+JEhxb>8w@PSaQrY?lG-7~9E+tPLoAm{v7L)7k&(R^sC%i@1%x*~Ztnwco&! zPTnk|`d0N#0G&I}Z~jWm_U`ML|L(gN4N0cAS-#I&;?M4=gX!SG_*k+$Lnd(NFYWXJ z3TJ>ax;-3Wi%G1(hqKzPI1{}_poVn!bv54MiCa2iN+q=v1eQOo!Nelrh(?_R6b*XX zU)!E0(#$()OJm#O-t7W1IraB>Sax)VcK`m3ohAyh!{i@zGU8&RzN8TS3*19v=-y4& zPBSIQMEG_do6neMo68G2`fT&(7*5;0^3#A>aj??c=F&l!P~wNH!o{!OeWWiSb>QDX znA!yO0!r1#rl><|8g02l>t5n(0w~z!S5&k_a{Mng6v7o<-{}F(JSj*T3DpkWM^yzE z7sIHjROo*t%kr9NYAeeYKQgF*@LKgt0P;sNM=4%e-Lfk8hd0B2O)tp7KgSbJJ_69j zCzii*aa5uV{JIz8FSV+60NkN%pORWjyPO!FTTnHa`ItA7u9u0UP|r7aM7m1UmQBr5 zN!X*ed^X_i)DR+;aA7^80!?EHAFycQHG3 zCt2Zn;nuz5*Z8z5k?;A<$(hohc`l-bGbi$CHF_6&HC5XK?)^m^d0gbQ9$q`Bl8cNY z1JxqC!5X=aXlLkZ>5N1l^gK@YpYEq}Y&1L?Uwi_BOi`(>93||K@Gd|ioSMDQg+Gsg z04bZeLnueXC&|1usbk#J@WkZy!xk!|)(2V3mob^GN>8PWH8~7PtUoJP<=ZrbYINje zf7tq45Ld9A0o2EZ~S20kM;vxqaB#Uh;SeL4CKzoQujiQp{=QC+$ z^O09!4WSk9MAx3m?#N%H)`YuW3p+3^DZ@BybQY3XGqJE5xvE~RREBaH;r{bIwnLa6 zhpIWh?gqY)sWd~os6oT1&y6h7Aq zm#%zlkq|D~ki?QN*$^U!=<^~puv4OwNSE(;rjh<^S2}|4JJMUAAi>OfnaiT@apBk^ zgVg2+g8$Z^i-r>hbgRe%P6r6V4YhLZnC3qVU>o__-a$mA{@xwz48mQ_{i52iw^Kg+ zHCezGGxqab1@N!qiGT6Gsz@-+FFCfF+JFIO6RwRULcl%sZa;bt>Ivk`&J~wWWh4#m zagNA=7w#vpyL*(?_r#eis`up_`m1V*6Ibw_sI2AAzn=G-thB^h8^|DW)aH8Lj+zNx z!FMx@7z{A5o$Ja;lRjf3XNDT#E$t$P*NU(bN2zYek&-kxCDXV%BjND%%h^W5D&X;N z82VYL?0S7XAuSO2RUG*Ab~2_Kzo~a$FS3u8PmrJiH)g2wFup{A7qKDAfbmNWKC)v5 z(!of+OHJeh*rO52h?AN~{ASQq6*~Rx?s~cNRsKWE|5YtyaMZ}6U1SF8vq=E%Kr>L4 zBthB+lA*L>KHziyUI+Y|@@hF(^#8EjV%|M@wOIt%U#c&3)zyb=0aGrO_0_^qPg|ES z*ScQ%N!*i7W`$&z&9Rb8V744d zchadsgp&V({)h&CF*}l>U_lNg!~*I473FCH#V}_$>F+vMdcc&(fjRQE=SsBtu4(SK<3>7U}6+nL4wrH0@4*250ak(zhiRPcmeu&4uo`e zVuFO6tQwsa=u)z&&c8h36M13*tByex0rB(*J8j@j15^xdT>2P$2T)aD$UuA18~sog zgVC){a~xaj;!l3b24%q2enXK9e&D56PB#j|F&0QbSJh+h@)QKqUL__H9lSq;DiYKh zEz{x9NQC6VaH~L^S2eMEh!N~1?FXxU+8Iy4<`1@Rsn{H*jiM6ul^_NUcJL7XI$)B& z^PwyOJeeZ7A1L6t!F;%v5(=xHz_Q!N5_aVCexLG*#oLFmjBi#x*3rR!VLeu^MSb=NWO_dysO>YN5I2@P%bIbcwQT&4QNHL&4#gDFyd*Xp$5Nq?qb0PKAy$Y z%73Tzph3z3X76K6CbghHtYHK`QS(X^T@AU7KnSh%`fbYHAu;T2-g;y>bD#L)Q8U3X zpX4t`Abv7l1>pt-l3W@cWIE_NiV$^cd1%5x!1*I3uJlV-=%(tlEbkZ6=rEoQ-7(kfdKsBa=|J?V72!t~y-*z{{XJ=U-vepo@c-pbf%%t4A5%-Cp-$*gSzJ}iA z=H>3N5JGsace+Oi!pj(ybNC)&d;0?c%Y9$MIu*0KTrg#GHQTN$&+F zzCVK*!Rdq|d{>LJb4_Rwbg(w8>L5gx{RCXE0%drL5e!luEQ|-LurNCY5-7zOyxAXs|C%#3f$Xz@Bl=h`2QScP_Kk&7o@l`q2IlH8;FEKs5IAA%tfw9l?L5V5jLc~7Tn?-?*fa?oD1(%jh6z2dF`Y+Wu zCgD~MXdtysP3Iq;zy5C+R(f^y_%W&=bwfjg_c7gC)9S0IYnQqp*h=A=DuCNYV$DF5 z=RrJR?jHrGwpY0Vz~v1)s8qe6fDRONSSxaqR~Ugf=b)X3#yYtL`&k~s5}*+5vIb95 zU(GF)Xn&ZS0P#~6`J6VW5^s1k$bjRJG5*;!z+06?6LGWv!aF(;aXf(kxnAWl zpd*{uYxV5AY5<+Sq=9P7xT6kF9MafvBXHJrBFJ6_k2o`2!lXq9Gsx(d8i=EtQ+9Dt zZz`s5KK1rcxO=kPjd6fOLb+lZ$|Z>NhB7yGt~l zfbkZ(li|cNEhxj>sHFyl?$X8oIq3n^$&}tAU$wz{3Cm_*uPOdJ4Iw-Bg%RR;)u034 zXLQqJE?P6xv09X(zGMrNiu8cXn2i>x3hnUyeT$!{zWJi>4O(8!!8!2QmIj)|sL4BG zeR3>V_zmN6ui!&sIN--*Q4`*IKqKLzuG|1rlH9;aIDuRV)MDV5S{M;B!}xa+*(~|^ zw8`iXBis9(7BIm71Q87T7-b`zk_*du(oKtn_UhFS72b!UDrFtWNg8}7%lIDe&c?L^&h-}P}0C3D@14ar5hGS=!8W4N(+kjS^m)L>SD4>H- z7K^K%0)zL5yJp#m#G$s+57%V_r$Kid_5;;c{a$8zc(Z8N33#O<>A>tGTv}w!v7~I} z{cFF`Vh?zamp@zb0#j9w2!fO>xt;jym_nN0b6k4RkuGjcQfE!-0fo!+q4fv&tGM&tlDA;$~;%y?^H` z;J|C#lgA^WW0{QCj%mC=fsCPG`LT1{)>yiG<3ASAB3%gIjJqjziiocfk}}qOcZg(s z&n%r5vwjD7WtZBAp(jWfg1GVahj#TS7Vum5ZtF#GGFfF1XGNs(Y_dY5jvhqy68@uB z@*$og3@v+P;LL4*>b?i&gav^@0<2#PRdZvY%bh-!HviHwFwqM>|^ z7;h!mqk12K6F>Byc`;fIW<~&xmD$@hoP`k>Df zs;5Z6c7`~#FO%jKlZLQl9pI|{r~YlNbJfz#%K30niDAkgDmx6M^J~tyU1dJ5R3)L2rJFm8pKJ@I6|j?C zW?xOnHjle+v#DMnfU*3?v;oyPkLM^-&mR4)t78-Fd1h%gB>4E*h2PEjIyX4#qqql) zv&khsIC312oUs8VN1!x5cA47Rn%9Q`|6K>?r+}_FYH`nRB>_Mg&A;*>nR~nC_C|W` z?I2E4A81Aps#uJHk<>}vArXQIxpQPd0sMaNmK}H*FcmuPD94T_Yo4F~ATg!5HqA?Bg~(8Z z*BRG3kO5$XGE3NQtawr|{zaTfN8dbW^VyQv34&Sr#e=EFIY6=Rc9q^-pZ4MHTkdqt zS_WY?p+$JniE@U%u`616^Tj4DOZ9~f^pL>i zuw{$WaN7@DKI8r{`sx&8LT)AI+n3&8Rl@19neU&g#GoacGii=VtSvIR!wDcF4al_n zgq77vmPHt*GZEAyqiE9UVMSs&y}NO{-u5s;vo3qsCOs|iBTbY;TPMA%@XG}8$V2JJ z^VRwWe{KirpGQjU$9;3Enrf;Zb#bIlUsqz_iOo4;5f}6;j&=xgy6icM^P;qQsbSj9 zS4E%f!;H50o=FXz3#qjY;*SJ+`Ii*eGLnCw<=Y=hph`+#u{UJXC*3;mbVh`>5P60g{; zxA;8Xk=T`+I-0-1x+u(UOkq@IS?l*$#6gJFhd1dvy8uJq!}?Ch0zigSGo7wSMK7A! z$HW+}HJx-0IyHB$rYD+1IsNyeq^ZV|w6q-o0+nEO>Gl^;wnCxzc?)&p_*>#v`u1+E zcOBoKFqiYTy>RmotRVG$v!~8iW@Ff8!{kaByd)$U`K8l5!IeT7#(>`uD?0t2hQ%yA zoRaGT)j)*;kBg@r&+y?s=0gQkN0&s&rAZz#$t9synUi>WiqCT65!mur)GM+1$>g$l_&7J7p|T1QrbVHv97xZxtcuj%Rf4MdvafE zXndk)&vAhM_Ly<}8%pk!mhXJDu}f&*c(i)8!X&=ozpC$Vxr9Tc75Vt$0O~IVWn9rc z%~`jd$gO%Ro(_u~{K01QFf1j^(gv|jE$$lg8Dqq+W@aP;OJ>DZkA;U86k`jgq|>IoKT5_w&nM*Z0V4 zx%l;24U=kPt5*c>MS6MgYafQ`pXhGH*fbrGfOQOe`muDHn08w-Wn|ZNP;@)fhkAR1 z-PudImc{jC%DNG#OK4}tE&xCIQPD}b`(OO^-{NXM9s@UOQU-X%xu!)E8KBKwJQ~B- zA`u;n+I|Qd5u7j$@FU*IpBT-<(V>MOVE-DDRm2)0cbc>~nP?D^3dga(?wPTnXqfHD zrp9>gATwn;4N9UGeO%F__{Oz#``+B|SIRU1JYNwD&v>IzP zt1wD?FxlZdnlYCg;zxHyFl6CSaj}8tMb$1M_2+|&&M|C!%7!9wr&^Q*zr7aE>b3}v z)UBI2aH7!ze{Ox9>l~A{lr&Tz^<6R$$<5!b49r9AljMX(tDI+3FCduN=hoiYw=`Nvt9hFLvFXLsuR}aW6Zv#SbtVuV0fhC{Qj*Ml%FgrCN*)it8R=M|pZFoW44LJR*HRY*2fHM$7(0F5jRJdB zV(U*1YKVBIU!;Ih?Adpd$*9?9TMu{PW=pMT3$Mp(pP>-#G17j7{M6ta$xunlpIfM` zPAJ02EhpU35964Y7>Fu>;pUirUMMXqcZ&nMku|>#`B*=x>LCO_UQsjS$r<)9|LHUm zjOXOVl|yqh5>k`oy1K;4@V&8KgA|iLJ}UVy?Wzgz+ueRd$HVf{|M;(9`)??lmO9jk zAJB`_Jr3z((*YClG5QfF1ut^LeMmU7_zvPS_sh3yn4s9A7S&94@6%)gp`>yW-9YIQPX=i|Aw5@48#3Oak zBulc98J&s!fdcLf*Sn)MGOSvdCRf-k&N`lVf9(+0aReK=^Pd6|<~7l*?kQK96UT zvZO%pduP9}ed1@qqfAA=?O-wQ0_9x`Jd?zhLDT&{{LAsAgH%yMI0QJdd#hkteN|D1&O?*FJMaS<{7n4jFi&=x>`8+al z>9Fiq*StdYDc2{ zlW}>YnuK+I*||LWPsE(hGP6cA@?bbe!NVIsbHPByDRVkH9S>8J4sXGI5cGvW>NfD@ zZS<4O7IgKV&0l3%{&g3YIjbL=003|G4D~SztTUek!%xjfGa%K(!_dl|Sv7_?B62X6 zja77q++n0X*wXsv^EtrHY%;2}P#m8{7e8xLA@;QhdRphQnECzJ@$bmNZnh_V>eM$; zS~Ef|BjUT<&dqKH*WeJkhwJQ8auo5gyL)ZMF69I+!`3_JQI;m>Wtk*~7EJMFHrZ#Y z7opJz1$p8_w%ZYO@!(W|ri@@$rjLfMQY?>RS|Z2rlLcB>?jOPYL#7tPl@&N&|`J;j!m z``8cuNY8$GXggKlOm+%%EK|nE_nfH&D*e}E*gVh4^YL#?YwN>KF}x;*GqSo5Q*efX z^1UwSvOdSX<*KhGbHGSIx~zZ8J?AnrxZKxwKQ?H<@Q8hv!!je6rD^cXF8W3syn@|g z%Gne~`bv}Rc5$_(^^)^is`Y@FwaH+a@e(u>JpenSul-@~K@;4@@pm2fA#v`v6X?=B z4VCBQa^cMF4zMR8kTS4v5Ti}r;)^)G{K21Ix2e%C9d>^;u@!LCt_w1G4HZH%6L( zFaMLrv{4GVNDNYT4ABIq3a#IK>Ahaaq^JG(|55jqVNteSyMic#5<^JW&>$r#DGV(F z(hVXYEhSwdL$`o}G>DWSQi`M?-O?=x4Begk8hm`-cYnus?Em{8$8g{GRjb#z)&pW8;^BdS~p|F3UUuo<3wbfiqzvRhxh5|_edGj*Don`(<56bq&Gg`OP1(&1*kb6Pz zyA0FPbq@QY-*nIX7?kdvIg7Ip%QD{ERk80_=~k?-{d8Mwq}0{Zc@%G(r1a+y=&T5oKq*{4QH7j9g&>ga8kWM>~X=u z#r?OjJPeO_9rj61=tN@QM$~v%e_G*#vluBEXv;3`v}|KJO&yp!S)cwC>FmS8noTh` z*=5a5`Pm|bqw71d=6CNz?r6!Sr|O1K`1oJ@nA4pB!H*X1Qg!*W7z;t&#AUfaC5;zQKMtronDPPD%w5CQwv%u6i`t=! z>aq}*_YtDiGR0%NLcBkdM}prx*wkH%E*fd!fiKy+hNYR&>kW(1T=z3f@cm#HbZ`${ zdQTEgx{B50>lum?RlA~yx$kUr2pK%JZ74B*qnOu$naIlP+gd|XI?aTd^H_V4YAqO8 z94mso^fywBE>A|PKl$z##=YOpGJHZ-rraZ^3C}a8llHA8owHw8-4{cgFlH76_xbs^ zSNHE4<(=IS=#|ou9z1(Xc5Y8La|k`*67%3PG3UZ}!7`yz`S2zku~khx__WQ^7S00C zK3RH-oJ{JevKL0;3M0j7z8=LwZ=#Qs>rU^o*_SCHH9HeazyH>zd$a#5o95i|4G+vr zkr37+} z`infK@%CpH_^`bhO$Wn-LtN-K9UM~AAt0pyNuEx198V|6r8wYW!e}wlVB!ic;e^Ep z@A2Vf8nOQfc!kO3ie0z}U3r{%etM|V$QN!@dLH~O?bCGs(0Sv94?3OmU=^t+erGbp zV&E3FZlZb5>#`4AeZYrfp%ETu1Y}V*iq7dVFt@aZhtxoMf=k?Gtblc( zkKkV1k%yBKc9Z)g1nz5M4YWS4Ln@4CvXGwyf01#*$@PE~+IkY(mCPP55W>@+EZDuV zQh6(SdspD{gPrZl=MNr`@Ti^q(kkW~gAA`(-UM@VX_}M620tv?bH+f2zvAoH#3M>% z9D>L9N3k+5w+hI43fxwp=%IMj2jZ%I;ej-=ZbO>dRdO^we7mG`0hHK?xTkJ+G#Sq^ zD-iDR~#;m(|x3ql& z39z|k1tmwG6!(LRtXxdk~7VdafZ|PvB@fmVI3oE z=%#pNcN(8AQ^MJwedAUtyxGtbD#3uU*J-5s)(4xU*KE|WF( zP3SM}mZ=rW5p>(RZayLsQmVo!g#Jz0(kMf;zldR+4OY`ScqWU-R8+ zNNHC-N1PyE-l!HUUr;1x_~kUw(2cDf0g-c$<4exlWE>ON`vVTviRQ?+uc5BR=(qar zp=Wcj(SOx;FR zV{rp8+al~#IZJ)AyanY`O3nho3p(Q8)@0jr8>f_>{bZJuqmgOQpEpCU;nSjmYs(?3H!Tdn6H6v`kNDd?9 zB@VF1?Uwx2UIYX0z-`po4CP6@l)%F>akc!my|WCy?!T@sO~*|P9&F9B>fuE#_9yq% z?kqsS{_10ckJ--T@kR*RPp>_Dw#EDatYVK0%vszfx>QgW^a>}JYQqI@klw%AoWh9L z&`SF_2E@8)7k-EY5BPGNWVb&%odn!c^=^D91d4XGzAT)eh)VRkB6hE?ZMb-PC)1A3 zR5BpWho~5%UrOl1vhk%&Nx)E0G+qcTAGfPJt(0uAszyshtXfP9q|ImgE4^!dkiZz1qjkDJwy3TwlrzFbjRU2Dw^;@(m*uNsychnYy3L|Rhv9i{ zzyy0_#wbP3^sH?u#$ndrYM1Qj`+%C5$ZB1x-$ZcVyt&pyjb5(kC7dbEk53Exh(*ff z#`#Q@Y)%0+CtCC&d`lo0U0~m;Uvo`Qne@hn<7XKD6!we;2ehe~=!?#PSiXl0xmq~N zmt3WUlW%i_FHwFllH%931EGv?t9FDxUR;O3x<&R1hou@_H?D?m{)lV8`pT57i8nVaTOxh#B*;W6}A_~Bzdp^qMYoWVK0sy#snhj?q`d$2cGL+s(A z)wv%kn`c8uOAU>FrBpWxxfMBYBoNq5&Zmfg;9ti4JM~m&H({i!FfxrVV~mZ50$N5x z2LzgwO8o1;!;IpWoXv^3PDOZJNR=qQF1)8-;>U8dWf?5Y^E-$YYM*PG@ofeQC z+ihaMQ-CL`zm}rEo+8pJG5SAnN6Pq-Y3N~zxpmAw?3gS9YTjGn3-LYC-BR; zK!mM|yJmeYw;*}36niin>pdlGW0r>dL9Vbg=ZANT98vKPbQ%h6mj!B(CPMN2Ts z5+@;k)aIeK=nj5jl23bMxE4jLo(56&UTLLKjcHqpcuTm1zALNQ%{IFE-5Oo=9EAxq znO|FiZNpVPRdzj9g{jWF8f`2Pm^`UGL*2|miv9z|l=5Jx-|y>iR%t@NfJAFJga`8# z;|xu*T{Z`rsei*mWBn9o!Ja5C9_rL}?Qzqy<1*)A1|pLJ@$U)T+G4ni(MVFr-H`IG z8+Knvpd;IQn`>PP5IK8vjDI#bfWTV(523Jv zo&Ge5wt#3(Ep=%~VBc|O%2%UC4RdTP|3GaOlCSusNB5AE!@8R^aP>gSjnw^!#@qSf zQ%5G{JW;s%{O0n8x1`{sj3<=I{}9-}DrOJQzZff1B?QE-2f%bEe%#evgnfJnk;&F8 zW@L*W9QnFg6z_`H6BIG0%UBgO04#jgeP@bO3v&)d5S8L-SWElolM;A99IW}jX30{( z_=pd8pLrZ{L&j*u6XFABNo#$Km&xQt>*^tdm|Kslk(?Jkk$WQlopgQbZim;PP&|uh z0A5Y-+;M!3LR`yW0uW<{uwKW%Of@PA`t7bSy?R|9{*uNhv_GuC?2W=s&sfPrB0b4| zlD+NYkgQXaRC8FiFb3frG1ZqXcr2i!Ot*8U&uw#>hcj5)|ii z7kuhkg$_%cH++t^K?WckxWY*yr)yJm-7Cvy0Zl?#cH_}XO#PVFQ{QH08);M z5KchK{Qh!~H0@15H1{5x(PUCLKENRJ1|^eA8{bwr-IjCSrXl8~@Wkees}{)*L6H1< zy+2W1x&xuVI7%sDa;VDw`<0i77y;;Y{~AhoYSSCRn~l;B(0IQ3UHX2p?9YM=;Zh0D z1E@de`zFSXL>hBn!o?NjqL2_ z+d2)8n|~v<^x8BeS*%Y$l9?*`Dfc*U);MCo~x+!uit_=GNU4Y5SXuCxX1FC3pXu}}=C zO4Q8o&xiZyzz-a+I*Yv`C0y;hy1Kr0Cktw)L5gdS7n2>u$j$xs_5drXHxJ<_BT8`f zYTo=6+5c#Jp~6K7cDodWPoN9#l;pgT;ru+oc|!=9;;ukaxQX-#9-LW8iIZCEk9;0& z&;=4+)9WcxG$PMxvz;O^ee2C#ZG|$@Fgpal^s

KxO@7Q~%P_5E~X#&Kn9y%0}zhQx=TJaM{jVJ1@6p-Jr72cEg^k z{jzf_F8UB=(2DQF>a+bI$3EhFRkV4E8i^~qJyje%RTQbtT$Oo9RW~L0ed~SaoxdlB zvIeanrjpkxFRYN0`1XyrQ=P$mh&5%snzBA{$lT3WH=P)zyO|N>?@D7-&y$OX3tEZh zSk2T=ZuU#nF}2ak=RLn}$ZdabmP!Gh2(`tP`J)Y_8(07s&|2dj(OqHm(&KHfu(R1} zy!iRk{bQ74Ym^BW;?E>}RoN(%+iN~Jas*3z-19+K4HNF}{$_sp9-haKUS}j?z<|x8TcoqCuC=p+|m8AQ6X~TMx2G zPQCO$jsr#voFge8UV;qV`ORN>475~W^Q*>W7!ciCrZ)dFH!Thz6)OkoPL^@S``sdW_ay%b z97#~Q^i0jBRChu{G$ENpE|TBLykE4jw1L!}B})kX&IQBAJ@uZd^t64_=j;%&p<{m= zi&HLekML}ku=gFMc~7YlTty;+Mw8{ZGT7fWYk)Ne7mPh$%&A2T)C71CWSy`?X-niN zQ;J8^lZwxDYusnUw+sSujQ4Ex-9P+t4PQP0{6e16%6F>4L zo)AH{n!SjuH>Mb@-qxy<-8cY3^S#nv@j;93BLPP6>HB$P_TDf`1sRZh4{P?9MhDiF zn*sKKlmQsO)bI7Y9)0JSChq14I_Tx zd&&0ohyQ)i;tk&M>+Ajpzb4a#kuL*Mq>j9Ls^~YWXF54dXMK-roRz2b+1sQ}KU*>9 z@1s|HBiDS)mVFf>n?xeS1w*6)Pva5X=(o#$Huu)Ap7maSw@9zsLe98c{KoS+>uASu zSPtH-jhPXVqcp0}n3JG9^8V%g(+~IK*;XxRI5w8cb@u(nCy^aNxl7;7W*3arQMa_| z&q=-aiZ_YZ<9ap4wNKA{W#E>9-7+!1zu130bE|heOQ!j{b>=)3h^+Vc`TMtuU$fZ7 zo^#X;AtvdK_1Pi)fcsEPapwAJ&Pnf0n(FSui`df!FTbDX4HWgg^Pby174-{syMwsX z^#(IaW6WziyCS{`n}*)KYnxuptj~F|Ofcx4y$`?56YG z55kASzaDoSeoMOR*Yf^-F1xM>48e5c{o`!um##t&EAXEE9Hp#BX13BW!zd#jQZ-sW zD~U0cL=oUAq@s7(#H3Zy%q~|zleh#=!QF+%WeGX^7MoRXRG!gP;bOQ z(AEepmj{d;H}79|JHuxPaEz@;>)f?pg_1DJzI06@b-*RLgC=_VWk8Db&OndIY0rSj zvq?7|6&Nada79Qr1Fsp^VCM9Ee-z{@M5-C(M%M-0x((nZ<*4)o{duM?96V!%LkwJG zy}?`N1%ZI8IKFlnKZ08t$q>c-c?ccDy}VOv)*Q)-@+SSAO7H2I^N|fw zhWvRWrms@AEOpoMd2DyW}At3HdRuc zqiuc)p6YKL*84Oq+#WGHQ>Tep4JUR7^bH5}ZQy>9%>0D}5IFZUucKex>8|yMa(;{A zThr+;>P2=xJ(?zQ-@Z7DGVT_m4mjl6oGIRzaVtLOI84oT-Ku@Hx~$N8@M7PfK*=#t zN{6D92&c491$%FIQ}}W_Slo930W~_A_2N1U=oQCNc>*R;x{@kaY3Mfw$5)$dxG>p2 zyU;|0Tz4GxUhrZ*Cm?7D%% z+x!uC{#fW+P7T%mQ+DpD?7oSG=BNOYs{%#IO9@Nr`-KsDeMbJ+qAGnQPAug3;FTvY zfiS^+oS&jb0LVD5p-b`u?PH>1QJnh#4;}P4xIPZ*3K0)xLW%JRsDIzoLj2?HW)a45 z%0d+N6v%IX6ToM=)s!Z{R2ZQwmDe<*01?4Rm-^$?zf{0DnzK{m$R-JM74jrpdT^}^ z#+TppUrd>rXsxDp>#t2hix0%0G6r98e&DtBQXqmyQ8poQ#at;{wlAwy+-o&dX8%fO zgo0>*8(oIbBMuD9w-vv@Ma+P*%Jc4kE~ZRF@XV`cq%#!3_*207s)nGAGukzwqi1^j z0#=Wt*9{)-;D0yaCFI2`z&9fPsR7C$tfN5q8Rg;Y^})@^u#`C~-HL4y&E+kteb zEyX4Q`fzsT4X+LA57%w;O)L~IUUNE!>6n-Q32vf%cO3Am?KSG;C=kJHR4w!)jbjEI zDF}VUcY?$ zmz{q#ri;$__)Zk|GWZtIL%&rU_#hUwcbcn(g4ST-fGCaR14433D-+6g(=Um6QwMFVytO{5GtS5 zeWEZRiI9^c>8=|TDuiKZbU+}XvAJAR&oac72sKSd$I4W=&24N>1k2|AvdN7_joQ>F z^C*fQzIHSxrZaTl`Lw*#s4%+mIxuYFT%Xi+SRQPh_P9NIC5IvrQb7`t~>&P=%&ba6%!SAtJ^yM39bJc3-d%U20X;@7!0C=l`5m&>ZXyI+a*H61Mqy7v zB=jLa3<|*!b8xtfesY12%CzM%AwVBYN&58|41rhCk)bcGQ0jh=@LM&dfSyhvCj^#l z()glT3s6lwJ=CiR;C#^ZQe5CV6&*{U4{f5y?7{lPTdUn-9b*fXqUuO2Xbx-C2t-A~?`{)9oOvxOj>w(+aI}i1cjF65x-bGqCE#pB*7Zco4 z)3V>lok%_4DSqO--ArQPOpNV9L#jCMwUVF9gO26VcL+KcFIC!(%>nOSBdqMn#_#K| zIL;;Q8we4k2+i@OZJ(!;!;#;8LnRnNS;Pl|M;E|vU_eL)HKFt)2C!#IF(&fJaY{Q_ zeO@c|6&2!L7BZX*6jM18S(9{JG!AX&)>;G0=;2!`0rH- z#{+MZ^u%ftGNnUTJnk6gF5tuSLd&4s+lh2D+gah9?_x|jXy(P7=Gzbr3GLaAvNJpF zadHkZHr_u;c@b_JZi37c;y`vtlEy(_LvNDdQUL1|in7b0w%OKviKwa#!gYl_Mo1>- zt>1)j)+3%s=)X`B_rCIkj9$$`Epu4nPHbN*fcFt{mdCD2Kq_j?5hY82Hp-EN(!I=f zbOCM5vtM|avGBTYZ^y@|vespv2I1~(>6TmeQ_d|DHfC+hxdc0ve!zdyW|`BVT_KXR0-zq|xi-TAgK%K4 z?C~H#yAAna1ekO1l@@*HlT@fyKF_Ne5xnv{Vo}71wh=y zOP0Tivq>(RzA9aqOFep~e8pydB6up2(tk93P3IT(l%aAcUSY%s>10!I4t-4MpGVlV z$n4bhsrK!_oQik~3jMOinQv9J3BCGeovQ&Z7*ljF28}rx4@XU-mDrxI6l8jUScQXv zp$TEL?v`YZnl}TH=KO2xv>hMagnqC)Y=w*Mzmc48@EfzNvwZnSvR3lQEvR}{f2)6a zXC?OqE0__z3ZUIyd<~L>-#3_YdM5qE7V?7$5xHfW&@a1HG2{-_lfJqGN+C7XTsDQ+xT(%ou*3k2`0}Nd-Hg6nAD?5P6LQne#XMRjL&2HejBI*whW{iyQ<61w zq@Zp@VKy=e_%E8y&dNNSInVeoeToi>tv$l)xRTSO@zj*}bQ_Ze>^h_yk3flbr!`dK z0HBl}MKxP@z8T}f4^e_h?KbF|50-G~^};@?nGbT;5JdtRhXy2hEETs#ZL(fE77P;U zj^nu23$761<+*8}$@wz}E4;Wka?bEw%rdXLWJ?s4h(NFo-CJ+aZaMBA3A;{A>Vk;h z#hC({pjE(`&w0OeFPDZ^yb2x}weQiatum?ht;S=S)-$$W6S8x zh+{Pm@jX||0G#o(jzu&*|?6bO~8Dp}4=$e)`d9v9fIZYx04r@JZd9_Tf* zWnV96T}6~w0}WRYqz=YyxDWI+jykj#CQA0D&ysX~zoV0+>vrwn4<%$NmGFx;e{dU2 zHjN7koW1xRo<8pNq1I6xW>D+7pAxEYpP}dMPGh{qy>Fghm`cs5;Zx~QRN;(BehG)E zsqzk03#hJeT-5aj(tHuB6MNgo$~&xXWrLh899ob;gP35YDB=$-*6-7C7^0#Q`- zsPn620%dpy1{oU27K13&z>Rz;r^lH2TXX8VWc#|Yc6=q;Cm?bkj7QV6kFHg8OKafDh8lv`j_{&IBy!8zxZ|Xdk=`cB?4fu z(e5cgs_79jLI{E$)@fnGGZ{kRbxA2@${KrLfF{4gX#W^Vd%`>7b1H;*ZO`oSstrFr zMGUId_DThy%~c?d=KwFsiw6_`CR`W;`uAuP`x?KIN_?xW04LpZf09j%vM|vG{apaa z@47lmt`sm*gfU2Y$bWDb?90O8y9kl7fXFAq`7l zk110-Y*Z38Wo@KdZUSiq_16Y!ZpncIJoNS3LRUz`=)db9ELqU)g_$L&Hnm9ghcvX| zht#%3Q7Wqs0NXzY_ZI4eu46p?r1_C&T!Kt1`OnISoLES}mY^0+d$n*Hg2s4DX8JV1hgE@7 zhW#oIs8;Sahfxq%@TUa?9`J-hlU|FAD&d=uE*Rx0T|-PiH=n})OVP)8+zG|@Z-N5! z2L+%2TRSw*aS#+ll;nO;{2VF)L~N}VI3%v_3kYA5^~cwERc zt2@<2qTJC6BV;k+3{fhT^^Zzj*GOH*ex3?-H8kH_OtycH>3;AI!#^o0spI$Kk%UYh zM-!^4N=9xe*$H^%sqd*Y#dAd!X>y+aRhz{fzb1cqpcZ=pelle_<(JM=< zdP@WbE6?{*R0AuKfwwgezaCS5d*h_wG$x_>^|!Pr`?uPNrAmGe1&Dq?>gn(`bo?ap#BSPc@4_>nT0-0fO$2mpnRNghcHM-vW-!=p#=?y}Tay z#Qv?6wA(gHa$sB23N0Sb0S0)Os=#czE+hj_)HqMTRJ3BgvQ&ybE?yW|UTrJ*zywX< zXAUdW2gYPgb$;p&sIJMl>Ws6cDJ^X{J@l4`sAq|N{QOZY7r<}EwI7OvqK-39DcPDK z!3A!a*^$?5@KW|6jr;GuU|Y*-|0XMI&*2w80NL!KNcQ(I19{815B{?|O;+ZbG+GPf zQH!hH3_0R}c|Cdei=X`w+kowIX_I$?>=fHgR|PdJGS^#z?BDM4=h71=VDVrQz=4=; zT2`QU?#SRqTonpF3P5Pm2+v0;opa+gLCx zlbG4P_pwz-BH)pBDp)ZyAefXI;vawHc?h@cpf!aMp2XnTkw`r|P{l#B5eD!V{@P>h zSYT!c=_)_a)AdruvesHy3D)|pn50JY8O!7$2e!VpJ$Q;b0{Q=Y=YVL792Dtj$qaY2tX6$B+5f(1ndN|P z0{>%sFb=RVSwVZD57J&breF%O(DCw5@va0Y*HSfEU`U?rijgSt-QpvqXi78JWwD&( z;wnGT2SvD=L9l`tL7ZYoBDmxrg$`D3Wy!kEAON#PfDa11`c%{cqJKBWz?}6TQtQ`e z3g5O96!5N2`6NUISm5nq-^wThqNtIgXcyE!Hye7bS8RmcEp|XTz|ZSJ5oy*$iAg{c zz8df%pO=3Q7v?!kd3R^9z@X^vkELs@`;u_WK?rszuoOTRq3d20OD5h}pR42hXIz?4 z?p%6;MmFo!1%8kU&;aR*(D5#@2f?OKC3zL{@xTe+xsp%-oznB`YFRfiGiI@L2k@n2 zaVQj}`defn%>$J{Q(;$u8+&F@9{t!1xC!H>eyyj=!6E2EKUHj4&7(pgYX9}wOr6h| z{g+9n@M>^(OKH7*?Y`C*&`vt8Zp7LH6gp21X&a~d1d?b)IG-Ikez6{6bKCkQ`2{p9 zUaJQEnr)&eV`ir|eZP2h+Zg>8cs_Z*Rca~vZoR3I<>9i5-?-0;db$&{a!T8xfBogk zr;85MWgi=$=bcN;GH}A3-G_h8z-VlGS(5@ZtKi(!@Z|WDzseIR4?sFs%EMHn9K|&v z+1%J_>^8z-wQaa+R${4xqYzlg2)J8?7mk_b_B&GW8;5@7R{&`@pH~hcyJ(RM83t)H z9yONv=Kvm0mOoMDY$02^1q7p`&G1JdRs%vii3?DLofhbI>pKGbNU%lNbxmiuw?A8s zcPHcm*`540GSr3-_Cv?x_napdnPJHjNm*KOZ_k6ILR{u%(QSMl%BOY1Y3d9Jn4WFD z(mzaZd3q4*jd4YKammt~nAkpTL6xgwPsHB6wt<#JdzELTj5s6da&Olq+M(v z1oMX6-anOU{fPw8hcZV72L5O$e7eSQ7dX`L0LR14nN3=i0SDDKQbigQH*Lqeg-*?B z7RD!v`Ozp116b-m8b*D(#E#X&_B?2Xy?knPYu%}C@x_lU9?3=EGQ~l|G~P*w?GLJN zH27bSqZENXML8vnY>YK!d9N*W?>t^dBZ9NQ((5ex4<8ZZ!;=E2-TJd+m;)oM!Ox{z zYIPefpKORY9|F@*p42=$ew7ShwUj;%zt=Sf=;1ft_h#p=8-!e#tGvX4!#wfL#YQXd27}rTzGPR26NHh02+`LIlTN1% zQL}xfhdT>ywsdOk?u6|{=Qo_u0RKhddkSq+nw{B(khwuIlg?okI2ssitt{t#Y^o*z z@D1qbHLAiR zspdnZDT-=Nxh5(xKbPsCXw>3y%gUzCSL}Yf zek+jcQ9;BBDD%sfnQs5KQhty^0PeHYB$>%HNdJ%MCa*MF6+$Qxx09CS)|i;Kb&5Cc z3g+3I4x6Ghr|v0v2)zjnWcKi4-t%yW(5>2UM@2c5cbT=8Pj~g>6p7t+CJYslAS>Yy z2QdQcodx3T?K#^AO<}uH>`BB%lumpP)x`wuP~%<21PXit*01b2eoMX|!-q{c#-{DsxxBiV|6LqyayCVYK2VI4%}ro z3YV20k(kz_+gx8r8TtAL>BE}}fVHR_Wk+M7|0u18@XIBvwEe39s9|&4bvZ;r+y9<_#nkmFDZWh!TQFJ~c~RmnR|m`>`{Q!4Jw9Lys5` z3Qv@ILaLDQs_>AKUer}wanC`Ihn~an|k*bpq6Hd5`JxH zze_vv@Zn2Hxi-lFZd$+F`i|$ zdFLB;jSuaZKX7P-%E|mzZwx#bC|uD2M=XEOth|76fA`y=p<8sdLwoX6Mg39nFf%s> zJfuPf*R}rqV1l_-ryMtpyrGfz?t-2groN)na<)>86pQP1r?+lqSc_FacLg8Ptv4w0b$%v&0_bFy$h(K<-@3)CB_QnV4 zoCN^HwF|d>!)f>~v``Q&te_Uw?gyIfj_sF9W{Sn`q>iqJ5;F5pym8uxeSKsTrzSUG z_I>k%t6?Fe%f=eCADR7BO;bH5aM;&6^6ik_N3p2Vh3k3gu=rWH{>!Sj-Y?>LV!xiK%tzl4nAEzY8U@FkOeUAWfede)QxEo7VW`^{M5XIIu2J zz887tE$n^MR48t3hb8{;uJNL|;QF`K;?CD4_`)y|1CO+fn%&eVuC$cj$Vuv&Dx7|C zoeWMtOusJ*&}UAy@mRgJ7dpu$8V;^e zi_h1#e{mekTRMi=R7GH*0{sq7MziWFVlF?Kv`7SirO!yJlTdh@N|zKE$5(q`{rwdk zDpCseAWx+wY;cvU@+&Sv!gct?nRmb2kr6K0MRomYb*P+LbpC=as6I3M+(P$BEVMMH zbGB}rKL7VIzvH#g*qCSmH{_K>UzvWb_x7Uro60Ev=XziSmS%z~X>GO6PT1hI%#7*B zR>^w!$xcr|qdkeSx8t+hCzD*T*BiX@hD*@&zy>>B zorLf9%qZ|PCQ@XkHLFYwC;ilc7G`bM%U5w=?4KksanK$gZJQn`; zoq19~rWJ39?zAPlWBAN162f86Ne7q_S;?ZG%07)``R3v`Qi-^BC2_j$F61Z+VE#R6 zcEa!uji86dqk?dj((Jx2i;KOC(=sk9SL5UH-Hkv(t}5fp1{D#6ip#!aIYj*E!G#ot z!b)`g=fG`#7`vYK^Xhd0wf6!J z`=fma4gJpd$2vpY@9c&&fyfh51IuQ0_z!XOMvF$5Fj)lSQoSSmx$3op*lWN2_G|)= zY~NX?&=b-O;y48d(8bug8t)k4#K9_oz^IkB-|`Tf?~Eje3DBELl~1$9y&$_GDJl>~ z8r@HKSr}fe&V!riJYTxLZoGy!3wkCpdu%cwh9bHqcgx<52Gp{l96C%J*?y)Lwrnbp zZ2&M#i<__CkPxsXd|iRU4Z@=l$#eF#5PD3hll{!?xc!_d^S6d?Vx>eUw0Qq!5%v-+jI|F+F?nx zdbv@-fbv!?;5yAxiC>D3k`+8T;0p?=n;Tvr`ms9W#X9`;L@n}*%zwM#)4&ZE&}A@f zNSh1dDBuZ8fnc=`ROu(a-5lD*GVPz-71f|6)iK!iV{aTc&ELJb+YzF8lr7FA6-TW^ z3FRs1#ShC&uMtP7%;FW%u(k)|w|lm-E_yc4l!qC^7>{Ip{$>rT0xQ3tWBf2sQyCfD)^ji*<`7|if~xclx=1|v$ozTHPm<^TfdeaU4Vd&oFJhm z$3c3;m!sDo2sSa7@zA6)=OTHY!F3k!A6J&>vg8_>PpRqVT){ycjxvp{H@!Lo)b!#?p{;$AD{g zqiwImcRoCp<1|ipfrToR@3cf!P`_Cy8cr>%F>X%^8|b+#%8rKWzDORmxdZ&)p^)zf z!&tL@g%RX@q0`|+SI$2nH)xE&$FU@@*cQ{DIQ(Fv4)_i_U%6I1<$^xi-9E8|Lv~*z zX@~S2o?%s;w8@FjiSOsD2n`ZkOS;_4eUN%iamPe8ydm15KY7sCWsR*hoZaB7L(fhB z>6ka!56|?lztO(q%C&twn4lPiUbv#G1mZAQCfTQNHDyux(?3$FgQAjyF}i5xacW>C z1x1}<&qzuX$VVzVlMashFe6-Q=>yc#1j$eVkmG`m1#OrZShw_`TYlNk49<%|~EvEW|;l9_!7uEzP|0e`A78cQA*LDk*TexJ58>cIOpV$Y!TZ;6RN7LaK*5W?V!bp4I&6((Ze77< zLr60Ys!Y$PHdMh0BZ6ybiROJ=aaHBlg&};Etz(07@wLG*yUoL70eY-$SR2~8gD=5+}W4c?+)ExQVxL!FRLUkS{En>JZ{I*KI z_%jqVl6L?|jI)%oG^tUu*a$SQAxz7N&!cjb7$~0Oj2c|axrILb!pwT4;?~QW7It){ zVPTuYK;0NZtY?}unhPCwq3>^x+ySMXh^LF7?N4!e*jdU6!0r3hJ-;(2+%0wA6&0io zrDkFc{;UjXRWs+y=?NCs8j|U4g_TeBZI|;suo0$%VV6Y#JHMp=q`v^vyem{)@)T)^ zb;t&jG$s$e535KQ9Qk1lv=*u`fPE+GHTvhc7*)#H8=okPfp=Co0ot<>J1rxji<)u! z&X9e81{*R9k4lxOqZJ1h7+XyeFE1})ugM*u+y&?x{6;I-7nlA1AH+F!R}5=v*{nk| zjH(|a=S`#zA^b)fT)B$I5P;wR$6waF@|QurZsIi)0(D#VIc^Ox?X9DXMESVr3Z#Z1 z`kXe|&>dmb8+(5nkZ92^0t4qcxFkh)p$}ZL-E>tp9a2T~`nZv#jvJ(9e{>U zkCnZRf!EL)zQmTDc(>+$#$Z#Vq}KKw-qZl{5tN)(?P(_Zj=@F5nHW z1#IhK#Sj`4G6e>FcUySO4fk@C!F8_R)80HoD-N#M$(|@RR~(jGYMpepcBONfFIZW) z)ohu!zT*CFbt#)wkQ_Gu7YktYSqu?554x57iOU1OoMU3;Nwrzomfk z%a!(Mso-mmGp^H6g)I}pW9mR?>%ne8yCVjO3?w6NHx6Wr*$d-!Js_5V9~Tl;*8*i zYZOCb;k+n5#O?bC)ajEo4yUf-CoT_uZ_7VHd4)J-Vg&FB&^bn3B^9z1Y<_}x zto=0JX4WMD6W2^Bgjkrp11ElSK?fZ`(ZQ_@s0fs<@i*EKrowm7lPU|RsA-?WDM6&D z@YLHjGJ4xV5WtSN(kw2?mA}-_8I-eylU`w;g4TyY{wS^{>|a0JfLItof`1llxKWVa z=c;*%bm_(~LHWtH&WMx#>sXm$a7gHjv;_BaP^+D)xIp3&JIRAJhKgkHs$}`Gzf>Xe zf#-?EYeHs0ZF6dI*s>R0xYHQM^|N{f>dXpM1h^ad^nV1f{|&>U!j#zVmjXq0>%AOq^q2f6DnYlN)QSdxJ@$hmrdb4fd7*e0FnMTuUm)hQIY;g zoEc7}jj|j&+dz&Gw)GmQc8cL<9pQpyU(h?AjH#l3dzw!fe2r||Hc!vIJOtQLOBv5ucjFYQ(qf_S1cQ5SfEXu1 zV^1V^<+Ur5?DK+!Uo#|e03Qq1%};=CNZ2Sp5HhW_R(7)!BSQ7DY@3xlwbzTS8r*KN z2NP)}+ZXC90f@e@_ei0jQ@sKNC(*6**2YL3@ zO85&1`*;*}@Za-mzi1P9vhEuJg08x5Df&WxWPkIhi``Y8qOkV; zpxl-G#I&rqlAgGIOL}Q)SMi6=0h7a9AYT9?wox);Ab};-5{z)TKVET=k`>(d!7isf z?&|o*%kyQ`bWw@VarxS8Tgoey_b^7}T{%D1Ju|*8C@9!z{~8Mo8hhQWbroU$0UTbfdXlbxG8A4Gm-PhXrxPXqLatN^7h|{(`;>6b*FllNS@{03 zSJ)Myl2b{HMRb3FiCRNLO2}a>lMzkX>i%1D0Ct*z2XgLar`$+V9uT{4 zC7rOQVL)i{OI3wPT>g{0y|kNqPX-XEhT3FQ86855ATvw{LuH0(J^qp}fj-FFa#Q(_ zA{)eokk?!M2(e2GmyIv2}5DpQvNpp-sg^` zyut2vfa`z~0xUpKCO}8Lh3?$<=N5U?U$Fig1HLHXg(b7=#1<}GH0QBN3|AR`XyyGwTwg?XiABgoj$PpR+N!-_oG0=P$ z26>wPd8Y4ZU~+jO;N8l(lG=K95VtubsImj&dgeF3hR?e<~mY(_R!1fm63?u zMCIEn_OCR>Og*qpMY)){i{HP4kFZFBljMumf&8FD*afCEzeAFq^dma6#6NZl-lXVv zew2yID8xnq04lz>9Y08qR>qjugX6}gK_dn#J&Ld!WbE<#u!0n}U(J}24=>p8d=aly z$^`i{aXneRBwcepj%i91f(Zr$<9!A@S7Fe{;A@{+ zl3AI9AX!MJhT31`KbS*Lp zK!>hNqF#dPd{Cx;;pSo8Dpm&=qP1gJS_Lc&gJX4vKS`fv;zQpb8GLGKrEXTK3<06$ zm$I#_`2PO1C<2rLUBluW>4AOC7+$1S^Gh)4i_7u5uA*o=^(A^#(v$aJ#vLavU3&|* zWtJWw;3S2aKnF!^o|H7chp6Fh{W}~c=!C&RU(|)llG;z^o$ZnM3tLF2j-3B=24DjL zUXZ4=MByO-1$}=;f-$vd5y^r9g1ZBP7#UxL-%C<= z_#`7q7m5leD!D<9nY0u|;wEnfqP19!wvuVVJ~273nobb?SCTOBZ+_X;C%a`S+ULFG zHAE(-71YqU>bTYlG0!3inXEpJwq6ym%(xW^1nQk2${&oN@{Sn+?lm!Rr9s(Y8z0Mh z$PtDu@1#+OU#j=-;s2p=ImKkUGJtV*N$4jMT)Ea=Fu)qP@`$kUu7&T10U?JiZ;XxF z<7g^S0bB$Qsp^uKD^2oB%udH_uQ@%xI$%zhk>*j>J^W-8m6N>rxs&)uyr@s+aTL10 zm(C=WqZWH!pV$%(I4TeL4cwmbj2qlA(kKQxHYdq#g4@L$!L5!(6Q51x z4kkd>ZWc6xM#t=(xk}XrzimS`JKR8BOgpvk);P^4_yAzHT<>ZZr~HOVR|9-2Dn|0H z!e-=O__uQOr}`03d|hz4tf(8<6f>Vm&>Va0KEMOMqd!DJv)fDUXX-wN>p!n*o_aB$ zlr*`egEDTIyA|e*KvH@5+4fxX^O0i1jY;QTmv0Z+4!iArc~>#(mxZj>xS1nP&vtvO zzto?v$5nNtz(+DPDyFK_{ulC^=?V=1hJ*peW!afS(eZMnZ*& z6F%GXiN%)Sz#*k&;C521?`TO)n242Oef#co>4ar{azbT;EN_lFb*9Nj+Kdx#Ht{;T_THm8Y96G_AjL>WjEir7=+PbH zTeAW=%;p3idT_36bX|Ki^YCfK{vG0~&xMVn=kc8lQ_kW5x~u$J$D0so3qk9;C~f97 z%(nEmD}ZPw**Rj)3u6XHHw&@{Za~sQVt0e$8$E7T^mW~ERvJ))1%IL&8P3F<2Pido zzsr)K@wx(6;4kpTx1)E;7!NL-Q~+>2=j(H5W*tD*C+G1gt{mg~EZ5Wjhd^5*rfysf z<3zWp7;rBJem^;*o=w+{b(%=Mn9b(Wef_lFsRwW01U-KQX{1w(v^_Y|T3jJhqs$YSrv07#CRK)d@ zb{AQ&yFh+0f|d(W*8j9=tAjAv`ZZ#+(ID6G-T4yOkR}h}g$Z502+Y|^IZ%{IOKkGe z{7)3G4BEvNaN=?_RChU7>m~}CeGHj-Ich_r1+e^pyh|2l+p4XIW*U}<%R2A@2Ct{A zaIwS6cUWG}F_|J6lp%d<+- zKF}h1XJ<%ab}?Oj-0y2Hod<7J(<*yRsIs5sMdAwKzczGtm`)bMg+kkRz(0E#CwdV%I?<- zEScV43W~N$qIbW{MB7(Z%xmogy>Wd>tW`LC zk~Ji&eLNv$c_?gtL~`dO((&!YRj2h6JCoq!X|dqITphWaBiyQ@Sa}k_lD`rsKJzOp zA&E8v(pPyA*JFf6h^|R^)B``pjPLy%8voq5*TVbt1Z%ZHQPMXgKXmgl*JP2Ud7XVv zz8es8s25xZYm2-748VEEuLcSzJ1ck!S&%sE98y`7b(r<%)*vg%6m8s!T@T;Sz6=+7 z`a}FLkH)S1{ztUE*06|4#qY0JYkaPV%51M`0!JfY`rDm7>pFHk;g(gRU2dIlKr3^! z@)4~qk&Ec79N8mZ)+J1TZWz5KTynDAc(UNKFfg=iyP>^syc3~yb8UD#=@N0WegQb- zv*(&E)ZIUUj0YvtoJ+jm6?ya;7Ocaa9QR_;semNS4>tu3OOX3e&wyh#Ab$-FBl1iY zc9cvoLhk~HPD={M0=X!$MS=r!$DOK2&V;d;rpErLBAM2@O}}R!M&v$_OMV%Bti)}2 z`ey&F_i*3R3=I>(xV-E# zh0sQB`C+GU5xc;4Uzp_j_!r@z4{b*;qr)^-ULIHMjl0^?j;tP-FHP?lIM++Mp17YI z2_gC}K6BI3)v|3ub)BqW^hZv?r7bst!ckMZ$enFnzX@wadqwe zoyXb!JUfDGCgyABPQ0o^OP7zGdfRjfkEf`q*B|;|vT^S`IY%UFAHrfYH25Ig)WuzB z;{|^3^KTgoy5UYKcsj#tG%m5Rx!CL#l8;XYg}HK>WEJWceE0-KR!uVfDx3~3{z z4H9yqhL2x(&Eyc|$elricK6;Mb!#UfdQ5^YTBo^>vR`&cO$NkA{^FN(BTDIQ;9Bx3WCBSL<^ExtZGvRX*+rsrs>>?j9Mb?eKc z>oLNa%_0HaE82H02yXS>om72w8*;mk?c7jr6-O{2zc+2?qsems3CUd&{6?drhwAiA zuvqFYq!_>p$qSOkeo_>b*y07WD%~*}G{{Jxw@#VK&xxN?sHYoC5m%Jh@YQppCgx-o zH>vtTQ(Rl#HOBkzVs{}#3g^EODb-)_KpM=wFmAMH){os~Z`KP$ zjrb$oOV=V5UiJQ>W^>5+#DP7ls zjXIGv;)hnpY7F~Rnyn9;Uv`9$zaV992zXrh?aSR(teLT~m%LBvwUL7r=s=k;dj7(5#AX{|hNv$^J>z=b=hXP^>z93ILgxBnBlu(= z3fw`seLQ=94<+>6@(3i|$nsq{*SpolGI2`evwIk! zG|S(V@3OV>7eu$WLv7sTcX$@1IwR4;ANpeEmVz$@A#eHYzSV5@rM(O$r}%m5^(ukX z0v}t=t5I{eL_tI;bC$Zw)nV_Xp-ksw-IF$Ea&hXE0iLJ+TY!H@2?o9qs-&*~Lu87} z(63+Ju!1FI`%E4p^}0hO0eMK=jSV|hR`K$YKRYy%5zLFb$ovqz&mO9p7D|28LvnN~N-%8ZHH+>JI3^i>Uu)YDT!4q~Z77#l3+~Oj zHojA%IYaX3Mv#*B_2S?U?3Y@oDq%g=*o?3g1Jh!7yCvz93)4i^k0Wj1mJ8TOl zTfhN&6yvyd@evU^1~H{*jq{pFZRIzuJ40pgE{KYOjSdRo!Khu^>{oYn*lY4(pDtY38cz;BjfaXzzyc( zk*}lxIQev#ryvY_uqT`hu(!e#KDck?(IDeXk|pH(T^+Zz?fUhXA87ACDxg$l>}v$H zO&O1xRO~d)^omJqI1>Wj=8vOhr4~3098b8OnG)(m<;a!<{}YQ6MD=A>gKFr@HsYx~ z`u!w`;_#&%BvxEhQY zxGVXT>OEKD%v7l-Bx!K6VdnFW@05l_4Uv;mV)dV& zAUDvifYrV$d`-zx>~1RD@Te2`(uUr| zr+qlxj@?zU@pK9?96+pe$U0Ykd4KdoD&q2BO*~|)Y^#t%0Tdk5>pB_56%JKw?1`v5 zKJcfJEbBZzFQ$6oK1pO6aQ6N2#HFc;)XL`fAMyz`ER_J=B^+;_QhQbwmtrCN7@I zYO8LfceeHHQRq;OHftB9nD8|&Bs9Trfb0ae7j`cgk&D6br#9t6NcPAW2N*cmtzBbU zNio^m&nGkWS8iR+N zM_AR_6#E$)g0Uv{%E&EzQ=2d=;jHhK;%M4Tz>=D3ef(oxoS;rBe=Fr9`McClB_9nn zRl^JF9Q>{8nhk#*^s?IC?WF^@0)=N}h8<8{1qMXQ;nyQ#V%y4!xSm+Ner4-yb5mPB zI!4=1B;YP)bN`&%XeOg)AF@TPa>ghN;-OmZ zgC3!}PE*j;wyU+{YVOR+Z@&s1U#x0JejDh40eR=P!ra%RRuayB8_3!7`{`kRvbPwC zihNu&TXt@_Fmq^WdvTYL(n;KGW510*Uas|Vx9)`Z_Ym!U+?}^|#{{{`J1VT)^HeY0D-V<8AQJ({AyojXjcP1_h&RT5h|XRKym02h`do`v zb7eA0J6f4&1)1e~v@oO$Y7u!KA_EmNSvKHg2_v7YMreV54yeLTbJtpZC`O4eF}n}z z&_c_MJ@?+jb1tf-!&KaPNMO~6A>4So1D{-6@rw%eXQo~}Ua>zGm!OcPJ}EfZQrsy4 z=Dm2KWe3&Ix;ux*m?vX=)Q2uLGmoWVe_;V??G3NNF{=xbzJ?>Q+WvVtib;Fy6}}8xPG-(ASaVu;Exk-x9y96Z_?n#S+c{QM8N*>=v!I5T@oS8z*~xA@C2Y zzJyn)jO78M!xhqPeLOcLz{nZ-7p$!?%W~R)uodB4C0K<9*=&}d{j$-!i85c-(!M@r zBmVZe?H*gId*#~4?fbAERaW4D;%}e@1)D%F1&%a~40z?xLm`Jp>`H76 zRpK4TFJ%u_7$@b*Y%0s=xWewQyd3Y#ea*vp>^|tyI^_T(&Y1^~Km;VQWAMgXAq`ys^`Sv8-NJaZ z@BEl@Tt%_f7$1qzr>UxRnZIZ70BsdPL58P8 zvbmTKQq2mu-t2zdd=DqPN`=6!d?0@d?-IZsB#5dEZ}n-%DEjtZPV=J?43Cm51@$i( zm!LEu-j`(es#!4e9}qsP)AxGj0K&axkIMO=C#QpE;-+?mla+y{(^AXQEg+TZY;z+e zPAQhzoH8H~1Ra^5)x4l<)QN&`Cp0Dxzx`QipT%!EiYj>5uqRVOM^;5^Ll9kAI=2!n zp^p()Tvp35o{Pyiz1McqsG=AGl5YJ!bvmXQU6%)_v3ne)A= zSs1w(9hFUu9jS1a%oUJ?()wbK{M?y2lDHY#>d$T4m#tU()SP1YOt~SKZs$K;x~7<` zzlczN0r`5U6jxC5d7G*5s?^EkoAk+DS=1Nc;Or;1V^aAx z^udAy7Rz$U!A8HOpWtqYHM2FyVdU9tUz5Ii z*{&em9d*GOR3z%cb_MhvFt97@DTM%O>74hPW_aaSdmmCy6+mm@TaF32dA^#FJ+I(N_o1T1A?`seq5U@5%s)+$TIa9D_T2)`_N z2iLOc5bqywlfJL;{!iHd?be|!`Im*H>M66@A` zSoJUm_8RsYa~1Ob`-osyPU?+XG?e=PytG;Pe4;kpM+oI|a(vVur?%ZdkJ;ysqIwxUa_LIzh0<9cp(s(NIno~- zn{6M`?!%B&Ji^aIdH1V5crn&XH*I__mJaqmyMldceYitRNVD7~v#a(Hyrs(M=~uNW zi7oPo5{w)P^V$GJ`O1ApYbE*u0BSPeBi32S*fc_Ky8Gkd=dh+0j<_5|T1KX-OijV} zqwgcb=;$YzKZH+NrnkYkics+ccNq|UNhRDe^p;zLJGTR>u51pryL*Y=$mK#eXu zogUq@hs^Pg7`X9ezRAY}^%6Qk{_^E1r+flfKKW_62Hz_pfSdA%73r4Y-?1_1!1|XO zPy<$;#~0LXTfEq;RsnYE%noF9=lPj=7&7o2a5GhSNTXj^Mq!MVZeDzRjcUJB>jCem z;<^9|<~8YmRrdzJZ^boC5QbyCMd(!~z>j|to6!JxVh)2CVJ~3RfnfB3r*jbZn;+JB znS=}LHN75N$ETqffKFOax{QbcM`jP^_zF;ugRwm;H<4r)(dbtdJHgtI(2S+6lUBbgm*Avh@Zv_Vu}bPHD@C)f^(Ve z>HuCW2z^C?%v(_;KaHcDpz2eU&EaAjQ#L52AcLc#RSuPK0is2 z*lDyjAOwI4KN!t|ny&oMeG?;3Neea3*;uET+DVQ5R3bcyJ)S+Py`P}Xsb8$c_}6^)eG6;_{|j7HZDDH9xLlx`inF1hTl)3$BC=EAd3JF>|0jJ9yy z5w=b$pMJ^3>2XXTd|q*v&cqImwTYBpUAR$5~^C@cwHRVi%u6upmvuYI#{QDJfkFd-DQ<_6p(?KIB!Pb^ecohf6T zh7S7-oJTjHfQsMG9gO14CRFwb#ilJ=$ z(*3~S_J&#Cb-uYFK4iX@em+UU^Y%wLU<#G+#?(W~jmO`USkPhJwcFjreEO!&bkd8{ zj?3J3b}yP`IQoOs(O{n+wf)K3S7pY}#KUOk;HSzuARD~8Xbmy+R1jLlX4BYo=jDAPgal{q84~U7ceOfC%;gL}`?hfkDdCBx;l8N(=8<&Ezt>nGraja4{C%>0%cYO- z>8Uk5B+f-9>s=6#xf_ ztFCYBmYado&NHn)RtSJedKN4-euJ{^?J~IJa)Coj86#Kr8dwdgJC&Lz<~vIkM{h(! zli_Gm>%ArEn1*&ZokwLtBNLj}^=XZ;HM{FUrL+pX8>!yW zF10%)_GK%QT)D}EbC&XyR42d;_+may-b$F?y~ab2H1&h~_NAJ(P#YeOSB8aAs=w9| zot}>1>R+W5=7AdxY5^$3DG>jQjz#e!mEYcqZ8<96V zPWlU&S)6#vKyJusv>DeZ?eC~~ryNYDiDN7GCD^k8<&v?)^|$6|n6%70G=RE2IS-(2 z*&W~i#%bNCcmL>BKw9gP1VSEMCdl|ML%s?YpdbM6PACKf8h zRomH1C5ESVgr@p;%UZyK>&wPVLigkn=+Q5HR)(tk$2xE{>hscLD&|OE|K>%O$^{Dl z+_`k>#WB%78<>3)r$lREAe{UFf&jk6`Q<_`mx+G>k_AER+9rsw0;C3wrG{cGDN4X8 zfu?qG!w`^-=`C5Ai+xfDk_;{!b{RNViRf`b5p6~bSG z<09FDBSC#O}hCKaI~@Q}*&)gG|J?4I;31as`J+~n;^!*?6M^*Y1?9gXab$oo4iLH6 zykZ6!F8~0Ef$)hRRi;bv00cug=R7t5pI>Z=Fh{UO1S6hk()LbKMLbXV4T{4xN&w0d zKsj-ftz2dO#B*@&! z{)4-FgUR*_{-9)_L@DicN0J`!exe|%I#lDL12d>j#ex`Lg+OobO#Ua(TkgFp?on|N zo)ilr06hB&^B5-fsX;Ad{4J-YTD+;juVLr_yT)&PHW&sFE&%xKy9{(3@Y9>z^g$It z`;a8NpA-0?kZZbt?B*};7@KxL@m7x6Z%|^sUoelj!NPyxC4vaeu0e!m0A9qQp8JEj z2Hz#X@o(THgzUNt$fW*n2-i8Oc&NEg*?*b?i@Jlevk?$g8Lv$nL5NN~h&;^fBc$6Y zWn`~FRwMS?|794tABO!uLkD#z>^wo)p|4nQKu<-0_3{2=1OPpz>oEzIGl5qkF-mXh z(=&HA{maF$6rUjdq5o8yp1E>0Net)wYaoqwi0p?^62PMF_I>FDq5nwP0|niAif2#J zf0Gv$9KHhB;wi^dokUmd>giMz#M*9yDp9D55Kq}z@$EAp^5CJ13(fl{{eR9o)fgC% z-}<27Ei0srp6tT1lbcs{o+ID+X-9-%)po7&v&88`u^q5Wc|&wg#Hynyi_f7D*IRvYtxHlQF3=n&^5ETvoLVDh_Hc~SPk7o}uQw*J#einEA#SW7()Mwo$ zIKuPO%&FCZ&H3sCGt%nE8JdPSE_{q*=>r z=XB{Y(uxUqvFC`BX956T7tiG%cwGqVwcXK~SZ@mH=s&<)h}6|{5{hgY1i@cBWT<^< zyei<(K4jH&Gt5v{0JHmYt}~%uuQyYZe{FkiMLAvNgJz}uidNx^t@*_PheY{<`jDO-X{+{t?oVXV!`vfQ@Oub)+zr`rD_P$viW);dv zKA;7dh2BnJRIOanps}C)FJtdx`8O}rAUYUg^W$&=F#GHG>HxqMfdM>3PbJDSi^e?{ zPhfkb=yx0~X;~Em0Cx>RAD^HL0HfXvkLeHpx0ca>0hf~UyX3$DG9C?keyK_lFpE$Y zx*WYI1i4=6QQk;rP@ol2x(xE!ijndD_&9b4+e^!38?6!bUw|86ai#2eyM41PB!laJ z<7fW`o_2@mSp9jVUo>>e>X*)TjrlnGC%czR;R}1&V|jg>wFC zj*b%)wuQ@u`CvReLkM&d`2zF3;Z^vK-_DUoiorT0&{DU2H5zTm^GOe?V2EM?8&Ljb70Lb-aWqUE zWFp%Cd@i}Y0wQ&h3N4ST#O0?*uE|wN0ZtmH7-fBKzhtSLaFEbnRO3Z%7@Yxl7C7i5 zb`dl17(NKCCI!n7e6oKJMx_H~kNrxp`nT>8_J>y6a#;G>uq@F;Z+I zqukyEhuDjey-X3ReIB-ZwFcaOh33wvrP%(Hei6C}^(2hX2V~o%T;18l28Jn18;Kya zkL3UF`v^!Z08lLmt?7f|7zy6&f0Gpb;#uf72J;}Gvl;^b@dP{_`bZGj`PcY>M5oSM ze-XOLLvJgm5|U+P{ud|NAg~An^4Njmja=L%qV{;b^86Z}uh3lAX{_f!k7%gAkvV>7 zv{fFYVNhU0jEHL2kkmBD@832d-F4lr>1JV3gn7x01N9i)D~1!$*ex zFSLiFxI^d2HT;i!j#qOlEyvJ>)-y8pfKi*z0k(BlR$xuc?uaF;?Bi86zl{rPA6DLY zbD>XdfbRfj^V{V3w+#S;a#?N!U@TPtsMz+Y0Z8P9$Y{&~k8Ie^`APWqOL~y6l(P8c zbeQ2!0EIcFII|!m$PjkgwY1CF##})U%1YE@xShCkBk8{w3-3K*G4#b@fuu@P{%3!JD_o$J+W9>)=lSY zyz47_(%ySNU!G3dggM8Z*aW@!Y#zTc(b&mkkye_BrLUc$0N#kCKIbKLu$kG>c^csjarHlVPjLUQfxD?ik{ zTf6U&W^6#;&zTdA7`q`+{oBt071(rh7?=2NfV$L@-H;M&NP=F-y1l#NmeA-ycWSv% zOin_Ph4-EjAN&PxmwvYwdw1)8ski$T-a2utGdOS*>OHE_t=;)p7F#FD5T?(M_inv% z*(`RO=W%J=-CMUhp3zO5ugg#`B+Fw+!&n{64SX?a0gw~#bf%K5EMO*>f|TjjjD%ZB zywG5;8D+H@n%-EZ(iMVXZfH?HH&GhV=Vrq-6g_PWQ2`pu{yEiEuGzK3`OJdcR{7X; zM~H)v7evDTt~!f8qMT(&3t6XD9{js&j3^pLamO6CL!wXQ(=dbQDoXUV?g$ z<+h7`1Gj2dt)z`&;%tTAtL=TK+TihuThsA#D3u*v%Mx$q3UrUKXz~x=PH;K6#|Zrj z^#c>Eh5&5~Vi{%r9#w2TFxU56XoO##L@4qT2^ZYQb9(_>Qe zid%bxT+5lKPG5H@)BoAZJxHRsv31<@YW<-O*__)I#B9v?qC%IqG%^LisLT}YFOyaTWlzO7FznDr^k{b z;KVTN5zj9$nC#j?RB}r#y0wbH0ZBnPG{y`^Tvw7y>i^+q{87ERrz0wLsn1Y5#LQ9QL@3 zbG-KkEB%UI%7z^u?y_A@U!l{hiQ+_ofde?OshMCi9n`-=b zga6(5nEjsJ+y61U{~8{O7iX|_icohq$wP==rKH@TKarM^VNRKS%lQ*4F0%-ElWB7c zjiBztPpitF<5WB2B~1h^lSA-)9qibXW=l|MDId4$dR=4A;W@MxyEJZ9Nt+wcBqw}$ zb~xdUIPp_oPiM>w2Zm|EMYC~7VXG;_Y@f+1CNCt+p}_LEn5FpNjny7^(3%i0E)eSU zbEhNJvTB9=v+_`g8=&Kly2Nu}*BIJu5Sv#7G$qP+(Mw?F<#MUXf31EY&`oD_4kb2B zfeZmxx=vMcb(WLQ=5gz*)3?aSgTubGz_t+nepqfCF~t{e6aQPrJX_$XPr0Zrx9nx> zQ5(t(0oygZQOYWZ-bhFTfbQ90} zRjUG6Y=$8@NDdXZ#Is0E30(ddkyaVR#i~Y;)4uS*R$szqs^PFbI}t25Ic#ZMbQa&g z1^;wn(G|DhC`2^qbF^)$VO*0@DwpICXbTkwx`M3}7s94b@BbNm-C*pc#;qw4Z)MF1 zb@dE3(#?Mb8wdW55p*^Efch@b2Q7|MTx%C~^f~QL_1`KS(jbCtw07cHZk|v#O1zaK zUy<_c;9)BFmuOp3*%!G7`<&~`ocnvUs*n1+DtSt2U;K31XeA8IC?$;O(<*LHcUjpA z^1E?cTAG%7C%v>q<3)p4i{bLHNv*{TFMH<#f7hd#%9>kAgaVJsHkS*vOqLJkm!^Z* zU5%&IZm(GO71E>%;)d6Wo52tq4sIpFnlw&RW6YmY9^O^=_5PZT50n&GW@DG9NMH=y zKe8^-mNq=7yM$pc-WC7iH+8-$Q8fJ08|8|5PTe{dTQ5=bSbvnt0kNJW7=%x`SZ>*z zl>A_{V!+!~3CRw+aFuw!SbIBAY!}iobG%NtQL`k>VedU;blsks`!UJNoxU;ydHE#F z`*R^$rUZg^ozyQDL#A11D~n-rJaU?ao6OcL$CMkJb0cAb_Rje#c#8#Srn-Z5U)sxc zE;fBLUOhs*C-dw}%)ac}R=%?7)Mn+^S?o)BoQjfbGU1K9!6V08PY>R!538n2+}{l} zJKk<{ji~mEVSQ$G()xCV^!Vqhf|f}B*1%f1={E+0CVN717O$u44J{)k3!~;U?$Ui# z1H~dee5%$dmGeb})Pg#-fS{M6ByvJJOIsry$EzimA&ZC5$xPZLQ&cnbba7BzKZ8BS z(ZZuEj>rUpl4#g;ej|43TRfxZX!dIEIq`b|OV?nH zOTmUGs~S0RcE@{_5nt4o?+gme0DGN%mEJ(=#`s5!RVvRFv6xQYJBKfGHq9=Tp3 z=-b_~94wzwJv{tuA)Ce7ij~qYBc;nM+fF=x@L96y#$rq~d)4&&$8Dr;vebG`#1agM z;HxrGf_-ZGfx=C8Jd1#i*=tDH<*8QGwgcz(y5rPs6YHL-ET+Q)%EC* zmt=Ulo$U@a#f!28@SIpE*}$MKOniOnVzMaCr1IDEBcUu+MBX5Eyn}R)(Fq zYFw6?4N7MSUJgkZE6*=hY@aka=sPWEBrNCr9Ml-HWGXBxab>hEX;T`=RJdNmxTMiw8_m-R;@3ETUqTW zRasdwBPfZ5)b{tS5V8)Xs} z3{NEJSX4AJzpy!YVTrp!%dK5giZx5mP92#qQc4Y5x>-1tmN}H?w@neHBpUJTIDUrz z)qSAE3Q)kPuFDMAfXoJ1Gx?_`BX?!-9@MNm^-do;@Qs)Ab=msHHh81Sdio&O%<<0+ zO7tC#0mE|%I*pT?-^R&gRo@4o3iNlz##IIPQa3K@2)kuJ4v{~I{KVZew|z0TbiHpW zSpFbA);pt-3$uhB>+r)6*0fbzIUL`0AvV2pVDP!VYc#TXRr(bY30DPsEd^y{7t|Xy zoLSCP{p-`+^2JTbgI{axE(g&MD3-46Xlir2JukJhTX0uhu2MY~#6*?Y-G}8DqGhsZ zS*{e_L#UPPuG770R1RW`Kf*!t8Wcxc^|feeN({9oMwwHV&$xMb?XL(QPL}y54V8Eo z;A42c_b;a|-;4nUdPY`rl)%XkbJrkcZgJ$RS73jR-M&>wKhYV-I#z6pf0Jclu;wSgO-p9;XVHYBgtrNO;4etJYDjA0O zB>4)JY{-_30|YObS?<&XRtoYha_%vN(FfP2K)VDECn@5ujn-Y_>k=m)pOp`<-C@4! zt=#Y4zO2TS`S?C|m0s==%&~67Fp_ZA`_N&56P|p~HFeRw6%I+z=7AhFRtN zo?-OD!e1OtK1^8EjWt)am>9U`6y7qJDv64p>Pw<^(#tWGMJPCc-T1Q11%^{eI5qMR zpA=Ei=cvPe7^E7Vp6N;H+hu+$4RcCc3zDTY zW+m9cQ_oVOHhAsUwCO*d@|hW2ct!Ubym81qr~-xwDjGL^i140hRz2;A!MRz^#vrK8 z&n$?QvB8~|=0(G|979s9$}+&fWuwFcz43pIRq4U;Ry9rn+LFAm+TaGZj_vx~X>)oU zps80bVAhX$X`Xm*+%q`tQm@$JDO-8mII5-e z??H_|1=ll2Q{RU!d;tT!seYhWx8F0uYk!L#ZR%v4wbenTJ<1h4I3Ow^m6J18Yt;Bze&ezYk4Cbp`zl0N`-a)eDp9rR; z#WDWbuXe)0a^Xp7$N`PGVSt&7Cry?B%?%6z2lOEnu;6Ow(*RhH1BUPRe{}`MjzV0t zXr_cd1%WvW>O#x^k})yRvc%N19yB8~Kn$XbKN0_G=R5-{q(h{^S_^!9@)f+p8Ck3( z$-mzg`~@ySPzMUk{5rh<_0@S+@ZVJ&%NKu*%jp+S$KpwUGAdz z3JtEqT=#keB-rgbc%1YzE7o6q?F3_+$ZMhC1BTN9$bC5;FEp^Ap$70NAO?LN{0Zv@ zV%=-4%l_*gD`K#V$mBJsKwY>19(_T@)bsaUr?>t(YZ~)V;{STouYV%sL`F|~P={vC Tf@a)U;73YaMl2Kk~I<*m@20axBdG` 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 literal 0 HcmV?d00001 diff --git a/website/static/img/block-subscription.png b/website/static/img/block-subscription.png new file mode 100644 index 0000000000000000000000000000000000000000..b6bda3aadbfc8f9b5c5130f1ac37885f5545e80a GIT binary patch literal 88912 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

k{llv~mux-1rT` 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 literal 0 HcmV?d00001 diff --git a/website/static/img/logo.png b/website/static/img/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..966973fdd63b6cd8d89101687a18b578401a2ab4 GIT binary patch literal 7522 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 literal 0 HcmV?d00001 diff --git a/website/static/img/workflow.png b/website/static/img/workflow.png new file mode 100644 index 0000000000000000000000000000000000000000..e2d2f0b47bdce169d42e452e9349da6aa11e281f GIT binary patch literal 256756 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

k&NVSI?YtA9d;sl zr@>&U#QJyTc+v~nWP0C;Uge+I_cv!6wwH|>d(%tKqHrqa+<0u=*&FM#BAwhWJl|DL zTfUXEHE%^H>{Ne^`ett*mK0V(-P33P-jBE+KR!7S=I%}WE{qI2_+tL)D}juzajsts zF8X|R+5p?S?6U)z&ZI_jhsbi)Q-A42UFHt-ul<0y;AfHC@9R{52k|ipZV)NT(|#~E zGgxHFr~S_r(7DgT0uF*Pxo4d^S7#wu(`apC8hSnjjQxDPV9S_nqIF-C$31ER%^Qsg z@ty)yZTm*lx?B21)?&A#vi2U7WPThvus~Li<<+k-Vi#`R9)v=)$~4vPn{&%05%zHY z&9T!0wLkYP#q7#<{nnhyzW0b492O?QtYwryBnF*0b3iPwd-fa7ukQci0u&?*iAejI zrq-?{_vl6JF`-NgDqB4`85Yf5J5b{Z+}ZaW9-3LTBQ$Q|E4?Qf zY4~cwWCOPCI)kKzYVL25CKvy6BVt94x8xDo`+i?o*`W;{>1zSYgI4l(1;v+&iuJ|0 zvni>WJHkdRFEp**Z^EO!qQO1e9-XiNKN2JQakuWYYr2KbdB4y8WCzuezpaBDU=z1* z!@2pSP5=~q$^jHo>s_3Y4sSl1pOZ#!H-vY2q~@4J*x+4Pc74B6d^{ID)KS-qViO&? z!WG6hzF|g{&ezyE67`I#Q#04E^rtvhYq@dn1gpj>d7)y^Bq04`G#cGaK5iQwCFu#VwCyrBi9|bFHmWM_lwQ+sll=Qc*K{yt zl_6#*r%ro9$u7GWHT8!jvZS1>;*#*owAT7gowIfuC2agc!DsO0_=!6_^WXKO9g=J= z|GNi~3WrT7LL-WJc$i6*60Ux-X1uOnxKM$TUg&@~`n~9Y&^sXVRVXPfe|`BX_2fR~ zVXpR%_oO=|y*7m66l~UBGX;gjxu$(Lh4@OcMy)7+WiH+A*3}K)=A*bFYg+KV!eTc@ z=PCZtNoAaCq+V_}CULzZ8Bs)2L*7Nj^udVqAV<3k2gOZ&64W*LP@iDuBDjhR=+s~d z;UiD+=W=r9i!59|;JJ#}0Y*Dbn=2$Te|jl@>*pPd{!r5=n3Uz{GtuHQVC{+X{Sd8r9VKc zunsG7-+)i)ubUiZ;ZT^pU8XaHYpBoctF|pLg?8Vu|C)x!Y{S)^3D{U4-4AY^+fukL=}VkdwocjkT=Ew{bIiR(j@n(Uv#Qd@U@uY*+Fl{6M8|@0?G~ z(wR1elgDzOx4jJ6Ccn{rA$B`3@FHw_F8Gn6#7$QDLw?ctqck5BRkinh^a8`#3oVvv zF)GKE#$h&x84obC^q!yi4ka&gw@Y`Al|H_vA8+aM+LHd0rx0n&Gv-?^7g-9vc3h{I z)^0an%VyY9S<$CjzjBJ21M0F3lU{s?VO`h$VNYE>a@R9@Y#qtgKEr%NH*Hw`qYE z=38)Z@7%IMInURh!rS>G5K5B|?f=PEvZy6g+`NbTW~{Sfd&X%Ur40shXj8d=uQ3^j z2}hew0vPz;?wgMieCN56ZuvMT2dvX?Ap@a`Cc-w?`GRaT|O1dcJr;t z^Ad8L6|Q4kS?(-d_N$cRyZt-0XX>EkpWa zI-HWveoVKyrA0j{#;+%Oaw-0rQ6$#rvrR`HoCLzlEV9Y0U-^usNFDz68V2(NxeSmT zl*P8y8oIWV+nFfXOIqfvu*VW{lIP9-F*AOwQaNX{HlTm4-BIb-=BDfM;%TtOw_9%rzd)}S-*ybm`!LrwiNLT)oIqbs z68hyg-{zn+B1x*wsJL=L_B=a=${&+k{bXZ}UBMUa6p2b^cg_plzRjPSqG`MlBvC3e zOg6`fV_TM5Uh!DJ1jYG&dsV!hcIBBj=R-0}TZdAg+#mP5y`DGKQSv5bl-^41UlUQA`?4@@+7_Bvig)9IZO^N#G3jKU_=S2>uW!(cZVMg-Q&gTY`c)>ZCg2jff! z$k-%fvp82jRGIMj{PK|dv14}%tvMY7!&#V^9EX^Xl##m$u9jVgBwF6^S8>dNPY@cb z<@(=G+xEQDXX8Y4b}xk-mIlscrO3%YnOzStt|Euep%|dUA@*M(j19B4R_@iNq3UKt zo7pBLS65<}(%MK`x6W%P5dG{h$`qmD{ikU`|98D)queEPV`z^P*+x1!167hEC8!N{rd_=9<&i?^F4fFA;% z58uGwb2)y|haoj4N_CLJM)PLzKc)VX`-0EQ7Y?-B!Q^RWev3ztfde>sK z@=-(&;NN+v5Umif>~s>)O`LAJn3)o?Y_oafcMMMj)_hRO4et)orDd$wTiH?Ct_z`d zx{W#A;Y0glbB0Nc4F+b(ng%I-Q*LS6{r3GE%Fm)Hi%!BR`=d8+fZ3V#TxWr+*=_dr zy2?-2^py)@_=eUyP7|I_FmGr)fvpu5>s4={Je!p+^Ym4$3xJV3*(LWcF-}c%p%w?y z+@xi04ae)NOk$%bTHBZ)fgl_61GB+ANf{B@2p*z{wgyNakd%@-I|jo6oMGD`Q=R3G zWn1v~h^OaekO{8?tuQ3sLOd#do}8p4py0Y3VK6kBh6w@;djV&ztNwVNo$L~U53C?9#BFM8$SZUTouut zHmtiI7a+ZMrzx`nKP+LVFfT9q+Ib?CUvDJEDgn6!rrzuQjcKD&F#dtruXw^sVHB{s zwx#YekvvdCa62x!aNhPs5dWtPKyK6>2k;I*MOfI(ZX-Ra0jqoU{A@x&%-xv zGqvn~tZOI}w+Rjj`S$%gGS8(Ttme`?B_v@UgBiraI^!(WivVbb`jBM_@G(s2R=3-J z{{|lmjxnk}|BoOs)XrB^EqCMwd%lZ@&eJn9;1x)+WLsL&HkdcU0Dve^(sj8gdaV#c zbpTYu1Z)@qLM2X79=cLE)CLy>E(d_59N?4y{w*Ldz6`=srQ|hcs1Dw6?B~n-r)6h< zfx$H3-m8VCZ-eZ?4;QX7+yrDsY-yPzaG>tM3ScHOa&BmVVD9@cWE9<{fL}+#coFax z9PR*ChxrW-5JRBv$#%R11AdnrDp=}pB*F+q67;Gx0m&obb>q$>+%sjo(?!7@?9*1X zFA5Qde3+vE3w%3z_r*x%ZNB2#+n)^Ntz2ALXOl*VUsk(T4-9I-_yG#gg0_+mPNRYi zgih$D->~|HTybt2$l8z}kS(0kQDb6&z4spLI@P45r+M z(7*HA0Bh(hufnjXdfeg*AlxH&+nbwjv$D#SJEVgvLY-M5@bMOSf3m{h5IjVXiVjpZ zuaQV`m_A7H;S9T;9wT|j5p5)2oGAotO~kMqX}uKL2>YP2i+|ez2AW)lFul6Z$XGLP zjXzvR9E|+t5zOyBz+niU0vI3NftQB^0RBfWcVM-D1?FS0g&r3u!y<>}2Qv~t;D36( z9~>&*Y!^KCubo`ahM=i!vdqd8NBBt?h`KZiS0nox zEi5zJ(2&yIMF|?lQz=7avZKa$ofR8&Z}@9c8_0VBY9?$EFyOijct1I(Gv}FJ7b2ro zW0)Pf%+d#KCTMqLB47{=gNe!$y|-_H zax4xUCTJ|QK?Dq%Wsu6B`Q{G8I;gy<;reuuPXvc5&89}h!3 zb__U~{W2e|pr^YR7#MiGgL?V$J@74ngm9P4l!p?GKwcc)TrtkPIa&Q_(*g5+bEJRL zwiLTS{AR(-3UZ&}1mZCtp}K-mqi{U+9z&+hPa^K$WB#%6h)Yq~*jLX`7jrW>r*)oE zGPNhz`J~~5*{Y;BVtc;U5%*kqHOhJF#(Adp)a`piiS9-+my4Qt=_bC)aVwv)X`eei zszg8NuXca>E|RF%ZYE>?BR8g;!@+H|^02~MED97X3#>S(52-tOHLOcE-Mfiri_(l# zHd`Lz4*C9`y8DStfA)?*yHbT()dlnOWtfqkq~p#F9V6x;J+%R6lGIN=}=Dv*}xSIIl_3v!H25+AGGn%-+ZxmDgM^Oz&a`tXZMJ ztK1dapsPZ02T;u(pOnLM@mxxZGAi;M@Y=Se4#+OyBds+KlCy#{g5K4v;(eD=0Mk~0 zQjSB)A!ZCmXua;bizNNW>MC#xS;UCz@I)N@KjfDaPv}~WyniRcD1gkg)%ZuE*{g|m z!PRqPz&s}sVI4jOHyzCCFsNamRqK7JeML*i80M+f)9-Y0RF2v+sMFe%08ogL9Rw%m`YNxYR zbT_l&U_pFYA1IS>LLzPfh&chUvCn=BXliPL-uuPhX}-WOc8z9*?`qwaK*%5|#b%y3 z0XqS>GHt0x{*Ypt-MVaRHCBZy0%lXj z$*=YND;O~&mY1MUzicdkdH~G6iz-QXnSu4Yyo<`{GKMe2r=&bPF8n6lX_R)Cd4Zlv zJb)%+pb$#%wrMplHw&$V5@~w*XUhxM+fO8X$-2gNYP|FxJm*knLW;Fy`lX;j2Ph8u z(i8N#D9-vP@V6^tcO^}Mvp^;67zAPqq{gVEW&89vOiL}T5{BmsPMWEbKeuV{P-no2 z%?f;DD?5D0Ydv`TJphq_p*;9kX9*~pV?R0T;ticbDaakl)TH}1$dNCrxKE*7t!BSW z5IomA3$C0vn9mg%TM0ygvHB-yxnMVs&VJ1tw3|gBi-1!+>oq92A}3=gukZa`R-^~E;#d)+tW0{nEUpPl6; zTBru#r)al)?7}V&Mmv(OdQfQD=IRywvnTrv+s^4O|9E2x+vj-Z&p22? z0D!f9YN_?9t!3!IeWG`awWazyjAU4ATDLlc(zV&TQgXk`pATC$<~$v2C5UGwRJ8!N=n zPD$eAC!wUEYP*HCuq(wPQ3bHlY{Im%7{K8$pt0NNjvh3i10^<0a>CmPO9Z~1F9(t; z85*zNKCn^gotx!pH0}wyYp%O93fED5l~tknS@wYuUY}m!5T&#uV|e%t;)Z!^U>_YH zz;P}qE4w3D8TU#=F7Mk~+pVC@Y}o$tW#iuYXDBg=Cwv8WoLot-=A??t<7G2nf81{@ zhmH#@WXEG-EU6gIL?hweEpy6$((ZtQjFhkhxXc&ep& zDlqE0OXG!!nr2_@Jz_k*=-6OAQG>@SKyAHzPPn8xg0^ayRJ)5d`eG@`8&f2kRhk}( z@ekeeb(8Do?$QQ-wS^uV!F$E_wi#*@F^dDmR{9^8gX-i#g$JFoeYqCw8M5FM*x7}C zKnDd@P0gb+Emrsz^fgfb!`5TCF>C46*O&EDeN~ioc87X&KcPs~a$}i{r4RKRe`Uc)^u)=Z*&yCP&X>N`OOSjOLry!sKdMZ@(fD&?5 z>gR_%vu{(EhXxn=qG@PeZrr#5+^#jpa;&IXZrOI&s(|C-?UiNvY-hfqq8pHh)LjzVz+fFNz%`w{KT4(lDQ9=JGvk=qMS^rgl${REl z6J)n3x+r{eDerK!`@7r>vn2U{u*7conZ9A7w=vHj3BAs*1t7<>4>-w9t*v)~z#q}( zUakGlUy2FzXh1B39Tc$=vhmJ~xt6#Ru9c+^3?kFvh~Mjnqo*RVdNH0R7+J}wB&XFW z_f~~#oO+hE=E>he^xjN-lA6b45q7rGUZ8Hd zaEJDIDT9-^0j=Gv?zi%5q=) z0t@gvd&TPE^W&15ni@Apqd0$|S4npij4t+(EX*##ayxSA1hdjAa9im$yijN|FoDrZ z^H;<2cmvG`k9i1C$F<3xv}V`f>HJ=qlY8>)M)`^^!=0se+HKtpM*f(4*LG=Tg^XW{ z%No_I0KN<848qup%G`*u8@XUGYg-CTUpX;!Rmh7{me)#3A*1?Yx~?}8Oz!k6lmce+ z)Q7|pM2}|&S6}X}mnYm6dOU3{;N{~Z?D9D`O}s_1nuiBuW;mMKMk2Gy~;dDoY?n+r1qbb zBNhvXRhOawccm-k#rdJgUj7Y^{nVzrFow7&Vq8REcQ5G2tD-w&g9_!Aw)~Mjd)zvd z4FjVWmAkiRdBvS0KQ{~UxqrMldn7LEVzHGsV_p0I0I9x5pX>#FpxiVdvcuYt@tQ5)LQza`sgvK7uqEQwBY(_fE8VcZaQr z0j)NAM@ujL-3?;CKuy|Uz-82jRxtA8r}B5Zul;x%h-NqyS55OXRC2)Yf3na7@CqEE zU+fr&^va$%e9NAatYlD90_b~_AB>+31eTF;XvIwlX)Gvi+&-ug6ZP{2Kg$E2h&EPBctrMVX-{yfBKfV3P7XapkE&NS_@dg~&{)WF4mZV`@j_Pd zIfI?9=Q3!&+m@%}b-5u>;&NN6@v`TEL>HQS@O;;YE{M#p!Hw_5&homwnu)i<0~=Sa z7lLLKkM~*RcgvH@LLryv3E;tcxvHDOYAPz8STl14>~9!;L#*8Ijt*i^;%UC%yHOi= z+}a{|5A&hRQ&l;<^i-;12GD{09tZPh*zh1=kkUzoa4m{Fv3T_n2)PSd>YF^HMCJJz zrMGGnQ5j7m$5l7ga`yZFXiY4AE;ob1uu`CH5svuqw~T#I@cFKe`q{i?fJqfE4Xvx; zl-&A7qPl_UvBBO0^iPSd!NT=RaFEFVZ=4xzTE2@%;@}Fq(yqyIj(;kgL=~*m1O^zE zyj*0T2}v5>Vh@vsv|M(B2Qkwvp+Mer$C~Lwi;=BdT< zALaQyaduT^k~SU;%2gPLCDaETLwSr4vqU7584i8GwZJ;`8eF|D=~me0azMjTSpZ;G zJUA?uYjxZS5IVEBAUHEMh3tMa?c30qr}8^8kGVAZ-FOdY;z#${OAIf18IWs3-vpYKeR7(L?*gU|Siu{h&0h zfBn*U8rpOKgnH}rO-xLxieD6{D}z`PHPq9iP`Rwe9HMtDWSWV&_h$$8rSrM)ei39q zYZQo%X{%4^){(Cy$;Gt)HwE$uHni!@tLr^}`LO&7%tvKG+SSPeSmZ)V7XS`_NsWmI zA8(awji#W%p)U}3wH#bYUVt#R=?&uY;I=9VYV6xEBLiLof`V~3Y&!}ULz>*`kT#n^ zgU1F19HD8eIiLv&197)ohi3zg-D9;4ZenP6=-Qpw{!@lSBYv4$;D?Lw_gsw>99zTf!3i8%v|@n;&;n9II8B+vFAU3949LhE0ry3R^gk zWQT?(9T|E<2cwYec+AGV7TR<*xq@$TgiJd5abobsrYdWE@ebE4gUaRF?CLPIy3 zMDGZ;|D6?7OW3*m&#YhvYw}09P;?AR*7XSQBCPS7$o%?QAhzYXOVFZkxB(rS$Kc&7 zu(UlRO&>U8{T?ZXz}sAYk+%VGcu;Kc`^5vIb0Ey{;K74A&w2ov0Huemp0HoS%5Osh zZ0*J<@f4hb9-8ItfADx z?dDv7YHLOltOF7joxJ;QVsp+?ms%^)QVZDqfZ1kzytO@UdBye9olTKp0n>jrIkR*$ z%`QoG6U%SdvH-|3b-c@+R6`tCtzGqo1NWSaNS~~VAGKgX*Wn<{HQ`1=DFcNp?Ap^{ zomFGP!`t5nDwKbx6)&#yLTHI7)W*;uQvnGSB$+)t2o*P0IM`4VDlwy1De$m>nn)`v z8J6{r$v=)RKsttp3`91SFH^HB4t}Cycma+EjY1f=amY*oq(14;z<(w@5rbJEBRPLJ z5N5B^P%$}{Yhhgh=!CzAJCu&)VPKHJnt{hg!r}$|ZjG#$cUMFs?fZbHR|Q2F6x~Xw z^APF+qL-;%0pJN{L$U+F3B7xR#a0X?K8Yi=9Ncd}8uh(R@2_-;=p>4Tk?}UTl$B!r zdyNu-*^6)$5JD@++O3<15SRiy0Ooc_1|abX-iAw~(xGqxv>4nC!Ch{C)b+}{Xx)4@ zT!;?V2K3yh6t;Jmqri@~;Ka_)9|kYQV^m3Gn2MpQIV>|y-K0rJ0n2t%un_?0_}ExC z_KiMr_@@bGhxAYv-}Wtt3>(_tn(BRhx z37s+SFW*-xk4jm`ST}XL98aeuMCfcMUuztm4E(^lK!E}YRso=k=*mzZQwX%le-g=^ zOd5E=DLPUGDAWt*sh|UM7#QNkD5h90m79N>u@1AD%*pJ~M>WA=(X%VCH(>}YfZ@ZD zoZqyP%7D-B2nVH|1nTasCQioaMS9Ao= znlEKFg!c*1`8RXATXkB6EP9_YFR1tz@zw2Felj&HUsU7C89N}-0U~>IyL+x3LmTE5 zSH9kkG=8b+ZGOPL+OTFGk_oaam|(DjE4No~;!v8;bPL#wI0&r_32RS}%&#(4j0;M-8A+y;M~U>*YP+9NAD)(=>H} z)vt?MD#^O_qlX*71%;VOf5Cag70lCCF3JApqr9KsfI5e9CQbj#s!@`Neb{pM-r}E< z_oE63$ODJHx;3o~NqXNKX%t^7`^4NE{!n7F{wb`IqT>3_kohKvGCoF_m84cbd27|E zD&=&%dA&7YxBi`c;1VnSw!XotEVI=C!>ueP#snc<`@`=G+$xbanA-a&Q@`k0MzU$! zE6VHlU)pyO9jP+eT@w+s|FY0Bp@yy;x7W#v*wQIS2KJRxdEX+10PJ)spph4qBCHTh zD2__D3*{8<<83Z9i_gVgyw!fh)s5`WPr?xoiz}pw8JJ2?3^5~GQbPL z><+xdBZRgyYsU$m;Wsum27dioO~_gn8{Gke0HkqknMNSYp!?=SUURE*R(hitjg-{! zrD7{85z7z6!lPHI(b4_4*zZcuHLA#?ti@#f%<20VDCYiL__ewgKk4LhI5heT(#=_l zr@j@4P$8_#JYA0Zv@7w7)Q1(3-J0R%6W-y1^!#8LzI39xN#`#cBhm(T7HyE{28FC0pRKfD7-rbA?+?(jYBUvVtG%sc2K_m% zSs~+5FWI+0F&n<)GJ|qHge3ZCmSk6b*6S7M81)M0KqjWkS3aBH_GH2@beWjz{A$kE zeR*Q3B%ULLADu5-zUv2+e*&ASsbD72^Ug7eVx8w4*1yUlhxt~&k3l%baYEW^0i$;! z1f}9LPQyPx^spl^vBN<uk29k|+SLc3`{@;s1cK9#QGsbbn2bQ9ZKGoPUrhlS8+AMY z#RK{=cw=Gu(T0E4KdeUf$-HBnRas!qcO_QOFc~qJrHwe1U3Xf|wQ7`rCFD_gVncM{ z`e{^6@3he7kxRQm?-lB4%HE}IU;qHuXr+?cr4QT7oP!L5J!5$O);lQIP2t&6Ev(59 z2b-?@t??iI`g=J7tTEwaQ325Q@ zAtw)zNj)&z2E9)B$+4@@%#45Tj4k|aT(5sJQaOy}y42L+R)jC+6{rspI$^(xI$q8~5;bpUWXNNR!-uCG9O!PJ0lx#Ct{$o(t(B4W~i5#E}@FXf8a9hxSRQ@Pat`*CZ#i%7df* zb^Pu9=cc@GkAdK*>uBQZc$S7ii-S)$??3h^(1^qX-r4A05--WJe0E1JEu(XHSzp(m7zEYp+ga+$oE8EM z8HDPicW%%Gn3tGj)KOMJel&3vkPMDO&KqYvO>KhENE9?1O}~Fv8RbvdmO{yM%bejm z^j6evAgH$%h#CbGeoakfrJYG)yNZ(o<0`umifgh%pa;i+v+8D^-^SoQFS%Pa;8-UE z_W_FI$qrX7f87qrii3k2U0ni|wu-$LSc^IGV?TkXc|f4J^+XquOFc;wuv)m=#qMYn zuA@Fv%?{f|vj0EK09DH{_EFQpm{@5Do4ZX0-)hb3W(nd^1KwN!jIs;M(JGorzDm!j zI*%K-=|o2eTy?Toag9pyjGs0BG|`3e+|f>4ZzG(0$NqQFD&}SsJqZrt?e$x9!u+_; zl@C}&T`wn{)VLOtT0h?Aqw$aqofc;;Icl2O=F?QyO}F}oI`U^(TaA5RqjAWiqK;3P4@y> zUz+hHc45b={XmLEf%7b@BbvwC&c=w7HsX{RCHUmAM>~<|!zW3Z>|5QtQ@^?!A3c{n zlR=^HeXoq;RiQAUl3?M4`kY{utLlJgF{29?1hpDORe)gv({r#Xf}!NQVjwrYyzVRm zs4sy2MOd@8mtUKIrfRs}cySwQAf#8;BmMTE@mk8(Gk_{3~6(kg}g* zArOTe=rCF4k?oU$PZwBsrL7{p|SAHyR%}-Pz{qjTwnC(d&EAt(K|_ zyTSn?A4Oaiz@>tc*X)vUc&|3h&w#e@8h{!w>%&V|PO_zDuQ?UWsU7c>HiAsnQk=VV zbhz2Br!F?4YTn-AJPT+rUdEA*AUWCusWnYPmnR^FKmd~tZ0y5dl^Nh*7cg9hcBx64)OAax+ z0;iJfsfA5jZ$T!6DRbBaDkyXnKyo@VV(go-7T^x_Qv=s+VliM0$|iT4YeL8UFShv` z;ziFyANK18;4SfjkrnhfK<9W*dp!aU;EQB7z^?NGcZSp$HH6c3ZnnsBa=|nrJp5u6 z&^B;J?XLOMe*mpEkO#9ehL%CkK@0sg*x}V0QPTcL)oWOw35x=USZK|y#2L~##I&R2 zlDmPvPB^dOxswW-IMm5TsL5g6`CS+8FE(!}ILCTw7q>U&lVje^(`go6dGjcc_Ghem zVUi~1FLz!B8BdbzBJ)3}peMH-`4w&av*Z+ckA65Z!}J;kQXe|bB=b7zWKlvJI#Yep z-8f>I==#CfKBk>&E0Ue zR%&pM2~H}eg`W#h@DCpAoAwveZZF!AM}k7I@PkWq`RD?2j9$GE!iti$wJi`ZT>#V@ zIFWaNy9w|=yIw1qxIdL7@LymK2w(@Mv>YG&pAMjv<~w51;ITTcZbPcH!*2J48e;{GxR&M_$w z9du)?jokz>NSO19AbjXgo^=2r@IM#4q|>-J|w> z{p)q9e-LL$cuKKm%7szzzF@um_L_0OC%?r4=8cf465C|j?%4m<)+J4VVIpDNCxaZY_>Z$D?dWH^S#XI3VX5@RB(k|DPYb>#6QkJ(YRHJtty z7R-L3SU5xS;qE)~Tj;()B|Q!%EEhgul+K1g`1j9+VVz|^*e{Vj#M6luouit?Z>?gh zoj!CacV6b$jIgS(nz6fVU9owLmhul4>=(iXa|b~e0apq;?0i69A5X-r4E@;BTOe~) ztlK&01Emix((jY>E(16KpqDdemxUZcF0LC;P{Kc2*P|S)<%*|eEKsf?Vk!FaN9&pc z&TC#zXqH8zVq!X?Bl=DsJ$}6GLhfDdtE)sYXL7jH0F$cguXFle$K3u(0D}Wf7Aese z4vr#4fm&Is`HMF-35U9_Wqtah)%)kqnS3=rBtQaE(mJX5Ja2L5XCKQ^HA=3SWSQiw za5nADyuSWz&xNF^?CrQv0r6D94=(~jGWQ)|g|L5Av&F1K>#<;}*Lej5C<_r1p$^j$(q@OQ!)_{uU|y z!O-VZqb(wJL#O6P+@^+EpV%OIa0eyvsA1UuRzq}Ku74RRtx0Zrc5(8K#)5^}&UtoS ziQA%%msaHM*JfUw-&_*!Ry?{APJmvtSzDBw%;cmapqQQvBg6Hjr1U*&vyvLSWHf?X zH9x4gL(zkYd>-~^_x0jg3@12B8~0^u`T%|nkpER^;J`;~$qI}V!PKD&&_t>g%X=g~ z?0~O>7p{Ls|8;_M$aliIjhE<=>kb^}^WYJ>A_}v6aHDtx5l;2}z>Y&ajKJU-B4U8d z2XsM~Ru zmX&QdJ?c1BQSjS&!))aYR%%EvRfLfOcr!uzS9YUksv7!ae-=ht)881I;bQ>3;vDz` zf~_E;NV)#A?ihL%9B>H!>BY`=e4<)qe6MrxgE6n!m}ukWu{nNf7eP~=6&hOa@sY>N zW|Q4=qV>?wuAwSv1H>|k2M4uZj<9{|))ID5AZB(bm^`IlB;=wX0!w1}2M|{OmJ*K8*UYh7+NIlEhZn3ko-T0`B_%yr+PzNo zlWIkgsJF^=XJ)`07Y9@By!N;p$lIYh#_JM$;KCqu<*o&TD~CI6s=J_nUtlR~4(56+ zI`gxoM=nZbu0=Lcdfl2}$A-NFXKQfV_sXLK4sbVv>Biu)RsUBoQ2Z{vW1JhGkN}Me z5jY=C+qM?IeD&(jv^^{eu=Rn_y>+oL4ur+MyQk^^BNCQIYwqaAN&d3R5sDVy)j2X% z+L(3`38%@h<(BDp9wMkePJjpkJhf);NY5f8^~unMzG@sNNeM@bEo{)@_<$%fRU{xc zJf@nO{o?hrkJ)%2px4!~s(4ivG0@=y!%ORu)tnbx6q~btPg~lQs(z;Z&W@|9cpjU% zZ(G1?%x#aGf%B{I8MRIW- zhV|CzkvDtHuLq$YVQ*HAI!2@M?qb8w7P^*SE@sVo2;)&0i40R|m#*Kkt5Q$3k^1m$ z>&*EXRdbDwe`UxTZNM3T!E}+m6(1Wb_1X@;S%C`iBjXmCyb+W7=@1eH-rmVlluO-y zLU7SuZ|K;&a?4#V)H9M({AxR2G2^rgZoSuBDo^%CT-_u{81CHrZN%;E(mpy`x%lwY z7ZDir5ZTz+z;;q$I>alD?&;MTjLNO4>>=ZP7U7?bk&a3 z!Jf_DdFS-*2;;!B^@-w+xz^L>Qz87S{rz!9aO(U=O>U=caR3gfC>OondHI|NhPTy& zP1V@3W{C;g&cA57du`$QdZ<#x_6hION4SZ$^QN2YJ2M69*6baQi;m*Ry_qlqTe@oC zM*5GQyNp**aVak!SF3i3m?x~WZ_qnrMvse!>-8m9u8;>2-i@bRf&_xt?3A#<=a6Tv5u@7+9uxBVDPI6ceGsTrgD0_1D#wWHSA*mA!ltPX7T@adGk6 z930<{Zzm`)zCCPF-HpBB^@L{Q@V8xU3{0? zEH{rMIuHp(#AZ^V$eqRLk2A9#4yQ^I0aDu9vGopps;`zmuj_Qc6Mf^iBV1% z_QjF1OaXsj@-An!`{qZNw&v)B*yXQ~&bM+`-^A3s_hr0y;a2&h45djR+9GpuaF~q{ zLvpxT?-9&yOE5b+e|4#nHRqw~zxb)+th+s~X-3iV;+eADowT)`ouw>Nxpq!iWEI7I z%_n8B9{H;)NEFLkUsXzHJ*Sua?W^-mAlWz~YS2#kP)#Jz1IiyCkuYRU!OtGva6Ffh z^~B~tA(CfAo5@}d)>PZJlFvwTOO3r@5Fg!Z!!eWU-?JKW0gYbJO=`V6+LrZjh1 zdcfLSMup?%*x|Q*g01ymVaYRSN2jQ>o9S8q*-NO{LCxj7vEH~>J0W2?@iACzX5`yiNcVW5UXtv>PGzj~H|U3kwSnGadWlUbb%I<%sAsq~`e>b>$7e z0~w4dy+-hxFzwWaX%)eR3p$}zZ{ggB1Z{QSQi!r#ZZKmbyFx;O50yL^Yz;fDmfvGy zdJLizV9B=9GlGpXn2QcsVZhZMaj|->qi`aRMONU!$VhSmaYN zi&trBiE);SASf)mF)xiSjoPnw7`V^4RTEZ-izP@W2WaZysX!d*y9^NjL7)|U0!*;f zYtDaacJ?u2@S}gOAT~l`PUhp_r|pBDo|-}kX4H*TOkPi*LN=9`VF+xxLPq8ZXaG`S z0ZZ;}+I0HtRRC$KwcY-;1%3)f#fOp7^dB`?Je1PjGA(^q`EChYFvKjtM~K6oc+NEb zInw9{3oZ5OWO(0Kuj;U`w(gpX^kHNCdEesy7Z(6Z@vCqG9v;Ay9015?c{lJGjhtnFtne_u?deBWPt#EZ5)(IaQ7d= zk`Zi#(8T^xr*@6&8kW&S*4B-muS~oH?=TO@9WE^7h3A%DHZR>*Qmc%BmWoLtpVDlu z(y6C&n#f$D;Y$BbxJe4W!YTeQ5j>ndmvS0T`hfaXmt8?(JcFjFP2U{W;$z}37-HqK zIIkQ0>ms#L>1jpWl_SDNZd0SY#itPn%`xCd|pjYjwyJ5Qy+4iKA(s zzo1_~&ej~QqW{vu%JsUm0{5HXS>|g&uv-koAYVC6q?ab5x?FYQ{95aJ1GU+vio{Tk z2XD<#e8+t&qLqC9w{oQL@M``o1WIbgEi5pacH=&#F_Y_whW)uLc=mjR-zkVMts7>! zEW{?pH}#wZZFf^$9$V}lasQ=#{=Dm9=O;y1Cn8`aVt+;l{Tm#QnA)dIT_7fw0F&n- zT4*C7mk_MuYbP3ZI6H(sW@ob@WaE_7)SP}>1%)6Px^C#x;b~?AXkiDx(B{J{W|ARl z{jA%Dz%d5TJ(WrqVQ`(m$=fZs=~EBOxE~G~z}VOKXJbCXh!9G7k_&#o;rfT39v@f} zx8Qq%!-ZN|9^e)IFF2<{@QBGsS(x8L#^Axa`npvD=r8DBx9U1=bxE*;WC)?kLCDh;XhDF6 zWa_U7{8{kthActA=w1h=|8l9klfas;};Bf!agZA4D1^HC;mMO}-HGv1G#u?;3dh zpx!0XjVh)unh>I#Gw@P=*tvr;^GQsKoz)VotsM_g#0IsCSe&EkC(& zI<<=3v)y*yjGHs~ei(O5li`lf?G5x_TAY+$mI_p1-5*e;n=+gvy1RbN9P(j#fm`_9 z5|g(0QAUi^MYlszTwL8M;fm{piY~p$GXtm)p^M#i=vl`G=J}SpP2{MU(u>gnp--=M zS}f~%4Cy!ya%_63&V+_Z%?J$n3kP%anuJ`{+nMKyYR#>%7rp}!D}m)Xk6MVGYMhNR zd)gjzf)hg=jn5BgSh^RIje>h zm$+vAI|vN2IP#)(r9xToEEZw*1ZxT9kYn9B`!_@$1hHJ zJ%Ompz*o9La1I_!V7t`A2@WagGdnBOGYzXW;R4Fo%O` zwj-v{-Zxcz$yYoZKz7LYlIe=xid)67hMDbIE0;UbeI<6G}~GKm7+<+x@2fw!7r{BPOR#fvaD z932M>2pj-zNiP8z1i(e*xP}k_qYJBLyTm3;2b8wH>y1T*abyq3h%37^_Ix++8lCQy zm6eY#3!i&D$XC?$$K5@RcYSl3K9Wz>BjF}DTOW=OZCe#(i!#(_y9WD*P#_@XdxjcE zmggJBSnxMA;6!qO#SWWgW`fbw4_X!&rAN7)q+Ypucnm36pIL5-g(QX7VAlsF!Xrb& z)W?UA3v(97dA**L&*i8H%kDu;i;u&smPY`9E+gPZ2xC?UW|WY^CCe%?FP#px)bbiX zjtR$mRe>%I%mn`g?^Xy7se@t0gP+pN_1b>>@BR-}-vN*HzlQx7 zi84Z2$%xW4B6}+-O4A-?M3E8MqsYpxL|I9tvdPMbtRhJgvMG|Cz2ECs=bZO_&&Q{8 z&i|k1`OWY5zOVZlexAfZk(~^LkoSajswhhJRS3e*Kg687Q}kX`-x57LlW)7q*oGcmSOOLF#$De(9`|L}z z=%1Z$Zlrc}hs((GJs!DJol&?Hydi9E77-4ZGprCPR8d)s7EWuC;`-9K} z4;^qc^O}E}6!*U`>Tdl+(;jO9FI~{Q;{eXZKf&d z=GoBLSeu$zR;M*aF&3oxd}ljjm=UvD>%Xnppy>Jr;GNo;Mt(rC(4(ge80+lX@Ou49 zrh&Z(FiP?IGkSnezW^e4$ul*I>b0g94Y%$lu4U3=fhmkDsBKGrs!! zTYmhv53(a8t)f;6-wH@1N_vC+1nzQ35>BwNmnJ8;^~7{2J<+;4X?0^#d-}U$6YI>j zYqo+$9@5+QYn9w#H8u!%@+ACBzB~8am<(mnis{o73m(i(DSx!6EYaBj7eD3Vo{;y~I z&|Ikv{ar|Cke38n^~I%`VdQs=UH8D^_~4-TCg=OMcCH6HX6bqtUE2AtvejGsDW9HF zp8D`vI_q^ktJ#Rk;0^iR%5N9zZCA4|y5}K@l)O`<+4Ato>jhX@)5qvq9ek$?m z^v_TrIg-~Lr{Z#IKgo^=;VFyW-obp|)L_+|jG#c!VpzQqgMQ!4OrdeGE;~_9V6?U+ zxLWP_aV2nE?%wNIZf`(l1w3nR;V=2q@A!SUik}b3Dk$!3q7V#k^7a=eBO@6$8fOFe zh5$Xg=5~5c{Q1?Om$W%GnB?8NhsOyPUG-1Czs!xD8H_O(bsqa}J{^Y^`r<{NSeK6< zK4dS109eh&n&cFub$M9D-GDJw?jzHE4F(y9u-)1*CoYV1JCvtOA#kTcE4@9I;iLPn z<0DjqrF*u{EjNo!Yh|`3KY2oq_GHwe^8EJ?Kd)oaK#%pkgxlRAPRa&5D<&s_)u!KK z8l#UtmRT-y`@=4EXE9{-T5IGk-5u2jXu5Rs5{kh3L>4*1u!?JW5gd5pSx)mn#ra+- z_Z9X}aU;|Tch%SQt~F*RxYd|Ep2k&|M2m-U$a zpyVOBux*<*pcOp$DpaP}=ujNmk7?PF#=rBNY;Yg3%klB>kg&@k!H~Xm+*{kc?>mEg zhb>d_=aGO^vd$AFI;LC}57PMKFS_M6=zlkIomo2+5~y>Nv19u2Lht(IyIf6I>34#& z-(+(GUQ)(l(1_KH?6ttFdE=`-z;t7@Gy>eF%+gYYdx;vV^<=x_rj$^LHjxs>Zb`!q z>+G?;v8Qeg4OEAbU3Av4Sa!2q*2R;nSn6^}=JKhm%<7!1x?DO8Fza_8lCtHt@V=an zq2@(K?{Jan(TcbEjk$F7z1t&i_H`Il*K*rby|P$Zlhu#qJ31ZYY4gy+ut+nhU@@*> zWP6<6-GUWmzHb-UBC6(dV;n~I(Z+j`RVDP_yMZAp6_#`zAd8rZQj5jbKi}4zy?*xI zNHlH!Ws8K+z?h!Q42!{x6}^YjHPQdVyX@&P3y+RwEh#M(ar||f@bc*H?w*95>lNiM z(0ptn^@0~y;8KF80;?P1PDzuaxsBwOnWwY4<}rGDg1!%m;qg34nY)pZWbvmg9CAy* zH6jULdPg}YGEzmzFfaog`|Jg!0vSoDWYXv8^_%VfXpMC_?gibdIPWNxGis)3k{19X z26E~2(!UMrl>VvPPk+r<*J&Dw2w7585G?~{P;zmRB;vD4E+_`axb~BDpaTo<2|ksQ zD^-KxD2@=52L#z;3gUYzMFmdsC(i~*)o8qnyd+oq^STuBDM9jS8{5s#-zN9qK%?woti z7v6TVjg^H3lStm3JD+saXGl9_6fehJ%&0_ELB0!4|Fg-J^Zun~<3$c*sTvQBx2GRH zc1#5rgV#(($Y$}a?kfJS ztsQjKeWEdPbuMsLcJpARW#WCSk%4{_S;EN`}$3So+ zC?Kew+Uyg(*Q8L8;XXbPG`#cXjWpm)wop}UlQJ!);V+HspSQ-z5u+zSrbu1WORqtE zvfYpOj980~9653k7z#$>);}YW84VY}_BMn>78=+@i=g03L##6Fny5E#Rsp2j(Him6 z#q0rt#>ta@m}3C}gN{@9$`}4%)9<=DyQQVy=x0ltAbm)@_D)a_BOW?z!vHp8K-ZPo zh+`~v+&9gYlbaif85#VJu#Fd0nzk6?;FMtJNAxWqUuREi?Y@VmAErADithdIWLb7s zgtT_>_Om6>RUIjIPBlEM-f060B!m86$b+AcAB3^OYl++yAu|M zxepaeRcsyEuzv`ax+>J(5(!v3c3%O}b98_SWA}>n>EcQ@*mTugLqMj1A|P>Acl6$0 zrrLOB+t0t?k@5i+B8nud&?q`8s{LetDMD)q)Eszj`}{?(Yoo7T-9Sl&Ant4zIfrg+ z5{|B!?GFbq*}Dz|89bltftdLgU4}Zyj3DZw0C%L%ioCaMI!z&Y(+Op@a6I2AZ-|YO z{@~cvYgc|J+INI<&`P~-cxIozJbQV;eX4>bTvUNIG0l|Ql*ucuAl!F)&R$%}(vbIl z)x2qKiTl@bt-B)OV{gCSZuIJzD)2!CkUZ$NU&gDrtindK(7E5|U`>orOX0yQPmZR& zJE&lSg}7nak>+{p4~pPh3|j9-qH_ylC1`6IxPQS_(cLP6k@cb*Nq*AuZK6f}+7j!6 zVT$WF0lLSoHEr(093-CWAE_CR5&Qj?{z5P^5RlY|{tWjV1LYV6Z~51H>D4DKiwwem z>EptxJv)oSK1Z&S)9_tn(V5JatGrPsFji^Ql0j}UK?x<+BKgaJeGrPI3b}Qx*o8e? z3Dh0!1X9$Llgg(X6~ggV2h%1@9gHRP~$I-5$~b=m2F(La`%*!Cz@YFWe!VNMY5QaTJ-DQ0u#=;SUJ9 z48N$~!+;G4EZ8VnF$KVZF;^(4E;$^26li9mKK?rTSF7AHBo_JB4rC|l<7tDA;@VWv zL+*O$Y=X1D4TXvtOtDM7g6!+$cj2K1;^+Zjb-x_Ly~cA9?2QfD`e)%`!ULqDiQL58`Un-7l&~LRI--VlZJ>62?_qRds}Tsqv+wg$S3%7v(z4KqX;T zJM{Bswwz~=%K#nelOc1bYr?EH=e-YR*K5NdoI?g<`0m?<^x9fM-?yg=U)DHUKAExX zE7EfM<6Y0LXIiP*uG5zhdffMRefe$ifuf1`5ZZ3B@1ZE1ODSj)A5P$^Hp`Iq+;=sj zK<~V;p7!CIkR2ZceXkfkd%wM}AuQL(C&D%U#xwJeZIMp<_Y~|pX~rD)O(a3QyWDe= zoW9bP9lsQR_-dDi}B&dlMUKaL&niRhQr;g5@uQ4w-r)+ z@?%!KL^Y{EaV_C!@>IAj`HH*T4tg@GkFuicU7q=6>>~PJM@-eX*Kz+GkhoR3!)*tz zZZTmHEHacXI{(V;dOV!@AD3zpEpDt$ z{k$%O3qVNxPNg`!`V+b3klp7;r*B8tAXE-{x@c~Q7QyK{6KVX-h@i*W6Qw5|{Iiab9GBWttQ93G$ z;yB@50yOL;OY>dq%7Mp-%^H(qL~g$WgQYm{b?V_rwc9C)dSUb8@*4)etXoYmepC4m zxoE$|h9sPx)~8M67axYi^|r2!&4uk#mrm87)ieGe$WUK`H7u#V+qSEsYVpUoI_AG} zUxNkoXWlyMhL`7SU2M)()Y0$l_**f7CuZ935hFU_9K-MvTfA`fmRh^4^s(MZRq*JYvL`Cg+^w9P1FzK2!^2j=Q^zt#)@Lz7njjVpM zaEnXP(AGQU`!cXcVmA0#v-=_Mgh9JuKBx< z-4PYAOmO{3O0YVzeWfR2fx zpvjVw1pgURq~x(&h`Jq75kM5eDRHu{Oh8IVJX<$mU}^AX)w|3%P+?_CNHWWQ@71*h z@m|)i$z$6R(<)``D_n%+*)acGi5|?Uo(BGLhrf*bLE{hInY*JeEX*v%&;7cjCla6T zB&MzFK!F}Mwzjgc(*C8j4dxkz|+mtl?$rVR3PS;(*Zuc5fvE zgSZjX>8XlO2VUat%g`kI~_Ogf)RWw0&ZWau=xoq@M6#xVtT=?=@6wuOR>8_lf zAqw6yr)7QV(J$=1Edjz0M}jw}lpU+so=N{Ac5$u{H+bV`*<{y2dJ4w`!?#yZ>f1{$ z%xK8fgmTgauki9|FnXPn8j4)%p3$E-Wx>}4^XrSmE!E_XIfY&WE7C;jjmZz*)WcJKlB=uP zWUit;x?+t6qk0rRWnK*OiwwM`f(@cnqK#jM&?rB?D`A;1TgCIpf;FR?l0>=WGSITI z#zm6jKbMmCc0>XZE$;i-(|LiMG#BS=edP=&$uLXmg9P>V={=(jpGW^pO)0JQtW)8T zcpphJC3oxHR=mJymp;H1SydOhyK}A~fGu)fhPtR>VQS`h$#h-M#v``k+DWd1fXd6e z<}>G9Sw8!4YbI886p1x{TwGY_%`+Zq_xa$(wm8T~-KH}Aaj&mkg6HbF<%tpVJ_8qq zq-Tczi|+ZdeulVYdoHI5k^8$VIb_qMgh)v5a9^0bgu4xC4wG`y=VZB3A0MB|TcdIw z#p~NYZod%TC)B7?zW&Vg{jGGxG4t^17K_6iw^sH0-VW0&l*lqLy#8gU;Z90i8(F71 zkAuZq^wfBWecso2NY`Omw&TQf+BtX&ES#Pb4C87GrhMy0tKTf&n3XvA(9qWX#=>PYk1*)HBIDvfJ9uN!a9E*kvCrWbA+9nX;)V zAI!ZzTRix*nU2nH#W>b(b#<2D9Us##P+1&zXJdSKtlqZ&w9J7m)NB-;ER+iy$oQxh z=(?=v#v~@JgedZ&`MH%k>zUA}+)L>ab{2-a z$~MQfk1Xe0I^4Jt8_Ko>HM_A?a7w@n&iCqe)Xw(aYWAzRNzIsE_{N^CRxYzH6#HVyv-~U)GE#+YAjBTsj2VQ?yd; z!2?AHQ^}`Z>{N|L9A>q`gqZ^g;nx*#!Ll1Yvg_0OX@7L4?Ed?s?ep@f%9JX&!t$LB zB#dq~zCS}<;zhc#^hp4bY-RjKg(SE<_7mGhPJ^DRD zH98wI_%J>8+U~S&=MAI?>Cllr%YUm1afSvKlU>8Sw@D-}rcQtUwQFq|nYA*lr)ryI9o~iww!^2#9ykWWk*~{^3GH`xq*eqy z+P*Nh5UC?u-P7E+MOiHcoC`RWh7cNkM`_YcMV@l7tO!q<$;B0}31A;eFVCaaFR6T6 z8#oZt7_t}}@o&{}ep0M>oA#_%rT@hbhHGuQEjJmwUsO9LFtD7mF_xahr+{T@X=794 zvthFxNDuCk-xCPq&J-0M-hF-(xNO<<28*OHTy8|17$c)%id`0yoY zny%Zu(h>)lR(F0u5h^Dw@!~#?%lKPrbyY0g$(c0iMqzdfb;t{d``Hpfsh0e zr>x5s48-vRn>B#H3AFcOUcPvt2!hkHF5|z((Z(^wH$v%v>m7v?uQE)U8B`B+c+%vm zhL`W)D@88%za9)4!wwzb7#RxEr44vi3W-Xh7jrx z4kZ2H=k={>c_0pH>L!7!HW#HEH?GGid~4=q#O*Cx`J+xMN0N$(b9)O6y?_#sNEiVB z`quWJ=hKrFMJ4->k^sBO_KDrSVUgXE>CFR`LS^J6e0k@MRzt3^u!ssGyN%HMlr0W! z4}mu(QxGM7czJmf=tH;kQz=MvANg48W+^D%hfi2&RACmrx@WcM6!maNjMu%jwH@7izYZ%$ssFAh!{=M4!pVx~!;35vwsbc`A z0qo<4oPq+wITI*r!B?H-PD5lv%(&GMasfOO%b4rFJ(! zZ%EHrroaZ|1DA>P`20SZF4libPO0AmmtD7svu#^`?~>+`x;2@@yF#<1=dn$vhGxLg z_m9sQ8ivA?D67;MXgo?%g4ZzonVQ$;Pf-RJyE*HW!@$YTZIYbp#~%eAFQA)t)VaOo z%QFlTfpWoa*rc=2-YIoYa&q#eLk|t|8;P86f+c~4xOBwYN|i3$wkL5Hl>o!to+rB7v)%U zp>k2+0pbr|bVUOLP7mC~9UQz>bY;@c+l0p6l4FY^l}{{q?J*Kg*e5{4yIKQpyetSp z0IOA1Re>$W1Hb_ehre19?&+mJ=yLiW>kHh$Die{LbGyv*^bUf8cx~T-iPCu^U^#@G z2e`=w66llSL6!-^h^O$;IRJ)aIpj5jUgoP#sA_Yc&TjifV8p!uEd-^+Q#2DVGbcmH zPvC-pEwScdLetKl15DAQ6n5J^qP z=AZo5pw@eJh_l94GU%<>4MyYm;ybUFE_YfxbQL7hu%|tG^k@?HaD-TOp)Y`2_}X_d z%=q|s?>>9|ZUk8x-fZGG1m+FiEzk*wOf>Jq+j6P%Mbg61O7?>0mm?=mF)1$*6i|Ro z`HfG3or6CMl#!C0Y$qR|jKQDR#gOnFU`hnOE>t?hMCt{$W5Jz?Ni@745q;etPTXTs zIlNrkYh$<`HY%)_oIGrbPXWFJj&B@Su}zn>xXnm5s5{`2f=CAPUrKVoW0gkvilT@ZWy5_591qgXqn8RU8Tygga*Fq^w6Iu9 z8u`-ITD7%q2%UWQBFQLWQ*4CS!N+T|HO4O$v-&9Y0$c`CdZ{f-DyP0N8uz~6TTm@h zP(;^FdZ!@m8`~{!(;>v`+tl(v>%6g5o~s8gidt+%b)9ZOWo_)ht@k*PAuykC_C%U0 z%qusePXuI#69DlDKYPF)hOdrcPRKBntJYlV8XA-&yomXF*C7YCzVQ@w0+HL%FKVf^k{W^`t68M ztIy{om94^fC;iFZhOqfgP+aakUw-~EyP`v`zUh_aJl3y8{w!kdXR5WlcS+^akWOTi+^G$q=tn|($LJ9-#43xq1qv_&S+2ceqquN_c2z>=$;hVXLYT7a$Xb1J2BAY9?o zoYLs97iSd4l;@Q8ZlMN5x#8K_D^rrWRlgAdP~4dzY6@)bX4t^xyK!I=zV6)jcR6Gi z`+IKilotUeOv^3L30+R5ZHHj9`zO2DAkPKQ{ZThdO#}+H^dfK!6b4Y!`$ykbB z`vC=MJS@&vcfPha%$`}6rCHy5`rWLRVznvxLdaut^W{|S1S=Bh*E%N^eZaoSrG4C) zyoILxtHUR$*$oyvomo@O<8+GOPJm&vxbwuz%c;jndwr&iaRQ()iv?(ltOf|JdIsI%O zPM=NAO$;8qkh}y#mFgyz!TWYFXZHdo#uLKfHOg3Dv56qG9obpo3%iC4hdJueJD$ri{J`7u*)A6qn5nF9IFpkIwKnNRS{mi&vbfY6zyr$``ZvecUT$~<(lxkR4}!Iu^Nd7Lj3DRotHxpn9QsxiveW- zIE}$(Ns2rqlBF0&a4UqBZ<<$ec){Q%jy6*Hl7V}*k#s;mpSC=`I|3}9-LCth>md=d z&L@=PB%QpgQ4Lm5+K}BmDJ?DfAZRb-<2DEWjYq+>7#--66aUxR&#=C(+Ry9z5w?#w zfu^>$K%_(WBSK=q|Y2 zw~$dTDJL#1{oN!@=KwKEYs-fERKLLHMtinBr9&L_9T46KgJ&IB-OqLH-oNtP;xy4u zsRCvC@!=jd149a0U;AuG0xrQgfCz0RkxT7zX$6k>^Pa#fY?r;7vy!%enr> z*<ilK**OWy{JaUkfxS^Yh~!fmxbp8I1WeUxL^Ju zCpN`(H)5Uo+}RldcT0F;d>r;6UPp;_H--C|O|BKS}o(kqTBDw2Q&+=MmaD^-3jrB#$)|9%-ApSKbXc672lJvy{By}cJv zdR7~u&0uy31pv^0w_*(Y!X=-U52>x(h*e3Ff#YQxB)(5{c1 z%cYl0?|W1P*V)t-90ftFBosY3hfY{OCeR1}d#?X|#L_kbcO-r7w%kDKoVJax9=PWv zAF|hCKH2LBTge$28X>P26SF$szke4tdmmxc!@xT(_WKdpfFO7ELjAVh^4psWG;7x) z`}Vy0K^B!FyThoU;hK%F#Lda@Prc6z!c!`FyTlqP?MSy*r?+aTl>EO)3^lLe6XF`E zUGNlLUau8TsEtVse?rH}S$d7*)awo8mm8UtIV&?2LO;|@Z0U3)o2^`#qL{bw;1=GI zeyw|TE$-^_jpLsk=nh)WJrQD=(SB~seBWaGqo$f9eCq$M^zbsV=#nM}j1MfIFtRGS zBUJEU;75_(M>oa5xPbNggZ3Uv!zrkI03cVO!|u${w8EQ2x`~No_-G<6r}oGQl$TOr zaR+Pjo~zzI8GB22ezS;conZofgL(H-ENg}_#Xc_Pa*NxU#M$1Tmj&P-w{(uJq-0oA z=I>ShB5_5G1Esp-f(PDgFmOYlOMrt!^tcRk4G_V@m@qIb%;|$TSRKSG`pa=FRd?7? zdUDYt+PZ@gBR@P#|IwRt<{S5Zn;NACiGTnYK%T!H5Kz2p_dgLMj1vpZy;slJa`7P` z1lC;4Xe;d6rE0+7A_(iNOS`PJbDcf2=Dpkoq`yQ9xBfz>1QiNho9Ep!zi|`BLnMti z*J7$0WezXf|0zxefuM4NE96*U%XDcU+$=ut*mrDCctDkus7Mpt^6T2nXJj|WM0l&% zA+h-U;`{=hQ<3XG)KIMN6c=Y9BO}9QdggEUecjooM|ffBi&V2QU7y`&s*C;CDto&a znD4{e=O(r@6kX%rwoCuLZ&jiwvr)oWb7@0Yay73l$9yxouOGMGb3zz~&9VIFb?DpZ z&;b^#9o!#XkLrMolvz6W2t2kBY$zx`TL-6zePHV*!XI^hyvyeJSvW?6c00G^wUpZ} zs|sVBBPjK2H?bI8dtAz`y!2|#nKgLJmzTg+m7blg06GSg8hv?bStD>10uI3rcLZv_ z-KXN(Mwa=XN_3XEGUH9fVp9QT4ODynSOTG-bCVWRrb@#vp{n@W+UgFxn|NRQ;W>21 zX@4Ot6$L~&XrIt&R+(gPeB$sCj0iFk42MCiPVbmshk}TLgm^i?A5fjjo^3h|4N~z1 zO^98r^V$JIkQW(@&$Ut8pGVfqg$Xh|JmY!$E+|VY)1^?eDkq}#y-uh$Vp#Y8Togm zpp}O@@V)*$znVG^v4pHu=T$TLQ&;{FZZFAsd3oBVnE$)XwOfFhRz7NDY$|tB5Ru=3v5Sa#y_~3F{W~N27QP7<` z@5Tyx#viOQ^`BUt8&ko?g-#Q@OqSUPCUk;;O};fJF@D*#JNsCV<6n0--cN}!J#ufrY&6(GcH2OO(E)bv`0#8)@98;t*E8~bZ>664vU;qh(g)Un#Y%DybQMWKLGf-}=4`P_B92|8uwI2lr7aZrjVp?O2 zEk+y8xyML4K$_h624Cmt1sAwc`|)26zxMbO(t!E?U#?%9+A=(rjgCg^{#3l#20nyb>G@&34<~6H!$A&+0XOrF@;&9ww4U-kMQt`z*NBeV&|zlDt~}!M%IF zSiaF(=J%LFEsEA3ABd*6EgLu%_$|=cz~n~-r3;Io!Fw%^LvWA<8;I;?*B~Gx+^W-h zdUwz^II@|7HHhX4@K1;D6rOlzX)O{yfSQ)}2(+7FoKik$s+?RyVEIfU#Yot4kq9sb zw#;n^m<*`zC-iKjHq&<{n(nK-@$4P!yN3y3>r`QG?Th_bT!_Q?r)S0MPOuC~59+A| zAP)wtH0pl0{wJ0{@lq90QBh!afI{2H+RlOQ`iU2h5IldAGBW5<4`K^}l2*KaJwq>V z6P^oePFb!@eUV2FW`fR6Ay{Vqfgk@$>Gk!|Qf~tU&9AR-dXb*D&GdVGgVr&c{9~!f z)ueb8rOU@RtvYtSOyE!JnYi>U__kZ;@YSHu*_natCrRlEu6pFw9#+oM_s$o)iYHM@ zY<)j^)AD4CsdvP$*Lh>AYK;2Z?k_KIEFK>`HLmzBoN{K>L-x<<@#QxYBc7!bBX6z` zxfxH)ZwHd(-*WXRcnDXmtU!B_co7DcImhpo|a6awT@a>z{+WOk12sti>P8$hGBYv$?Suv1gS0*b- zzbSAnau^!}$(1{|dg)2exP6PETJ5JeFG7DI!K{QL*S zc74ie)pz!-&)KdcpGJBd;B-jdo+@e`L?9A>4{TPQ;6qS&?}IaaZ7E#avRV9i0%b@nPHE))q2jut)WfW{5x-mt0+!v;)`=Mk z_s!$MUHyr{?iDR-0k>mioCTd>ip|*4{^iR%vR+)-&qbd;o%&>wZFlzNeTwqD*RQEF z&g|K0U}2H+xCDFS!R;>IPMvSlKA17BG~lAf83+#H3plj&k+bwUTg@yA3g#S@KBrR zvdGqfj&b1La`F9$l)zOG=SiqX?!6C>06pE-wPp8QRb_8dgmWN=|H{K zjcJ3lwDitDTg{H_+)vnio_5-U8i7Q6+u0oKMx~_F<-?Btv;jq!vna`7CV(Xcve`W$ zmi*?rfgR_6aLA`kr%~dlW-?x)v+qUn+R{G&-bd5O93V=uP*dThE1f> zLgYR(gKD?zu@yRM)uXB+{vFW>Q+o5}jn5LVmG;F#a~_W!#QsDgI7k)t_=OuI-ykEJ z>QhIg7aa*WB-4GE@%x?Mwe5keygsTmJ~3=qhFvIZ)L5UoODe zmtyZl#SHLzonD8^*RHthQV(BNPuAy#Tc#AG1ov?%QaMr%XnIGA1hdjqlWG32a33U3 zEJ6j)F^2w+;kW6>;QOxb1^M}Zj2_%_>wwWy%zJeW&OKH8l{D-K_i^!4o#D9i)0H&iWWUOMmLXZmp4@TouzS3yT8sH#t}>L9kP1|$^}ttX4l z1zC|Fnq>TU)BCcnb%@9P;Z$mJ2%fw&QM=&Osar+1C1_H5H&FVfEH4n$P`CS3+l1Fr zke>KcuG@u@fSdM3n4dd$4wru6t#+EuEhOzcDJ*0YDqGkI;Wt5;m7Of7U}UFV4LYLb zA5(EvSZBMAi*^kG8o}-lUKCARUf!zNLeeR^zANsO08)GQN^&39Ww~>&LOGDiKiQ1- z*s)4KznIxY58uHzn^RVO`)1vU>2aK-J-NlFI3*PoqItE{72#{NGn=t^y_8csPyto1 z9YWILzD zPm~g+T5!jWdr{~tIn@$44g2mWP;8*3B zb1SUHE3L&T6x9aw(tuJ9$i22Hoz-eOq?LNOpEHb)7m*1O4w)}rP@}B?tV|LXocV>= znw6W$L9HXz;72WZQP8sWI5;@;O)u{J^L%?y2;W$}VDx4xU!{v*Q7(YYxb5{d{t&;l z>d%&9B_9v0BS4}j96b?Y6mZltGc)Bw(=;U!eC7w@RaDi7n%>zIga;|zFZAM}*rlX# zS~5%pYa7Dk{X#-QUidr7+4oa^@90n&cjFJ7ssi8wY2t388RD$Hgxvx*TANN>KSiux z#E<9()!jm>yX2h<_nkyQru> z_nzhMtV+u;7E^P(%QeRV#UM>(v+T$7`z{=zs*9|Xyj!;ND>?k#m=gkfH@FIi`)6yw zT%okovud7(co-0wQQ2qLtLeSO{PgTzxF*`wAB5;`ZDmMd02nw)*rrufMnmc?kQ~8>pAUgIvG$uY@zJ9%pREv= zuE=(`(n%`9|lX6xqEmQC1Azo=Y? z#*b*Wk-Zji>&w8@S)W_xZPRQt2P9tU zB<>RuI(Fm6VIqPTD1=f%vtqJ7{-ZOS{kawoL_SZ56<#0Q7Q&>jpo7j&StQLu{z2BF z6%5Fg23m37FdwXWb7KSrdtEeYJ35YI2OMp9Dt8=9x_HS3>jf*nFWwxeBcNQssn$h% zg7D$1Tun82YFCCrzsW(8(w4~Prlwn1+es9~zL=6GYZWvPAJNy}j%MPpjq`TKDonhH ztTZl`;0=V42Og5sr%&Gm4t~%6ZsFkLEtz=s2NJQQEqxAQa}8yb(D)=hBkEz>g@ik`4*(=%Nn;?^2TqYEog0Us>D#_A5c%tUkIPUvd}QlMd*D^nI)VbtHz60l z)nyZ)v3q-D5|owdpXo{9JkcyKHQl%8_ms+dy8v^+Nq96+P>-L(f&eS+zU?@ow_-X`~?R7}g9hF!lC;s-)7US*Kz!6BYLz`=pUfC#4*;h#NWMahG{$_eig=6`V}8YiOA7CaPYwjG=xM zMy?x>q6N2ty+B}dVuoWh}H8Rh>Jb&@? zj&aFN0hqS+6K{0se0P07!sSLR)sz0`NrZSDPz%xdp)(-$E|b}Ns!*NqiMa@RQ&L{u zHln>EG*pF!zoe84^78ud0Wg{pm^g0IX^$O=_-l9|2*DMiyMmz~feUuEo|q5Yxjjer zDQ&4*AY$Eqjg3{)NpvbUapGx{MkZzD@VSXi%Ejb2kC3ZL(yy1+hcYPq23WQlK?FDQijgcgG@v$t7Bmo^} zG6U(wjSg}^dU#Qo?KI4U!@}rEk;A`Q->x0T{F5rToz}O41PRag?|Pgt z=6;`La_He_tabm8Q49>RmA=l;Cv3efEiHRP4KX?afgz2j$K>hCsD%Or$w+*CEf=QX z=r@~A=idrBU!%5^6BCA=8X+9`bJR%&@B(^bsLglE!mTB%-TKl`X|$*Wj}m_YKY+Rb zso-oQA3eGQ_WHEHil6c=Py(oipO=+!!SLlpM#iA|%e1sATkqwLEV`(V^$|Z4&4f+O zd#BXCi+)SPRe&rLJbjskY+k^*P!|oYR!^gRjRXE(FXwpiKk z9K@kjK+B%xHhUGuV#440QAyD;eXu1d?>5liGFpJ z^IK-|_Txbk19efio+;Cp)7`suPC#Du*#)2ZZg&QWZWmK)4rSWS*u{&xxU{4L)#{ii zMdbGi2?bPKR8&{5&yvfh+4(49BTB_BZfQi#m`Tv@+sq;6zP;-7O4Cyk&1$>iGVBa;lb_kB`r{L?FnXxXE^ty-Ii) zIYAq8K>#7BR(^j$0kOXaK%86*QW5Y@KVI;jwm{fv*g9 z?AT#xWmSo%QH(d_9&?LtAayt_YKZL;Z4Xf;V5)>ZRqjdE9HmkS!~1)8i7SpL?bpai zC933e9@Aw-G4a<~Gv*8z-3zs7)_iOSGKc!JYkz|})?tSPq9?`0GT(MGn;!A>T#dQE zi`vBFEbvqiRCI!swp=0>ZZ-jeg9)a@JG6VelMyRj6J7ztOdarr3piiNg4$MA1YAJ8 zO$eDiQtIV%k$4Nyou#Lz$H=-Kf@OOcQnrbHJewtyr)C554d0q0QL+Dywj>9AHRs3z z>6OLWu3v`x1?B1K-04l5o*WOH3P_<6I+bq65-<##o^vNAJD8>T_LL3c@$VDLPC+;S&pIW^% z8ozey8n8Q#Hpz1Fxyc}`=Gyv=`+bNCVjZ?vcXvKyTn*A^Fybk z)>*sP<@`z;GEK1|iQK>c_c%x53zLGkP%_dHI$07$k`7`2OKdZQc`crRaPF1fjo{=t zo&VVBycj>|!f8TPDFP}Pt)`xAvRc15CMYB{RIi(^RUs8!zadn$GEwu? zDOFcjsS=|Jo#0a%idD+Rhx+>mCNDTM#JO{1^%V_)lT`9}WWfILqkg8p9b&u+7drA& z3nRws`xchLeWWCL3~cXFw6U@I`typFm5RH&4BXp7iT1Nq;q3c+mi-RkiD3utBkstx zf-ZmDcJ$>(yY4OMUBWa=r^H1B=!}?U5yq+n&-TpSJ8X1q9uiF|&%TtL#9Dgow6=Bt zK^#SO39K9iRJmfI7s?v^U^m4E#ilMTk9zl3yS4xMeMCSD(U7dg!N3eC5Pa=Ws{)e_ z1bq)PJ;F?9c6Ju%o5=kHHe2hCe7ahhQ_71U!;dN#*9>%(ZnW!B5LCq2s=uGygN1); z(kQC`lt+B#e_o{nybQ!EK*_OZHGI9F^=&zLG*5QdYa4s|;n5-~LY`eVn{Im)wJb5c z_gY`>A&^9zz9PmEgj^hP1vMWSbKt+zX;(?-`{4M=le<)!BnM${08!Xh4?&nklGm`x^Q116S)D_3qoquXvx|7U9n*ObI5 zE=fDZi*b}UiTq4lG}LM+L-!m!coghZ%u)au0YEh2mlENED0h#L#HFS>gU&eNZJjM% zYn@N`-m{-8#Tr!1eGyd@LCku;LvqE|_)yq>E~-3Yu8 zO%AGV*%x)S?d`c8^c(T;!I<<|-e{iUf!ZpyguNnfP!_#7|4JvAcT+=0-?4Mv#|5t%yrRhR|t5rty%>U znaBVmp`)1L0GPD{L9R;gwH3zyEQF|@x3LGfuG-0yp-+w+dD`i{zKW!89|pe31?RWR zU2G>#0P8RaN8$8tXj&;<%M`e$(h#}=*TxShilx5yhwdj2y@yl?pJ{YTHU-;(ry}0W z&IZPdq?huHq7@o+r&zH-@FE^N9~;^HBycf}|A`|XC`XS0^L2kU1qxd-_1VU-310gHj6J$;2!j&2+mZA_;-h(C{7S&hWheQ9t1;I8=`fou?SnCkWDC~-+K2ZNX2i-#E&LBuhFr5L(6@_oJ&Okq0P*YGeIscB1wNf8Cvm80KU9AKi^IMKpxId-z5R=dpa zcp*8BB(bJrk{IIDtQv*kiydbG)E+v)%16}je6Jc|(JiD(0x&N^FA2h7gn8L?`~-pU zZt^qHFvoM{2O8yx6Lq(z6VTNWa40Y{?hIboN`gBQbJ$8NWLL5HC8PVdiuS;Ph-x;2 zzlH(-#v`HsG^lt7PJ9XB99%rbHHlY}IT1u7ZW2Is2$pyLK1xGdw)ZIb2V+hunXTwq z$xxw!%Lj%1YaUyqB>7>eg7zLA{RcC0;Vqv2Ki@bOcd4B?(Vv;Emkt02jfTpgqnSBb z$knxDm*dXw*@~K+d~xNH>U*p%xbRAb%kvW(NfoN{-k$%Q3kY8n(E0F02L9YtqcBl2 zO`mVmbtjdBIl^xL(z0Okb{fl)*KB(wJSp4z3hbWrX{zb0$gTVkP+;8QU0PZ?>EuN2 ztsp=apxzm3H``DdeUHg5Z=D*IoG4K(u~=LFkfevX)Iy5)Qp$_G=)aqq)y6->tE*^X zVe#JK{|}Bm?ZebC9@RRTi4uz7YH}zZP+2Qs)Prs~HDVwkNT>#7I#K|9uxLbZh2BEW z6zH7sNeeb8~aHa*AhX zFRd=kmayh~(`Ukz+$CH)Kgwt-v!E<+$r{OFgVsvjpX@73QeE?!kvY#<2Qhf?>Kay4p==SgM0 zRQ2oZl1|BTVagQ=nU#H8zE&r|cZBK4HGtkv8h3wj-OaR0`%OVdilsX$&qBDylzTS% zVAcczTjgJ5*e&mgbq4hxn+|28!0CLox~2LpVMhK}qfb102VjXrqVm1;?&l~!u}g1s zym4d0j^%F!WdOu!5yQY>Wif6?l+Xkg4yWZ}=BxIxF)nzoh!Ul@!WXt7hfwQApcKSB zq(<-ch>xOB-Gs-GjM$(6TQJMPcaRmRB*gyeD)hf{4t=hZk~-KnEznT z1uoDvwE=^pNBwwyywuoTN$4^X6T<yD%z@S(h!%=Wj-0UsB%g)`JuFtyl zY#zAB7{7RQb7{c>3sFXzr=74x5pWs?Mi9M-zldx~{7*TA@4^NJ#Het6ZFytEM$l+3 z1JEkJC=7p}=~~cSFw)&uIEVM=$vi5aU%!4)tE-xr99m8|UnP_n<`qWK`*te0vc-$0 z?1UkiZ5-1F3C@ZF0Z*T@wd1{8q_1yi73!3&F&qmp8_2yP(E+%KZSV)_@Uch%+GXMvxL&{$r4OrY;3YT^gN=i=;?b%A~<|H=ZkC+Z92}W01iv5|j%F{!gzPaShEwXU)L9$OJ z84Z_3Ww|sXHckV&AiHVXivM!8wa53O90VEa$(uJEz|7SUQg`LbaLh1FGOJ++vEq^3 zJiJgO7NO=ie!Xbnmmn|*aHM~YkNY=V_C<#bIQ+9*g6Etrk=OBmxO(rf9{c|d_=ARM zOQJ<-k%rNbib^GwtVnx`hKBZ*2Aa~Kl!Umo)6&+GN)qiU7212(b6$LZ&vP8larooD zsn58s_w|0iUgvqf&NI20F#mXNVdSGX@HgOws2hRijE#+%-?~NjwE!Nl3Fr9f*2@1} zI1pK+V>uAi{QGC=_NxhogIBeYENz&1^^V=P(06M6^95z=cFVIj2tW2Wm;4_EOf_z- zHUea-DDhaiv;3XlHA(K3WFQIs{yK2!>P$c5nC*T=VA(`STWeMP6=1fQ znhT4Mr;Vt+i*goF6VV^uxxVA~i5*n(bD`K_%S{eX4ML8A=X%Gq=Ri*A;dY>A2YF|lTtnhk){&#O-nFSye@IR=_kF~XruZD2y zJc9Yr4{X24xILxHuje*)4B2P4$l`N@XgMDPVm4k&b^6Z_RNy#ll%Am+Q@&d&E0n>NF_TJ)rAU)vJ@zV8?QbGm+LA4!jeizCdqeCqYHR!SPz1CX22J!O? z7?Q5M#%9EF2?@x(?4#LWxc{jt%ozytK;k1HnejmmKPtK;g=uXj6=}+so>%9J<~uCl zD8JKbX?pP4`>M~nbWn}iAq4CqKU2LO(y zn;f>ine!w<WK<6{NwCkm3TQozjr4B(ap{%R-_U@h$%hh|LZ3P-ZCh;uW)o<(l z;GK{t@0Sg=Z}SvfoJpvWto2$K79Si|DmcAUb9_pFRqD#@0~(!v;dSAK0q=8$b2Evu zrHiFq1EtRKor~8PuCGm)cEyWF*||9h5BIn&>@uvonR!)$6sITrP5=MZT72R(Uegzf zwg+9hYMa;DzbKv?)$6AE-jKJ2mBy7Rt`gPxX_z<<5vCOh9IG}HxwKT|&tX~!EOV4}qvTWY;F z?67ft_tln-k`y(cUhR|=T!PShwY{iXH}wT68i%F$EU>D_ei*a&vGe<5Z-h50&-SXh zJ@bp-CDU?^k;%+vbd{3>$ehdX$GAhySY4 zr`T}1TXvn{{#2Jzy9AB9xbcLVz1IA{d)Z zBrp|(6)uOzY||;Hlpu0+-q;Hi(Z6A0g!B9Rdn3Le`V!G&p@G6WBe0dkzcxSrgdjyD zAw|4!F;@d!iiO|?EZ%+lBK3=X!~tOdf`r)a2xu)kdk6Cazup{<16|tr z9QVpv4TlK}ISA_j^n%z2z56Vy{z2HFBO5jIAIshn58}9A7HL!+vfWK9$drn61O}@h z{I(@G?>_u94y?~#t!5b-y~=>1oNKACQ&asq$EvZXE>7w}E=P>Lkg!W&+7Opnrc3v= zO+e+b4+i^YBx%{4b{4NqNw_X?tWJG3a(8h!z-#Z(C!s#M`@fL4Y<<6DgLCV8%a`6Z z(>Kl;2EHXDg4h24iVt$X5710)rhEV5#eR%!pfX#(3iCwo^x^^*P{Z5&xKdnrzHB#Z zP17ZL12o?DC%V=Isd0tRsIe7Nxb$SWXF!R_`cCjImgsg9!>koIxzI=s?tMB>(*Yc_ z)sX+b0&_cQX?B?e;a@!0_ulFNX z@nfo&7pP(zcw>yEjvpuUsxvuk!?x?E!0|m++maiL*XBD(6h-D+M)2tRe){RvSmrrs zSI`Ht-i-k8#~jb=LiCL<1Y+(yjX1}cEz`D*xXzSYTgvY5JV-Wp^_TH0-~Rd|;qEVSfI3ax$f@ZN&Kn*@+JtZi0Uf2QlkSl$AX2|KJsx zxxbY*S+Do#?ZkbM#}KeJA?!E(#3ZO+!Uj-av0A)DbQ>dM#UNOz=T!$ty$T(6a+we3;G7)&;k0v^3J)DS$)f{Lq3}-(vhP8gl&CoWr;iuangnej&3XUk_BJ& zz~7$&%LqV?W%v8b^oN7;s{*2>Qo`W)7bx-Atewk;U%CP;Nz!_zWvzJr`Fo9h01)f-HmtF+@;Bh$h z@jUa*SqS|&rd>1X?^udz3088mru#f-4u8?G^>T}>QuV;4)hQ>AyvmVtS zgnAnUJ{6*2K;U$_j-HOdK|O32 z_Nd?5#bl?ndb&QOG*J6TTA%yI{u7&558eWbI8$%93PbMk{QXV9AQ+vC>lOsWdrvg^fc{+vk zR;gb(Db#h+?O(pozau$A*7M8)uqQO6OQ(O9U53DiR9?WhMpItqUET-PeE^7mHMNE?juVw+cL%(C&KmCK_wh06epoAEdCpC*NaS9;v{2$#62>j-vW{$@*9z!KGCLhyXg3aeuVURoqX|1IBG?NO)l8yB#<%) zTEGmT6TRcHGQWw0;T)f>FIi=P{u_gvH%_ck-l{YV;SkYLJA2gH%t8|l3Vh+0jqgW%#iDq`5wwK)e^Tx|x@V1_4OAdT-NCah#RZDR z9*{oE_4M&?tISe4kmAd2u412%R3N0Nga3ahub`uRK*-J^FB zU_|ToiaD9a=KR`{$L<1dfw|pT6A!B z(fds4S$fUN4@-tN))Ojc&z`?_Z5xu1fWC}EvnUozgVdl30$I&Er&y>KJ`Gg` z+@798%Z(WgDJYmZuem=z%u(H>U{o}Je(1)>9$~)C{q+L%M{aZFSV-vXqWH91{9M(p zT|!3H?_xy;hTaex?^YwZvZ2rYVMl}I&tKzOxBIl^cm3mh;W5t>5w^0tYVUfb%QF#^WtsCXCb_y>o(*?dctNK@EF6pHx`L ziuu4hepj)9fero3PTQSte1+MN1@MjMiCXP)%h`=G z4^YtS+o++KChbM)9tl+UHHgs&Fc5{$oh-fdX#GnYAb`YQhz}Vo2Dm~<|c3{#$JVS6)=?WG%o zjRBwc3EU?m1;oe4`*i6S@7l+ZX*aJsTBQ~fl(4ZnoZs&cF$_d(&?6YORu@G`bDaA! zbO%7hr7Kr30>m!we~HQ0#kn2*(S%#CFNiVj&=ZVP9;*|S)Rtje9|idRiWN1k&X|y8 ztiBK$P9O4FYyD^@4GXuZS%yd(o7 zinYEbB~F+R$^(SL(+f2u8BR+8rx2yl{i%|T6Z6~B`HZ_Inp@O8V`3CL$Q0|t)zX^l z#NaXEXlWiI*Fs^Yy3OzEZNKn$vP=VpY&6&7??ri5vZQ-FDo*y>#(F4R88gmHHkI6C zN!aFj&OnIQ9a&=61~l-9-XZf!Gq2hv3QRx!_?(?1*wMKP>I$TYTW zmjJYYc1QnPWUYyNe7b2}6pV0zY5Fw!RDNEHc=V{Uj@L05o)b{%DAYm71T+=oA<-m* zbZ{;9RH#1V^)yo>ori}9lFH#-?_y(P@yrwNpy!WBv&n)Oh~b^t)2?Q2E?pfIcJatu zRaqZ1u2%ig@dLBD)ug%K zbSY38I0*c8m@OZfgG4b2aGSX=Z5{W=Am^m-uWSrhZ*N}7f>i6^#(so3;0#NGyr=Bs zr7d2P5f2|OiX>>hz5Hd@s>99$^;Y~9!F7Xsb~F}EiG*-HzWzKlp`0bu<56&PSxJpp zVgpuHrN`}~fhOuydAd8;!4!*D+{}@~&KBa=F;!#tm@Iz5`2_TO&s(9EpkDF~uLTLM z;G`Zzp{n?xifj3~rk)AyST!{oR6b;dC9pw<%xU*Kn~`%?R)>gSYE~Bf z9n_V+j^9>>myArY!){1@foj$|Z##3`Hqh+2N&Vv@Pyw zJbxofKAfjn`y^Mq>@I^1X0o&&%OeAFXmlPC3)?V|0PWG8(~brPB-C<0j6z8 zK{(~0u4)PD38O$tw?o(0J#BPkL-gZAg@yR~3a3)@PfznKZ;ql_Sy>Tvnf__mn$%>z zXcG+(`1n(GgT=h7g4}tpMz^JZe%{Lh-1@s_sl18D##Ai=)Ndf~Oz1r}Q4)Eb-v zIN6N>Daq&B@;_Y}(MWFIM1tt9>HGI8w+xy9?5n_02Oxx^?XY1yuq*!3ft{8bhK*lM z4%0kQvV~VM)H(#c=(*C_i^xk#sLh6?+tqk?kot#qZ_JMaXTqmaOtSsr>fijz2IRoD zhYxBw1&ENgTOzLge!K2`zJe*|L=AXF5a~4Fx^{k#WI6me=+<>RyR4I+!)GHkg65#e zLWgYr>pJk@tNAvDAD!aRB8yvfRHn~}15gulSi1Ff&lyhkLm!@A#P>BFHk+MTK0>P^Jv_2>fOl?3IkpD`ux$@{tjbGE8g?usuhG7C zjleu$`U~`g>T^N&cvx#7JDtLtOLnM#SVt1Uz)Av*bX8A*a*xBnepmx8Dt;>!oa<#) zZ5ckq+>NYs+VD?V!B>*ef!q4$o6D7(8-~U`EswXZ4emRV|YuBzB9oi;(iqO|A?l8`Vbubo7@YP8w;FWzKu+FPTdZ>h*6sJZyJFFK@VAR zvPD%rQNso>)hScC1}fjw0T3^=C%>%8b?-R&ac)n@fW(6;#{F-rj2IpnT4)$qNclL> zYKlD)OtH>%3(e&?#JruDdLhMu+-*`8@%>!{(CEmX;8kf{63oO(0+a!yv@Pwyr55Py zaBXMCuDt!O1J5m|*54?!OK)}|#I2XFyp}xh ztx!GR!|doRcZ)1BU9=ztk*;}cjXkI7z_-$g-NDt*N{&C2`Lxx2KK#*K^szYN020;p zdlFi6kTHbSG%uAN2ksV9soVZ1%2!SV3k&Joz13Lv$k&doEM?tDHyzGQ9HB~mb2T4i zh|>YUEcohG$)TqZ#``*w<23#G^XJ{!pxIK*`igMfM#uQg^xwE~=pBB%IAd3!vo}^2 z@^bilQ9ZmkDlx-MPT@FwSQi~9lvG@p)x&_z!tx{K{i|!Mb(F3e6s{@DZZY?BjyA|5+o=zYkAkO#&1uG!~Cv!OW z_%P2ewtWg9{;czq2A#IrS~1>C$<7oIqoj5eV&Z3H{Y{v~Fdh3kp0AYm9yJR`5008WaJM z2aZ>GKcQhh2)x2Y{-d%B)QE(_0rdwG1^JfeY79i4y?jZ|Rs$%Q+%Lj3Q45U@#(AL- zHP;0w1YDO;Fp||Ew4jsYICj%63!T$TM}~oo?=nr5>Q`>OGxSfY5cx)Bu~)|&Yu%f~imO*FOv3^z$6>`@!e7-~lnS!M}L>Osqq z-;2OQx3O{m{PLROa~srVSBa44Thu0;d7Xzo`bjXysPf7@8pRj!oz~%x z{PfHb@ZDj`2Z~9XYHWdixw}F6E6S{c;B}~}2`L)j9Z|1!cU)1>EKv6U`^f04HXng~ zw4mOqt_ffF{ZXxp;T|>b(q*$>ak-7l5~=`O+jl|AsHW*Ff1SF;tnf{MpMUE%hfl;H z^Ter-(?=-wj9|bRw=6)nv}VAk+nVn1&8)Cj>-%)pig|oCO_X4EaMEkVP3-W%pA@ZZ zxt3z1s%-#jWXMuW^M-&48RT^ae18RiR3^^^v)NykPBxw+nOj=2*-t3{=qST%3*0AGPfQy?yTpa?kVJ2a9$oaihdsCza-QK&dtAx0<37ILbNNbxxksWR!o zDugRbRM}8kOhm`U1>vHf-_G!P_SiMa^mM1TBdoNTQF^`c-=zbfQc$giWWC5V-hZ46 zw>7Yd3l54?whzNN53B!OrlK8RI>{LvAjHLdps(EHsOad5e>)1c^00)a1Q_M#caIwi zvy~u=!CmjH*VT}-L;YJpy!z-*A_gD;6M2eB{f#RDYKH^_7>d)cvg4k+7VFulCpCeX zlz(|)swPGA;C{w4A5UM|AU7nzAX5>uh8hKUCQfO`_}qqF**;UAt)v*II`95MXv7JL z(0Pn)fZqGT+PkQjaynK-#5j1Pjf2+Y=sv~!@Y9Ai%pG!OXqLfgVZN7#pYy8bn@e&B zA~%zmww?YWxj}P+1OoE(1ffF4r1JNh+=o9A#T?`~tGs-Ur2v&<8it;oXi^x3FbW&p znaXP8=sG|672&2p!FV#g6@y6Sps68~1??Im<&^f?(BrwN4*UN`0*Q5`K#T;562t+l zK~5>EM2Tk=73tv0aYhMG`|Z-(`sws;T5RLY4^Gdr)!EiklW=XqdLIr*oLa(TS3;xm z@^DI#0Ac(!>R9-CDkmw)cVQb$rQGb=elDpP*h53xMM11u9f`Vp!mG>N6~Z@hJm* ze?n!kh3-U|mv~@9FSdR>RnR@t?e>fd$7Ia3RN3EdV>&k6|2^EYyg@slX(R~rXVWOY z@Y~QNo(6StdMgVbT4M!wv@`!x-{CC3vpfoUJ?@Dg(g>M>34^DAe5=R&9-D)I68HZO z1w69(%zNonxXhs7@ULgpx_0frO><*HIa*Q@PiM8KR&IZd?Pg(RudC`xxs#7EW*#OS zhSj*PlkcAo4bZ6>7027R16yj-W&&~&JXRev@d{!0a?}Ksu6uYC8*B>uzEdULsF5#2 z4T3oNKl>-2B*sfIup^zD8tTdd@%jD_-+jJR!vD!6Za*4m;KV5PNM!)3lP)AM^Xbf2U)Oe?}nKYO`Qk;0z$2IWiC(!ZoK@@^ERvEJN3cF>@b%W3@0|^EWj|qVTCXysf zmm-?*ZnrhjW!|cvJ-3?}M@4;EfkW*R|EoMO1DHxa#Ak5>~uq16Djn?FuyD4N!W0)0W;12stpzyr)@tU*b>GH+$Pw+HE>`s;_oC zNuUIU>EPLCPV*(ZKGpj4+ZEVAx44-^=q;AV>D*ZUW z##wHj2v`{n*fK(C6e5DD!NdJDkCJ8SylQ42mpTL|?ytOa zW9EfX-6VvH9jBs1><6K`L- zaN$D&daI%|$_R^*mQ)f6c6AAj0pNY^zC8PPqoF_@^>-}W7>w_#A`gLJA5A&F+It44 z2MCBISHS*I^eK&b2@0e-aP)HUVV+h5jG(B_{D-r=^B6ZnXGB^#6p#Tb7PgY`VUtw+%1}91epg)M@MdWrqOP)C zm2OPeEB?RH-2WSM!gMPaXir(sBp&|@dMk^6r)*Kyv0kO)2E}mNq zivZ!mn@ta7BR~y5iZLPpQ#KwjqQ$L+Qh1Rw7j~@TT+6~DkZZ^#HQ~vh;_wN7nel`-i~)w8cxX~aJLpBCWj1)03xm>JwXYpKQ3aq`?b z-}1e#Y46@W>OBM%TmUS&PdQuyv;}b4Pi~)xg8$8cQXSv1EV>GgDJSb_(La4Vu&WDc9m6+rwYz ziBp9zeY}<`_M)GMEs5$BnUM&KoPXx|1Cfh!>0azRCEul|gDS|mp4KQ9*TC~kWM z!JSfk0b-KaMiU*m1y-=ZPufvNeK$W!nB$g7h%v@{F5mEphsQFcIanBkXx~y^e7nmr zkLB5nPjtB&XhMC5DlpylZ%}^5U;34Cu^#7(XyB*)a7?_5a|z{#hDYME^=tYZaQPXX zW9T;)!y3I^diCUImGJ+}HDbC>Z2Ab$&7Z+ZRpErI*fc`eG5woGJ0HOtvDVTU>z z%r!0A5IDH>{5B%cH4IADn;n^-=#@2DM)&D|PA7$&Xdj+8cHLkj|M4BGm753Q8z*L8HMlYjar{-EYpd zUrLj%6?9K!nFN#d`XJVsz$rZ-z4qZ4|0k2fXzri=Rfmwf7Q-Wyt<1Zr2L@8;PK2T@ zF8%a6xqn0G$brn&5E7v!fue37rh8HIqw_=Q2W*xYyYU?_4~jJEzYS~|lW$dS+KLx) z6BGBn=qmHJr6z?Se)xG+zdgomgBCfA34>$b5G)K&r^1SU(qv8ur|YkMO>n;XSMHpi z*hQ$R;XKke=ZXgzXQChI6^QsVW;9{2B6Bw{h5>Y_auB*$A$1W7uwj5VD!7f1&=>8bR zHsqp^v{Ihd2&53)NMwQJPTo}ldUw3^T?`U5;|#-(K&gRK82^D1ehbSaJ)G64ynPx% zj9Od2f1g^2m$c`FNYsB-P4vk^>f5(FW~nxYC7DJS=8;8DKh3TJNFMD7hWz27MtgkY zt^V4!vN9TZm9rH^&)ap9F1(TZeA(#u zF1IL{Eyz)z(#;SSnm>_V!F%(;T}kt#d0#<*c? z7u+7L^{nUjS^FQjDd93noR70SaTQ>uz{JF~7d{80w(>!7TPSHsTZ{J!4LMY8Z8evp z_rdOjVUg`JxQ@R@M--YhhS1JOR>G)2Ms1Ev6bTG9-1l@ZA5gHcpyF!0xC zp1^Dj+~bR9*}23mI!k1jp?N1x6QDZi0&t83E3aqJznvLsV4W@re`NYVEAgk$e;JpQI_nMZ(6AOQe;_cX0pOMtk8x{;Tm7ufBBWD=92L zQg43e{c^LA_U$Gs@=d~Ib&(9`29*uM%H!A#FN82;TpiYYu8!xjw-S~7RQweQE& z68a*D#&GOHsEs;U~)+DjfnWV6Gy5qlk>R^pAIA_lc|glfQ!ff~FEOC5+1 z+5G-ymX{7d`s$M%BwQ!SNT!6?ue>$T7v&38Z&@(q_Vkd zoRW6?7Z!=vf;~PgQPGdWIT9+>W3K8=*G9gN%t7v22192;912C*+wb>L(}mu%Y4xBi zTZ!!^#KyqExZn4W z)%(iw75~a8o;0kayb-8a9^=jW%w}FCC&Lhh8`jAq*L`yK;-jh`+WKU9PbRaNWx|Q# z`2&&nSyz#aN7%UDy?a+vy47&R&QcZ`0u->4OVog;+yh(}1Oe>BYuq@wMQR+=_Xwx% z8riX9$3T?jA%6b85J@)=(n9OQu(bc0qn&x4UbL|hfh+4%MNc%~`y?dVQpGQT)O6~9 zGi=2B3Tt(1dvm7?r3j7^Nk@m|PMQ3N{*tJ7;`;n{;`3UAB?01oMWbeNK ziGkCH4V>@Qr?!`U+H@Ds8sh<_! zTM}hRy=~iva$m}Vng8JGhfN;WTyn0>6HGl+C)X~Ol)OQi%dlfdMUELPmyw)Tg)-A) zyzn!{OUY^vOudrkH{Eqx7#~biyNr$ucU@Zgsgo|wDqJiWqg4d$h9(U1c3N+@i4jP~ zU?EK`%{?Qg9+gYW4%v6RP{9yuro*4a@wRm!TI6sGj!TF3x%jRQeYSw>^&Ro^iZ77~#ljX*wawP;=Q$HRE zBb}w|5be3z+Myyvv+VY<96DNBN7u2sPoMHf3PO4*A>IFBHk7M?I}JD+q&gCzL?NCd zIeFC;jt}F*;oEOPzBq0b@?H$lJ3uq8UL%cj2@Br(^5q`akwfr4Og8@9Gvi(p86*{kN1W>jWE zv?YVO*EQaMzB-qi-edOFV|{5DPh1zBMCMg$u%}4SadW4gN=F|?kR#&ayOcsXs@g|3 zKl;86o5o3agKi}aSBe*kd^wMuoeMChp`oE(^o|ErUB9mDpXG8HNIe>a3z)}f^W(BTOXzqU66*QBpwQ9x06IrXIqEgECKa_c*Mu6oLEjowO3_qrYt5%JV>C2JTin#n6#$NZ80z4Wf9 zg&xr-5an^flLzP@N_A`>sJIyKRVGirvE+R<54HPReNe6(2rX;&-y)u z#}Y^Mjhv=2@3qi=U@-K^bHAtXVXgo63(dC$+b@=vms`K2gWR%C`tv87NkgiOt?T3{ z$O$`hlFbnY&?p5CLqC5?q+W!`J``UQ2Kf(HO0io)&8_wcNHTU1+Ywl`sZ(8H-tMNu z8{wjwj~cCD?H6GPaWgw2RAAjJzm z%Wf3ar#hwokoWG<$bpqlnMD_rL|kXh(Fa(Yz2Jjn`fXg_dph^*aBw;kYt*ba#slY$ z-3tyTw-LAH4_9^;ex2D)3gJB$Pfc2E^jfbd)NQeU@un^-X3>>r^D57M?;-ZsTt-|0 zNLsVoyUdq}s8{Pt*(3L44NB>U?~g@e)iVZ+F1j4IRx4k2%WC})nQod7tR8!WbQGn< z%_U)2n7&9dXw_%i;LVy&!$8RF4R)9&gMXUrnCWC~?Uf5kw%F5fD5pb*D|{>30-b~Y z5MW`F#C}lU;|2p0E9=OtcG~OLfnd12SJZTMbf^rK)nFda-7AAG3HevuA0#BYRka_xkP<;~&8aT()k9P_|+| zpC+RA=h3z=mSS#k-Jd9q$HDxKlz;z#d)@meCbN4juy^GG#Rqfa)Shz@VIe_`k6v^? zZZJz&*$l6RU{-;OPXWtcU+BENAlaaL8zeR^aD8fXGj$Ds?D1Z7d)7w|vF~ueGSE|6 zg?l8%ts9J~vBwd`g6OTbjPe^#)3Oh-->Whbvi+@++$?D&b?{)_A;V9MFTFjOBGY9* zXf3@5Fe2lZ}jwoV_Rs)fi$p0jr>cxdO&c>p9g0YXQ^G!EMi&>pP{02W0(k|UpFW~kNKs;%3p>*TvLZyCEQ$9Oo zu0Am&Gwz1gW78z3I0`Zpd1U0|OW(Zi>rTRv#g0`KhRcAZSam5Yj6m1gX}7|P2&zi3 zG;c-`Mj58gCm>M!JMW_LpzdR=a573tW5|_Js(HgAzunS$S~4tIjFlaM<8p{vAS8m= zJMG4(_Y?|=fjT42_d2I+m>?o7nneWGS!-7R@Jgm3Tv z!|{-thF&pO&unUKXGEDY@7jL@1LEpb>a8q^ zw129`6;YcXE-mP(6g)RUHY)1~2M`#Lb@mIHJ6+!;qyzQ?h6h4OoDQzx^kTz-15wY6 zlezAmv%JuDZ2ycfm2e=?}GHA>1`xJeu*Tk zlMpI>`0xSH3+tgxM^Vo}_eS6bfcteqImB2#9&`EB!U=b9OqO*vDhV=kqcFgOGA4;m z27+as_GhmISs57m-=(O$w7Y+rpXmqZ#-h?&c2<3f`R0a?A18m4Nx@JXwKjelAq87$ zX@}-{@D4aa@cBr6-|Pb#QG*o>`}_HwLd|_3^7!Qu8|mth&TUjwKG?+$e*bd@>+MR2 z6-+U@woV~!0X=7OuSYO@5(-63S%B;v_xRXSHD8fa#00PPue&qs*)%)C*TyHKVz3t~ z%E@hpvWzf+14aiB8x@?8J?;MJy5ihUTfyGn&W8f%3Zv%GyxDKjNmhTl4J8oGw9O zanw#iJ%L$fYjG?q^k5LsnMFI@q{EI4qX<9nFaT|STpzT@r|AAfyNU3w0IiB5=vk9_ z{*GB(+H@Xobd>1rw*Q?Isd)PBR#$%0jRK7MK$wX7GEjR1f&%EL@8hrRGLO>=q6)*S z54NOhz9q;&qW5EFK0YRIA8pDV39%Wn=?I(Byw<#enpXnE1(?g$I}%JqLvtDh5ApwZ z;YAE``4pPKtn6&!htq4%INAp4KKeI5U*w^~7Rsuvm(;ZP#0g>$*ZSpHv4Ef;P!9zN z6`|=Dv+B5u?iOp#I+u6kOmmwkP+K`X&qyIzSkCYvbDwY^PWvN>O~jIienQ9Pir7Ho z4o7yshQszJ&KZ5beq5?W=Khr>O|ox!dExg|ew$@aXzal8w#@Ka6R}jy zx02vi9yI1D)Jw?%!gmn95ziF zG3iZL`~I{vy1^_PqJmmHvR0)0iMLR+ht{F4uy#q*Fh8rJl6CpgrTjw&k&r~P1T9N^ z_ALfm3^2`^ySloX+ml24wqW9-l40#aJBQARkB&=!UTWUIPya*sT51c0_X>@|`&X2^ z=(42qox>XI#0Pc0wf3HI2U;uLiQ2(Z+`7xAQ>@qcZdl9U@9fVqUhYNGLubyG@CE8R zI_A=NtMNv(-<3=392lLDEF9|lJ>iD&a3`VfZM~kY4u?mVLq=xW*Lu<_GVG6@JCWMi znT6nlsHaaGR`NCY-%SB(!8n$-eOwwV$gUHkgg;>M`NCY$$; zefblk(m!s*j=tyU6$O81U2};m+$Q1jt@NkR4UVHn`4-pOb4=Z_-b1pwLGrpmqhYHq zdtRMPtRM$&SF~w@Y|R9Xqo7Xumdr61w*-c-J7;hDvGMIwdx#Dfya^&|lHaT%O^I+w zXt%^}slK+GorjMP=}?GJDNF)aV&id$9U@Uw0~qSDeOXG(H8)gWdUHwVmXPMq<(C(1 zZTXq*E1n(5v;szvydLFUO!*MqBM!Mdk4TllOr1jq(*eAGYIp)^@Q-eBtgu{R#(OWc zj&P#r0F)7zdvef*FAP*g49Z9tey2o^thzManH4>Fpd0LIk@3=I`Pw)l<+AH{042hy3Q};@V7%g0oO}IW)Am2J4_oCuZZ5nrv*d zCVX=VnFe;glgZ7AMMZ2d)7TH+dTLB{UoJN~atU!bJ6b{ff(;kBv2=Qxy*%dQ014=jt9`$mM9EMLRHnr7B}pD&11o zu@iW%{ov%L6^Fc6)W1wk=XC4VvRgKXyqueJmB!77rGL`kyjJN#>Dt&WwH|SO!dQ`L z8f13B&L7;KdMo+BzyQ`!Lubex-Gr`UH}O2;v>=XcrfmfSNN>ei-0{mT^iUyawlu#h z7R8EbXahdx$f2;q14}6UL3w1i{(kUJoQQH^d=UXw(;OJr!JTP!<(86)WpysAtf?u? znZ?2e4E`-C;`Vl^1J3#uYCejH*g`7GF1#ielg}8PrW0^%f`*!-t@LuM+Iw-IlYO>P zdt-~+)Hyljx`=5>>|3Y@$Vm#PPwzW$U|W)6@XgtRI}$rdWtzX%YWA*pT3Je-YfPFj z{mSuVMaF?C{xF3@tI)ugsAiU#>=(B<;9J{(dKD2 z$z5}EPK*hJB_Om%glctp_V>WMF&U5Ld}+n4Hy*o9(3H#5o%2@AroH%q&uuj&yOFoo z$vdy+1Vd#z<;BizosPMGqq&u@DDmh~?Rya+iJ4opCTPNllq3ZuC1bo~mnspO`l1k{ zVo8M3EeU8vP z>LD8MzQR4blI`X(3GB{b`G;a}!rgbTJnwYorw==Hway%OrLtTcchrBB@ZR6bYl&&G zE_rb%ePoTD>)(steRnYG1vMRA)!M-LjFZH_6ecBhnNu!*?o=fYs_Um%4Ja$;HE(dZ zJ1rfsS^BT0#>2yl@#_WSze9 z`96d~i&FoKLQ0}dcxGa5M!)Zc)O(i!ijYT~mHoby8cs`$;A=%C-4AfYOw#93H~i%J z`&sVZcB&~6TZ?%S7b?&nvG%{b^BW2Q=QL#Ev-Ns>xee8w@-g% zz8{`FChXt3{!a_=tGiGfuFih1U-J|3a=nTFf~>!e14rHrfe##mX>sP~PjB2x@v@d3(VG}%|aPMaxz zd_L#i2=+V`BC{qeB(;K%A9r2~exZa}hs{v^ZX)&>DJ=+xds14#_4f2zBy?r{VRJ~7p zjCZbS*cFHi>5Tg%4UNPw&R^XBDcT6T6LLD=l~#;tTs^0va^Y`;&TUIHzIVfDe*&x~ z&Nt!%t;|~yo`^9r9>)S{cG-3P7>(H}@vE`xj2zS)5AqlJCRP7z$JJ4AQ|j2alWBFw z)Kn>+lh3`0|KjK9ToTs7Dy+Zs# zX?4FE2oti+KBX1rSBT~Z%8RM?>j}UP{xv^$9!C7T1={&`gWL3U^Y(nbNB6^|)-`3V zU6ie{BxICndwk3`ve_A@yX^9%JtyTPCX;)wYDC(zTMV-Y%)J#CoeDQdQE_947Sf$| z3iNEvJ14w4!`T@gzkcAqQpvFin}3f2or;Dd1B@V0;GpdVKX(Umn!n0+KiDuJ#}GJ6 z(wP$AF4#2D%cG;mu_ylC3N#mJ4aW%O{Wx~MJ7 zvc??e`1QTHx5>f%V$GJ)$ll6CS)D4n%Nun2el$5BnaO*dKEa!?(EqL++~7CMB#x>E3nF_#{tKd*A^0S zYN5^m0;qiU?9BVM$w?dZfYUO>H~ky+4Kpw<e^7j-@WBSGZHY$(rZ=Mh zD+TfF>KMgKMAVuTEg)NlC0KE^Epv;XpC9`lw>y8AxAT0hlGNna?R5-x6ISl|e-2ys zQiMFEkaL5$d|UTHNAvT}w#!{kvkhkxQT}iOczy4+EIE z#(p98z5OsPjyVlDuECOxd;fkiTr&VBfVkM7B$v|z$?75nA+;p;m)qN%cem-iWhoe7 z$?XtmtVHaZY3Z1Av}o3^mZkAhoKC@z5`%{2=Ct0;mE7K<@;?1?Cr_}&EbkR@&yarH z`aN#FC*FN+y+*s;%e#4(&z}izJ!$WPi{8!xPmac3Ie38TNIR(;%|z;C~e++Uzs7~=1I~a_@>YTVxtgLk zHT0zDxJR`n$X_>!i0^DC6k`x$2Oww>r?4n;iP1o*sOO$Ur7wQc0OQV{A^!hXE)n2y zf@6Kh>cw>V;JH2PI$LKawiIPkFCJ%Yo0HmWu{*<>;h8FlS#=8+M=sx?!j3~f3|H0# zc6d9SYhQ3{ofxg@Pe16h$-`&Xs?4`>^Cu_o54>@j0n(&VEid+6>#?C)=R=>vMlx9M z{6?W`4hL70M_c5^ZqiD@pY6he71t9ygWg?hBRL(x5{ zR~Sf)J^d0cJdim+R!C?Zs1%~g;8c%Flte-$Xy03pT>%17?87|@{3Ie9oJ8OI2GQoLcV=dTi5+bb(KR#QCk8zz782zEif>Z=s{*MqP z+$by(2q$jQT`V!y)7C~P9^rPYm{*HU4x=aVoQfkNXmC&w$QOE0Xg&yEQkG*k9|5JC zRSe~zA)#{a@(AXc+tmAT8q9)Q8lR;BX~eg-RsJDOARt!9JqqQmvdtIiRKz-Ctb~Dl z4?Lqdw?!0ZEy5wYa%)?CA9mw#CC=OAHh9<6^i;bR_gM9vHZ|qhwS&0w?~}pC8`Zzw z8z<|wxiqNc-nnBZmlC|G3p}Fs7L1&y%21>o^@ z%&I#Qd}o`c7SXMBh?&dg<4#uu^oH)zS2?YcwRU1%SiASs@}J%%aS@_SqW^KZHEp7a z<}Z0f&&t~Qd@jQ?G~~!CFf0Iu*jvu)>UNuN{R4rvGU03Vp5Z{ zYGC2CCP&^uVwB;`87kcG0F>`ocW7q%eJD95K?u6oS5c9(%kp4yf~_L@ zT+kOInRgFWP;g~fsXaML1!Un4(un^N6BACFIy!6%S~SQx#E)aW!jAX6p5Az~tq0uV z3QEdGC3aI&fWR%76Z`z}qqky`j|RU2rZ-4#k4JD~r=QUy?(1z`Lb4})u%=vIDPujl zY_-Nx!gbr?2Wt0gn{vv&Oc0!PG6S#(XxZZK={0rWkzw$&oAZPzc^MrLW8np zdtyZ8H+|Px8Cx?lI4w5$g{3d=fj6Z)%Sg<;!Xdeu`HGa7nrC&N%Nuv`Un|mQ-*i^p zGuLU?frGU2M-}dV{VE?AL|xNnYdlpelvFDJFDjHn<4#sYClS7}S41S^^oZdahwk;o ze=w6VL>n@EPhv1cOIX0+Ap`T@RbO8%G5_JVssenOSW$D~YJ*^3h#>>6p&shz?{DY9 z1#7~t<+tz*k>nwr=3;^P4{}0?BX{d$zY5t@`*?TZrb)WCzO#SCj|u$MDPq#fY47G( z9%;~ap;Ke+*_5an0UWCLCZjYUvWXB!R76XWiH~Ua^%%qBhYx=^?4=+U{HVOy$ehzh^m+ty^B(LL+L^5U^8 z)pB3`#^+@z>aSF~1$N|bHF>#PC-*U{7y9HUTQ2_Gn=w97wzxuS+H2svS47oZ*y*@s zhP3VQ*{^BY`R;Sp3AH%k|GVYmrMp>3hoWSeB20!cV_vYFP_9*K_-C3#jKY1WB_cQHsE!y_JhY{%7ld^t@Ffy8PNxJL~ zVLwf4vE&=^TY@M{q8n%;0Jg;D-$dXa3b4FkmHmu8+h^dD>Vt9$h*>w|BS_S&>#~3P zHvQ@}KiiTGjRcV=uU=z|9+1?>JORH6lCw}_otZYs-aIlMeK}X)U#R~Hw{w(hM2v~R z#aD~&T@c#D<&NA~!vcuZwtH2OZBh6hPBb>zybwF^s>XQ#`em*fC(+cK=pXgz58%GNPN;u6Vz9F&hz zbPB#&(UY-yPlF~~dGrj2t98u{I3yB}L2oJw3`!C*e9En0N4dm{@O9R1=6x?Lj=L>u z*RPLk^$IxkaNU-r7X#uGLsEmfgx~tG zng)uvl_K^U3GE8xOyRy~AX|7`0wOp2>{GhJqh63%kw$A;w7rz)J0x7;pVU1LmZJg( zJ6bjjKjpVS1^^31h~;fw{?3r(*85fw!nJ3+@Cr!~5hA?#Bs4#w$j=}EqhDVV*J)EP zTBJA`f^xc*6)gOw_x~a5E1;rWqppVrl~7blKtw`Xx)D)8NkvLh5fPAXh6Y8XL_k0q zM7q06K)QsXJEXhoKjZzrwZ66fyY9MPb(op=eV^w%=j^lhJ~2N;sqRh-l6boM0>>ja zKfn76L}c(s12|Hw%YIG;xFS>L?)pkA5-x{%qXt=_ zET`%Q#~5nchqv7$g?5Y_SGUr^y>e38Fy&;!;J<{x-S$I5=t--R8)ltb;&{ZhEa(

(lIW!RXD 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 literal 0 HcmV?d00001