From ae544bca9e3e64f2c57c7f7145f80296126529b1 Mon Sep 17 00:00:00 2001 From: m00g3n Date: Tue, 12 Mar 2024 16:58:14 +0100 Subject: [PATCH] Add app-gateway k3d integration test --- .../kyma-integration-k3d-app-gateway.yml | 20 + .github/workflows/run-tests.yaml | 3 - .gitignore | 2 +- tests/.dockerignore | 2 + tests/.gitignore | 1 + tests/Dockerfile.compass-runtime-agent | 17 + tests/Dockerfile.connectivity-validator | 17 + tests/Dockerfile.gateway | 17 + tests/Dockerfile.mockapp | 17 + tests/Makefile | 33 + .../Makefile.test-application-conn-validator | 59 + tests/Makefile.test-application-gateway | 82 + tests/Makefile.test-compass-runtime-agent | 38 + tests/README.md | 6 + ...pplication-connectivity-validator-tests.md | 86 + tests/docs/application-gateway-tests.md | 233 + tests/docs/assets/api-auth-methods-mtls.png | Bin 0 -> 24949 bytes tests/docs/assets/api-auth-methods.png | Bin 0 -> 314875 bytes tests/docs/assets/api-tokens-mtls.png | Bin 0 -> 11751 bytes tests/docs/assets/api-tokens.png | Bin 0 -> 89916 bytes .../assets/app-gateway-tests-architecture.svg | 4 + ...mpass-runtime-agent-tests-architecture.svg | 4 + ...nectivity-validator-tests-architecture.svg | 4 + tests/docs/assets/mock-app-mtls-spec.yaml | 51 + tests/docs/assets/mock-app-spec.yaml | 186 + tests/docs/compass-runtime-agent-tests.md | 138 + tests/go.mod | 89 + tests/go.sum | 574 ++ tests/hack/ci/.srl | 1 + tests/hack/ci/Makefile | 109 + .../ci/deps/application-connector-cr.yaml | 13 + .../deps/application-connector-manager.yaml | 665 ++ ...applications.applicationconnector.crd.yaml | 183 + tests/hack/ci/deps/istio-default-cr.yaml | 7 + tests/hack/ci/deps/istio-manager.yaml | 7401 +++++++++++++++++ .../charts/test/certs/invalid-ca/ca.crt | 18 + .../charts/test/certs/invalid-ca/ca.key | 28 + .../charts/test/certs/invalid-ca/client.crt | 19 + .../charts/test/certs/invalid-ca/client.csr | 17 + .../charts/test/certs/invalid-ca/client.key | 27 + .../charts/test/certs/invalid-ca/server.crt | 19 + .../charts/test/certs/invalid-ca/server.csr | 17 + .../charts/test/certs/invalid-ca/server.key | 27 + .../charts/test/certs/negative/ca.crt | 20 + .../charts/test/certs/negative/ca.key | 28 + .../charts/test/certs/negative/client.crt | 21 + .../charts/test/certs/negative/client.csr | 18 + .../charts/test/certs/negative/client.key | 27 + .../charts/test/certs/negative/server.crt | 21 + .../charts/test/certs/negative/server.csr | 18 + .../charts/test/certs/negative/server.key | 27 + .../charts/test/certs/positive/ca.crt | 20 + .../charts/test/certs/positive/ca.key | 28 + .../charts/test/certs/positive/client.crt | 21 + .../charts/test/certs/positive/client.csr | 18 + .../charts/test/certs/positive/client.key | 27 + .../charts/test/certs/positive/server.crt | 21 + .../charts/test/certs/positive/server.csr | 18 + .../charts/test/certs/positive/server.key | 27 + tests/internal/testkit/httpd/http.go | 44 + tests/internal/testkit/test-api/apis.go | 122 + tests/internal/testkit/test-api/basicauth.go | 30 + tests/internal/testkit/test-api/csrf.go | 95 + tests/internal/testkit/test-api/handlers.go | 69 + tests/internal/testkit/test-api/oauth.go | 190 + .../testkit/test-api/requestparams.go | 53 + .../Chart.yaml | 23 + .../charts/echoserver/Chart.yaml | 23 + .../echoserver/templates/echoserver.yml | 41 + .../charts/test/Chart.yaml | 23 + .../charts/test/templates/_helpers.tpl | 11 + .../applications/event-test-compass.yml | 12 + .../applications/event-test-standalone.yml | 7 + .../charts/test/templates/test.yml | 17 + .../values.yaml | 15 + .../compass-runtime-agent-test/Chart.yaml | 24 + .../templates/_helpers.tpl | 12 + .../applications/test-create-app.yaml | 59 + .../templates/secret-compass.yaml | 10 + .../templates/service-account.yaml | 59 + .../templates/test.yaml | 26 + .../compass-runtime-agent-test/values.yaml | 20 + .../resources/charts/gateway-test/Chart.yaml | 24 + .../gateway-test/charts/mock-app/Chart.yaml | 24 + .../charts/mock-app/certs/invalid-ca/ca.crt | 18 + .../charts/mock-app/certs/invalid-ca/ca.key | 28 + .../mock-app/certs/invalid-ca/client.crt | 19 + .../mock-app/certs/invalid-ca/client.csr | 17 + .../mock-app/certs/invalid-ca/client.key | 27 + .../mock-app/certs/invalid-ca/server.crt | 19 + .../mock-app/certs/invalid-ca/server.csr | 17 + .../mock-app/certs/invalid-ca/server.key | 27 + .../charts/mock-app/certs/negative/ca.crt | 20 + .../charts/mock-app/certs/negative/ca.key | 28 + .../charts/mock-app/certs/negative/client.crt | 21 + .../charts/mock-app/certs/negative/client.csr | 18 + .../charts/mock-app/certs/negative/client.key | 27 + .../charts/mock-app/certs/negative/server.crt | 21 + .../charts/mock-app/certs/negative/server.csr | 18 + .../charts/mock-app/certs/negative/server.key | 27 + .../charts/mock-app/certs/positive/ca.crt | 20 + .../charts/mock-app/certs/positive/ca.key | 28 + .../charts/mock-app/certs/positive/client.crt | 21 + .../charts/mock-app/certs/positive/client.csr | 18 + .../charts/mock-app/certs/positive/client.key | 27 + .../charts/mock-app/certs/positive/server.crt | 21 + .../charts/mock-app/certs/positive/server.csr | 18 + .../charts/mock-app/certs/positive/server.key | 27 + .../charts/mock-app/templates/_helpers.tpl | 12 + .../credentials/expired-mtls-cert-secret.yaml | 12 + .../credentials/mtls-cert-secret.yml | 13 + .../charts/mock-app/templates/mock-app.yml | 65 + .../gateway-test/charts/test/Chart.yaml | 24 + .../charts/test/certs/invalid-ca/ca.crt | 18 + .../charts/test/certs/invalid-ca/ca.key | 28 + .../charts/test/certs/invalid-ca/client.crt | 19 + .../charts/test/certs/invalid-ca/client.csr | 17 + .../charts/test/certs/invalid-ca/client.key | 27 + .../charts/test/certs/invalid-ca/server.crt | 19 + .../charts/test/certs/invalid-ca/server.csr | 17 + .../charts/test/certs/invalid-ca/server.key | 27 + .../charts/test/certs/negative/ca.crt | 20 + .../charts/test/certs/negative/ca.key | 28 + .../charts/test/certs/negative/client.crt | 21 + .../charts/test/certs/negative/client.csr | 18 + .../charts/test/certs/negative/client.key | 27 + .../charts/test/certs/negative/server.crt | 21 + .../charts/test/certs/negative/server.csr | 18 + .../charts/test/certs/negative/server.key | 27 + .../charts/test/certs/positive/ca.crt | 20 + .../charts/test/certs/positive/ca.key | 28 + .../charts/test/certs/positive/client.crt | 21 + .../charts/test/certs/positive/client.csr | 18 + .../charts/test/certs/positive/client.key | 27 + .../charts/test/certs/positive/server.crt | 21 + .../charts/test/certs/positive/server.csr | 18 + .../charts/test/certs/positive/server.key | 27 + .../charts/test/templates/_helpers.tpl | 12 + .../applications/code-rewriting.yaml | 47 + .../credentials/basic-auth-negative.yaml | 10 + .../credentials/basic-auth-positive.yaml | 9 + .../credentials/mtls-negative-case.yaml | 10 + .../mtls-negative-expired-client-cert.yaml | 10 + .../mtls-negative-expired-server-cert.yaml | 10 + .../credentials/mtls-negative-other-ca.yaml | 10 + ...tls-oauth-nagative-incorrect-clientid.yaml | 11 + .../mtls-oauth-nagative-other-ca.yaml | 11 + .../credentials/mtls-oauth-negative-case.yaml | 11 + ...ls-oauth-negative-expired-client-cert.yaml | 10 + ...ls-oauth-negative-expired-server-cert.yaml | 10 + .../credentials/mtls-oauth-positive.yaml | 12 + .../credentials/mtls-positive.yaml | 10 + .../oauth-negative-incorrect-id.yaml | 9 + .../oauth-negative-invalid-token.yaml | 9 + .../credentials/oauth-positive.yaml | 9 + .../credentials/redirect-basic-auth.yml | 9 + .../request-parameters-negative.yaml | 11 + .../credentials/request-parameters.yaml | 11 + .../test/templates/applications/manual.yml | 23 + .../applications/methods-with-body.yml | 40 + .../missing-resources-error-handling.yml | 101 + .../applications/negative-authorisation.yml | 254 + .../path-related-error-handling.yml | 29 + .../applications/positive-authorisation.yml | 141 + .../templates/applications/proxy-cases.yml | 38 + .../templates/applications/proxy-errors.yml | 20 + .../test/templates/applications/redirect.yml | 41 + .../charts/test/templates/service-account.yml | 33 + .../charts/test/templates/test.yml | 15 + .../resources/charts/gateway-test/values.yaml | 19 + .../installation-config/mini-kyma-os.yaml | 9 + .../installation-config/mini-kyma-skr.yaml | 10 + ...al-application-connectivity-validator.json | 24 + tests/resources/patches/coredns.yaml | 40 + tests/scripts/check-pod-logs.sh | 36 + tests/scripts/generate-self-signed-certs.sh | 47 + tests/scripts/jobguard.sh | 34 + tests/scripts/local-build.sh | 10 + tests/scripts/test-cra.sh | 21 + .../suite_test.go | 136 + .../tools.go | 5 + .../test/application-gateway/complex_test.go | 28 + tests/test/application-gateway/runner_test.go | 109 + tests/test/application-gateway/suite_test.go | 34 + tests/test/application-gateway/tools.go | 30 + tests/test/compass-runtime-agent/config.go | 19 + .../test/compass-runtime-agent/suite_test.go | 166 + .../synchronisation_test.go | 157 + .../testkit/applications/comparator.go | 125 + .../testkit/applications/comparator_test.go | 195 + .../applications/mocks/ApplicationGetter.go | 55 + .../testkit/applications/mocks/Comparator.go | 43 + .../testkit/applications/secretcomparator.go | 59 + .../applications/secretcomparator_test.go | 117 + .../testkit/director/directorclient.go | 320 + .../testkit/director/directorclient_test.go | 1142 +++ .../testkit/director/mocks/Client.go | 193 + .../testkit/director/mocks/DirectorClient.go | 60 + .../testkit/director/queryprovider.go | 127 + .../testkit/executor/toolkit.go | 53 + .../testkit/executor/toolkit_test.go | 101 + .../testkit/graphql/client.go | 76 + .../testkit/graphql/gql_client_testkit.go | 47 + .../testkit/graphql/mocks/Client.go | 42 + .../testkit/init/certificatesecrets_test.go | 78 + .../testkit/init/certificatessecrets.go | 41 + .../testkit/init/compass.go | 53 + .../testkit/init/compass_test.go | 159 + .../testkit/init/compassconnection.go | 114 + .../testkit/init/compassconnection_test.go | 139 + .../testkit/init/configurationsecret.go | 83 + .../testkit/init/configurationsecret_test.go | 73 + .../testkit/init/deployment.go | 160 + .../testkit/init/init.go | 140 + .../testkit/init/init_test.go | 232 + .../mocks/CertificateSecretConfigurator.go | 51 + .../init/types/mocks/CompassConfigurator.go | 58 + .../mocks/CompassConnectionConfigurator.go | 51 + .../mocks/ConfigurationSecretConfigurator.go | 51 + .../types/mocks/DeploymentConfigurator.go | 54 + .../init/types/mocks/DirectorClient.go | 130 + .../testkit/init/types/types.go | 45 + .../testkit/oauth/client.go | 119 + .../testkit/oauth/client_test.go | 109 + .../testkit/oauth/mocks/Client.go | 49 + .../testkit/oauth/types.go | 38 + .../testkit/oauth/types_test.go | 53 + .../testkit/random/randomstring.go | 25 + .../third_party/machinebox/graphql/LICENSE | 201 + .../third_party/machinebox/graphql/README.md | 67 + .../third_party/machinebox/graphql/graphql.go | 354 + .../machinebox/graphql/graphql_json_test.go | 233 + .../graphql/graphql_multipart_test.go | 302 + tests/tools/external-api-mock-app/config.go | 54 + tests/tools/external-api-mock-app/server.go | 79 + 235 files changed, 20883 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/kyma-integration-k3d-app-gateway.yml create mode 100644 tests/.dockerignore create mode 100644 tests/.gitignore create mode 100644 tests/Dockerfile.compass-runtime-agent create mode 100644 tests/Dockerfile.connectivity-validator create mode 100644 tests/Dockerfile.gateway create mode 100644 tests/Dockerfile.mockapp create mode 100644 tests/Makefile create mode 100644 tests/Makefile.test-application-conn-validator create mode 100644 tests/Makefile.test-application-gateway create mode 100644 tests/Makefile.test-compass-runtime-agent create mode 100644 tests/README.md create mode 100644 tests/docs/application-connectivity-validator-tests.md create mode 100644 tests/docs/application-gateway-tests.md create mode 100644 tests/docs/assets/api-auth-methods-mtls.png create mode 100644 tests/docs/assets/api-auth-methods.png create mode 100644 tests/docs/assets/api-tokens-mtls.png create mode 100644 tests/docs/assets/api-tokens.png create mode 100644 tests/docs/assets/app-gateway-tests-architecture.svg create mode 100644 tests/docs/assets/compass-runtime-agent-tests-architecture.svg create mode 100644 tests/docs/assets/connectivity-validator-tests-architecture.svg create mode 100644 tests/docs/assets/mock-app-mtls-spec.yaml create mode 100644 tests/docs/assets/mock-app-spec.yaml create mode 100644 tests/docs/compass-runtime-agent-tests.md create mode 100644 tests/go.mod create mode 100644 tests/go.sum create mode 100644 tests/hack/ci/.srl create mode 100644 tests/hack/ci/Makefile create mode 100644 tests/hack/ci/deps/application-connector-cr.yaml create mode 100644 tests/hack/ci/deps/application-connector-manager.yaml create mode 100644 tests/hack/ci/deps/applications.applicationconnector.crd.yaml create mode 100644 tests/hack/ci/deps/istio-default-cr.yaml create mode 100644 tests/hack/ci/deps/istio-manager.yaml create mode 100644 tests/hack/ci/resources/charts/gateway-test/charts/test/certs/invalid-ca/ca.crt create mode 100644 tests/hack/ci/resources/charts/gateway-test/charts/test/certs/invalid-ca/ca.key create mode 100644 tests/hack/ci/resources/charts/gateway-test/charts/test/certs/invalid-ca/client.crt create mode 100644 tests/hack/ci/resources/charts/gateway-test/charts/test/certs/invalid-ca/client.csr create mode 100644 tests/hack/ci/resources/charts/gateway-test/charts/test/certs/invalid-ca/client.key create mode 100644 tests/hack/ci/resources/charts/gateway-test/charts/test/certs/invalid-ca/server.crt create mode 100644 tests/hack/ci/resources/charts/gateway-test/charts/test/certs/invalid-ca/server.csr create mode 100644 tests/hack/ci/resources/charts/gateway-test/charts/test/certs/invalid-ca/server.key create mode 100644 tests/hack/ci/resources/charts/gateway-test/charts/test/certs/negative/ca.crt create mode 100644 tests/hack/ci/resources/charts/gateway-test/charts/test/certs/negative/ca.key create mode 100644 tests/hack/ci/resources/charts/gateway-test/charts/test/certs/negative/client.crt create mode 100644 tests/hack/ci/resources/charts/gateway-test/charts/test/certs/negative/client.csr create mode 100644 tests/hack/ci/resources/charts/gateway-test/charts/test/certs/negative/client.key create mode 100644 tests/hack/ci/resources/charts/gateway-test/charts/test/certs/negative/server.crt create mode 100644 tests/hack/ci/resources/charts/gateway-test/charts/test/certs/negative/server.csr create mode 100644 tests/hack/ci/resources/charts/gateway-test/charts/test/certs/negative/server.key create mode 100644 tests/hack/ci/resources/charts/gateway-test/charts/test/certs/positive/ca.crt create mode 100644 tests/hack/ci/resources/charts/gateway-test/charts/test/certs/positive/ca.key create mode 100644 tests/hack/ci/resources/charts/gateway-test/charts/test/certs/positive/client.crt create mode 100644 tests/hack/ci/resources/charts/gateway-test/charts/test/certs/positive/client.csr create mode 100644 tests/hack/ci/resources/charts/gateway-test/charts/test/certs/positive/client.key create mode 100644 tests/hack/ci/resources/charts/gateway-test/charts/test/certs/positive/server.crt create mode 100644 tests/hack/ci/resources/charts/gateway-test/charts/test/certs/positive/server.csr create mode 100644 tests/hack/ci/resources/charts/gateway-test/charts/test/certs/positive/server.key create mode 100644 tests/internal/testkit/httpd/http.go create mode 100644 tests/internal/testkit/test-api/apis.go create mode 100644 tests/internal/testkit/test-api/basicauth.go create mode 100644 tests/internal/testkit/test-api/csrf.go create mode 100644 tests/internal/testkit/test-api/handlers.go create mode 100644 tests/internal/testkit/test-api/oauth.go create mode 100644 tests/internal/testkit/test-api/requestparams.go create mode 100644 tests/resources/charts/application-connectivity-validator-test/Chart.yaml create mode 100644 tests/resources/charts/application-connectivity-validator-test/charts/echoserver/Chart.yaml create mode 100644 tests/resources/charts/application-connectivity-validator-test/charts/echoserver/templates/echoserver.yml create mode 100644 tests/resources/charts/application-connectivity-validator-test/charts/test/Chart.yaml create mode 100644 tests/resources/charts/application-connectivity-validator-test/charts/test/templates/_helpers.tpl create mode 100644 tests/resources/charts/application-connectivity-validator-test/charts/test/templates/applications/event-test-compass.yml create mode 100644 tests/resources/charts/application-connectivity-validator-test/charts/test/templates/applications/event-test-standalone.yml create mode 100644 tests/resources/charts/application-connectivity-validator-test/charts/test/templates/test.yml create mode 100644 tests/resources/charts/application-connectivity-validator-test/values.yaml create mode 100644 tests/resources/charts/compass-runtime-agent-test/Chart.yaml create mode 100644 tests/resources/charts/compass-runtime-agent-test/templates/_helpers.tpl create mode 100644 tests/resources/charts/compass-runtime-agent-test/templates/applications/test-create-app.yaml create mode 100644 tests/resources/charts/compass-runtime-agent-test/templates/secret-compass.yaml create mode 100644 tests/resources/charts/compass-runtime-agent-test/templates/service-account.yaml create mode 100644 tests/resources/charts/compass-runtime-agent-test/templates/test.yaml create mode 100644 tests/resources/charts/compass-runtime-agent-test/values.yaml create mode 100644 tests/resources/charts/gateway-test/Chart.yaml create mode 100644 tests/resources/charts/gateway-test/charts/mock-app/Chart.yaml create mode 100644 tests/resources/charts/gateway-test/charts/mock-app/certs/invalid-ca/ca.crt create mode 100644 tests/resources/charts/gateway-test/charts/mock-app/certs/invalid-ca/ca.key create mode 100644 tests/resources/charts/gateway-test/charts/mock-app/certs/invalid-ca/client.crt create mode 100644 tests/resources/charts/gateway-test/charts/mock-app/certs/invalid-ca/client.csr create mode 100644 tests/resources/charts/gateway-test/charts/mock-app/certs/invalid-ca/client.key create mode 100644 tests/resources/charts/gateway-test/charts/mock-app/certs/invalid-ca/server.crt create mode 100644 tests/resources/charts/gateway-test/charts/mock-app/certs/invalid-ca/server.csr create mode 100644 tests/resources/charts/gateway-test/charts/mock-app/certs/invalid-ca/server.key create mode 100644 tests/resources/charts/gateway-test/charts/mock-app/certs/negative/ca.crt create mode 100644 tests/resources/charts/gateway-test/charts/mock-app/certs/negative/ca.key create mode 100644 tests/resources/charts/gateway-test/charts/mock-app/certs/negative/client.crt create mode 100644 tests/resources/charts/gateway-test/charts/mock-app/certs/negative/client.csr create mode 100644 tests/resources/charts/gateway-test/charts/mock-app/certs/negative/client.key create mode 100644 tests/resources/charts/gateway-test/charts/mock-app/certs/negative/server.crt create mode 100644 tests/resources/charts/gateway-test/charts/mock-app/certs/negative/server.csr create mode 100644 tests/resources/charts/gateway-test/charts/mock-app/certs/negative/server.key create mode 100644 tests/resources/charts/gateway-test/charts/mock-app/certs/positive/ca.crt create mode 100644 tests/resources/charts/gateway-test/charts/mock-app/certs/positive/ca.key create mode 100644 tests/resources/charts/gateway-test/charts/mock-app/certs/positive/client.crt create mode 100644 tests/resources/charts/gateway-test/charts/mock-app/certs/positive/client.csr create mode 100644 tests/resources/charts/gateway-test/charts/mock-app/certs/positive/client.key create mode 100644 tests/resources/charts/gateway-test/charts/mock-app/certs/positive/server.crt create mode 100644 tests/resources/charts/gateway-test/charts/mock-app/certs/positive/server.csr create mode 100644 tests/resources/charts/gateway-test/charts/mock-app/certs/positive/server.key create mode 100644 tests/resources/charts/gateway-test/charts/mock-app/templates/_helpers.tpl create mode 100644 tests/resources/charts/gateway-test/charts/mock-app/templates/credentials/expired-mtls-cert-secret.yaml create mode 100644 tests/resources/charts/gateway-test/charts/mock-app/templates/credentials/mtls-cert-secret.yml create mode 100644 tests/resources/charts/gateway-test/charts/mock-app/templates/mock-app.yml create mode 100644 tests/resources/charts/gateway-test/charts/test/Chart.yaml create mode 100644 tests/resources/charts/gateway-test/charts/test/certs/invalid-ca/ca.crt create mode 100644 tests/resources/charts/gateway-test/charts/test/certs/invalid-ca/ca.key create mode 100644 tests/resources/charts/gateway-test/charts/test/certs/invalid-ca/client.crt create mode 100644 tests/resources/charts/gateway-test/charts/test/certs/invalid-ca/client.csr create mode 100644 tests/resources/charts/gateway-test/charts/test/certs/invalid-ca/client.key create mode 100644 tests/resources/charts/gateway-test/charts/test/certs/invalid-ca/server.crt create mode 100644 tests/resources/charts/gateway-test/charts/test/certs/invalid-ca/server.csr create mode 100644 tests/resources/charts/gateway-test/charts/test/certs/invalid-ca/server.key create mode 100644 tests/resources/charts/gateway-test/charts/test/certs/negative/ca.crt create mode 100644 tests/resources/charts/gateway-test/charts/test/certs/negative/ca.key create mode 100644 tests/resources/charts/gateway-test/charts/test/certs/negative/client.crt create mode 100644 tests/resources/charts/gateway-test/charts/test/certs/negative/client.csr create mode 100644 tests/resources/charts/gateway-test/charts/test/certs/negative/client.key create mode 100644 tests/resources/charts/gateway-test/charts/test/certs/negative/server.crt create mode 100644 tests/resources/charts/gateway-test/charts/test/certs/negative/server.csr create mode 100644 tests/resources/charts/gateway-test/charts/test/certs/negative/server.key create mode 100644 tests/resources/charts/gateway-test/charts/test/certs/positive/ca.crt create mode 100644 tests/resources/charts/gateway-test/charts/test/certs/positive/ca.key create mode 100644 tests/resources/charts/gateway-test/charts/test/certs/positive/client.crt create mode 100644 tests/resources/charts/gateway-test/charts/test/certs/positive/client.csr create mode 100644 tests/resources/charts/gateway-test/charts/test/certs/positive/client.key create mode 100644 tests/resources/charts/gateway-test/charts/test/certs/positive/server.crt create mode 100644 tests/resources/charts/gateway-test/charts/test/certs/positive/server.csr create mode 100644 tests/resources/charts/gateway-test/charts/test/certs/positive/server.key create mode 100644 tests/resources/charts/gateway-test/charts/test/templates/_helpers.tpl create mode 100644 tests/resources/charts/gateway-test/charts/test/templates/applications/code-rewriting.yaml create mode 100644 tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/basic-auth-negative.yaml create mode 100644 tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/basic-auth-positive.yaml create mode 100644 tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/mtls-negative-case.yaml create mode 100644 tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/mtls-negative-expired-client-cert.yaml create mode 100644 tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/mtls-negative-expired-server-cert.yaml create mode 100644 tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/mtls-negative-other-ca.yaml create mode 100644 tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/mtls-oauth-nagative-incorrect-clientid.yaml create mode 100644 tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/mtls-oauth-nagative-other-ca.yaml create mode 100644 tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/mtls-oauth-negative-case.yaml create mode 100644 tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/mtls-oauth-negative-expired-client-cert.yaml create mode 100644 tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/mtls-oauth-negative-expired-server-cert.yaml create mode 100644 tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/mtls-oauth-positive.yaml create mode 100644 tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/mtls-positive.yaml create mode 100644 tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/oauth-negative-incorrect-id.yaml create mode 100644 tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/oauth-negative-invalid-token.yaml create mode 100644 tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/oauth-positive.yaml create mode 100644 tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/redirect-basic-auth.yml create mode 100644 tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/request-parameters-negative.yaml create mode 100644 tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/request-parameters.yaml create mode 100644 tests/resources/charts/gateway-test/charts/test/templates/applications/manual.yml create mode 100644 tests/resources/charts/gateway-test/charts/test/templates/applications/methods-with-body.yml create mode 100644 tests/resources/charts/gateway-test/charts/test/templates/applications/missing-resources-error-handling.yml create mode 100644 tests/resources/charts/gateway-test/charts/test/templates/applications/negative-authorisation.yml create mode 100644 tests/resources/charts/gateway-test/charts/test/templates/applications/path-related-error-handling.yml create mode 100644 tests/resources/charts/gateway-test/charts/test/templates/applications/positive-authorisation.yml create mode 100644 tests/resources/charts/gateway-test/charts/test/templates/applications/proxy-cases.yml create mode 100644 tests/resources/charts/gateway-test/charts/test/templates/applications/proxy-errors.yml create mode 100644 tests/resources/charts/gateway-test/charts/test/templates/applications/redirect.yml create mode 100644 tests/resources/charts/gateway-test/charts/test/templates/service-account.yml create mode 100644 tests/resources/charts/gateway-test/charts/test/templates/test.yml create mode 100644 tests/resources/charts/gateway-test/values.yaml create mode 100644 tests/resources/installation-config/mini-kyma-os.yaml create mode 100644 tests/resources/installation-config/mini-kyma-skr.yaml create mode 100644 tests/resources/patches/central-application-connectivity-validator.json create mode 100644 tests/resources/patches/coredns.yaml create mode 100755 tests/scripts/check-pod-logs.sh create mode 100755 tests/scripts/generate-self-signed-certs.sh create mode 100755 tests/scripts/jobguard.sh create mode 100755 tests/scripts/local-build.sh create mode 100755 tests/scripts/test-cra.sh create mode 100644 tests/test/application-connectivity-validator/suite_test.go create mode 100644 tests/test/application-connectivity-validator/tools.go create mode 100644 tests/test/application-gateway/complex_test.go create mode 100644 tests/test/application-gateway/runner_test.go create mode 100644 tests/test/application-gateway/suite_test.go create mode 100644 tests/test/application-gateway/tools.go create mode 100644 tests/test/compass-runtime-agent/config.go create mode 100644 tests/test/compass-runtime-agent/suite_test.go create mode 100644 tests/test/compass-runtime-agent/synchronisation_test.go create mode 100644 tests/test/compass-runtime-agent/testkit/applications/comparator.go create mode 100644 tests/test/compass-runtime-agent/testkit/applications/comparator_test.go create mode 100644 tests/test/compass-runtime-agent/testkit/applications/mocks/ApplicationGetter.go create mode 100644 tests/test/compass-runtime-agent/testkit/applications/mocks/Comparator.go create mode 100644 tests/test/compass-runtime-agent/testkit/applications/secretcomparator.go create mode 100644 tests/test/compass-runtime-agent/testkit/applications/secretcomparator_test.go create mode 100644 tests/test/compass-runtime-agent/testkit/director/directorclient.go create mode 100644 tests/test/compass-runtime-agent/testkit/director/directorclient_test.go create mode 100644 tests/test/compass-runtime-agent/testkit/director/mocks/Client.go create mode 100644 tests/test/compass-runtime-agent/testkit/director/mocks/DirectorClient.go create mode 100644 tests/test/compass-runtime-agent/testkit/director/queryprovider.go create mode 100644 tests/test/compass-runtime-agent/testkit/executor/toolkit.go create mode 100644 tests/test/compass-runtime-agent/testkit/executor/toolkit_test.go create mode 100644 tests/test/compass-runtime-agent/testkit/graphql/client.go create mode 100644 tests/test/compass-runtime-agent/testkit/graphql/gql_client_testkit.go create mode 100644 tests/test/compass-runtime-agent/testkit/graphql/mocks/Client.go create mode 100644 tests/test/compass-runtime-agent/testkit/init/certificatesecrets_test.go create mode 100644 tests/test/compass-runtime-agent/testkit/init/certificatessecrets.go create mode 100644 tests/test/compass-runtime-agent/testkit/init/compass.go create mode 100644 tests/test/compass-runtime-agent/testkit/init/compass_test.go create mode 100644 tests/test/compass-runtime-agent/testkit/init/compassconnection.go create mode 100644 tests/test/compass-runtime-agent/testkit/init/compassconnection_test.go create mode 100644 tests/test/compass-runtime-agent/testkit/init/configurationsecret.go create mode 100644 tests/test/compass-runtime-agent/testkit/init/configurationsecret_test.go create mode 100644 tests/test/compass-runtime-agent/testkit/init/deployment.go create mode 100644 tests/test/compass-runtime-agent/testkit/init/init.go create mode 100644 tests/test/compass-runtime-agent/testkit/init/init_test.go create mode 100644 tests/test/compass-runtime-agent/testkit/init/types/mocks/CertificateSecretConfigurator.go create mode 100644 tests/test/compass-runtime-agent/testkit/init/types/mocks/CompassConfigurator.go create mode 100644 tests/test/compass-runtime-agent/testkit/init/types/mocks/CompassConnectionConfigurator.go create mode 100644 tests/test/compass-runtime-agent/testkit/init/types/mocks/ConfigurationSecretConfigurator.go create mode 100644 tests/test/compass-runtime-agent/testkit/init/types/mocks/DeploymentConfigurator.go create mode 100644 tests/test/compass-runtime-agent/testkit/init/types/mocks/DirectorClient.go create mode 100644 tests/test/compass-runtime-agent/testkit/init/types/types.go create mode 100644 tests/test/compass-runtime-agent/testkit/oauth/client.go create mode 100644 tests/test/compass-runtime-agent/testkit/oauth/client_test.go create mode 100644 tests/test/compass-runtime-agent/testkit/oauth/mocks/Client.go create mode 100644 tests/test/compass-runtime-agent/testkit/oauth/types.go create mode 100644 tests/test/compass-runtime-agent/testkit/oauth/types_test.go create mode 100644 tests/test/compass-runtime-agent/testkit/random/randomstring.go create mode 100644 tests/test/compass-runtime-agent/testkit/third_party/machinebox/graphql/LICENSE create mode 100644 tests/test/compass-runtime-agent/testkit/third_party/machinebox/graphql/README.md create mode 100644 tests/test/compass-runtime-agent/testkit/third_party/machinebox/graphql/graphql.go create mode 100644 tests/test/compass-runtime-agent/testkit/third_party/machinebox/graphql/graphql_json_test.go create mode 100644 tests/test/compass-runtime-agent/testkit/third_party/machinebox/graphql/graphql_multipart_test.go create mode 100644 tests/tools/external-api-mock-app/config.go create mode 100644 tests/tools/external-api-mock-app/server.go diff --git a/.github/workflows/kyma-integration-k3d-app-gateway.yml b/.github/workflows/kyma-integration-k3d-app-gateway.yml new file mode 100644 index 00000000..ba97bb3f --- /dev/null +++ b/.github/workflows/kyma-integration-k3d-app-gateway.yml @@ -0,0 +1,20 @@ +name: Run app-gateway integration tests on k3d +on: + push: + branches: [ main ] + pull_request: +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: azure/setup-helm@v4.1.0 + id: install + - name: Checkout code + uses: actions/checkout@v3 + - name: Install k3d + env: + K3D_URL: https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh + DEFAULT_K3D_VERSION: v5.4.6 + run: curl --silent --fail $K3D_URL | TAG=$DEFAULT_K3D_VERSION bash + - name: Run unit tests + run: make -C tests/hack/ci k3d-gateway-tests diff --git a/.github/workflows/run-tests.yaml b/.github/workflows/run-tests.yaml index a5b2ce37..6f821bfe 100644 --- a/.github/workflows/run-tests.yaml +++ b/.github/workflows/run-tests.yaml @@ -11,7 +11,6 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v3 - - name: Set up cache uses: actions/cache@v3 with: @@ -22,12 +21,10 @@ jobs: key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- - - name: Set up go environment uses: actions/setup-go@v4 with: go-version: 1.21 - - name: Run unit tests run: make test diff --git a/.gitignore b/.gitignore index 71607907..d4281c39 100644 --- a/.gitignore +++ b/.gitignore @@ -30,4 +30,4 @@ manifests/* mod bin -charts \ No newline at end of file +charts/**/* diff --git a/tests/.dockerignore b/tests/.dockerignore new file mode 100644 index 00000000..63055f97 --- /dev/null +++ b/tests/.dockerignore @@ -0,0 +1,2 @@ +deployments/ +README.md diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 00000000..9dc57812 --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1 @@ +resources/charts/gateway-test/certs/ \ No newline at end of file diff --git a/tests/Dockerfile.compass-runtime-agent b/tests/Dockerfile.compass-runtime-agent new file mode 100644 index 00000000..08813029 --- /dev/null +++ b/tests/Dockerfile.compass-runtime-agent @@ -0,0 +1,17 @@ +# image builder base on golang:1.21.1-alpine3.18 +FROM golang@sha256:0c860c7ceba62231d0f99fb92e9d7c1577f26fea794a12c75756a8f64b146e45 as builder + +WORKDIR /compass-test/ + +COPY go.mod go.sum ./ +RUN go mod download && go mod verify + +COPY . . + +RUN CGO_ENABLED=0 go test -v -c -o compass-test ./test/compass-runtime-agent/ + +FROM scratch + +COPY --from=builder /compass-test/compass-test / +ENTRYPOINT [ "/compass-test" ] +CMD ["-test.v", "-test.parallel", "1"] diff --git a/tests/Dockerfile.connectivity-validator b/tests/Dockerfile.connectivity-validator new file mode 100644 index 00000000..85aa6c17 --- /dev/null +++ b/tests/Dockerfile.connectivity-validator @@ -0,0 +1,17 @@ +# image builder base on golang:1.21.1-alpine3.18 +FROM golang@sha256:0c860c7ceba62231d0f99fb92e9d7c1577f26fea794a12c75756a8f64b146e45 as builder + +WORKDIR /validator-test/ + +COPY go.mod go.sum ./ +RUN go mod download && go mod verify + +COPY . . + +RUN CGO_ENABLED=0 go test -v -c -o validator-test ./test/application-connectivity-validator/ + +FROM scratch + +COPY --from=builder /validator-test/validator-test / +ENTRYPOINT [ "/validator-test" ] +CMD ["-test.v"] diff --git a/tests/Dockerfile.gateway b/tests/Dockerfile.gateway new file mode 100644 index 00000000..9e620a90 --- /dev/null +++ b/tests/Dockerfile.gateway @@ -0,0 +1,17 @@ +# image builder base on golang:1.21.1-alpine3.18 +FROM golang@sha256:0c860c7ceba62231d0f99fb92e9d7c1577f26fea794a12c75756a8f64b146e45 as builder + +WORKDIR /gateway-test/ + +COPY go.mod go.sum ./ +RUN go mod download && go mod verify + +COPY . . + +RUN CGO_ENABLED=0 go test -v -c -o gateway-test ./test/application-gateway/ + +FROM scratch + +COPY --from=builder /gateway-test/gateway-test / +ENTRYPOINT [ "/gateway-test" ] +CMD ["-test.v"] diff --git a/tests/Dockerfile.mockapp b/tests/Dockerfile.mockapp new file mode 100644 index 00000000..14203dcf --- /dev/null +++ b/tests/Dockerfile.mockapp @@ -0,0 +1,17 @@ +# image builder base on golang:1.21.1-alpine3.18 +FROM golang@sha256:0c860c7ceba62231d0f99fb92e9d7c1577f26fea794a12c75756a8f64b146e45 as builder + +WORKDIR /mock-app/ + +COPY go.mod go.sum ./ +RUN go mod download && go mod verify + +COPY . . + + +RUN CGO_ENABLED=0 go build -v -o mock-app ./tools/external-api-mock-app + +FROM scratch +COPY --from=builder /mock-app/mock-app . +ENTRYPOINT [ "/mock-app" ] +CMD [] diff --git a/tests/Makefile b/tests/Makefile new file mode 100644 index 00000000..0be826eb --- /dev/null +++ b/tests/Makefile @@ -0,0 +1,33 @@ +GATEWAY_TEST_IMAGE = "$(DOCKER_PUSH_REPOSITORY)$(DOCKER_PUSH_DIRECTORY)/gateway-test:$(DOCKER_TAG)" +VALIDATOR_TEST_IMAGE = "$(DOCKER_PUSH_REPOSITORY)$(DOCKER_PUSH_DIRECTORY)/connectivity-validator-test:$(DOCKER_TAG)" +COMPASS_TEST_IMAGE = "$(DOCKER_PUSH_REPOSITORY)$(DOCKER_PUSH_DIRECTORY)/compass-runtime-agent-test:$(DOCKER_TAG)" +MOCK_APP_IMAGE = "$(DOCKER_PUSH_REPOSITORY)$(DOCKER_PUSH_DIRECTORY)/mock-app:$(DOCKER_TAG)" + +.PHONY: release image + +release: publish-gateway-test publish-mock-app publish-validator-test publish-compass-runtime-agent-test +image: image-gateway-test image-validator-test image-compass-runtime-agent-test + +publish-gateway-test: image-gateway-test + docker push $(GATEWAY_TEST_IMAGE) + +image-gateway-test: + docker build -t $(GATEWAY_TEST_IMAGE) -f Dockerfile.gateway . + +publish-mock-app: image-mock-app + docker push $(MOCK_APP_IMAGE) + +image-mock-app: + docker build -t $(MOCK_APP_IMAGE) -f Dockerfile.mockapp . + +publish-validator-test: image-validator-test + docker push $(VALIDATOR_TEST_IMAGE) + +image-validator-test: + docker build -t $(VALIDATOR_TEST_IMAGE) -f Dockerfile.connectivity-validator . + +publish-compass-runtime-agent-test: image-compass-runtime-agent-test + docker push $(COMPASS_TEST_IMAGE) + +image-compass-runtime-agent-test: + docker build -t $(COMPASS_TEST_IMAGE) -f Dockerfile.compass-runtime-agent . diff --git a/tests/Makefile.test-application-conn-validator b/tests/Makefile.test-application-conn-validator new file mode 100644 index 00000000..f408cdd7 --- /dev/null +++ b/tests/Makefile.test-application-conn-validator @@ -0,0 +1,59 @@ +# -*- mode: Makefile -*- + +NAMESPACE ?= test +GOPATH ?= $(shell go env GOPATH) + +VALIDATOR_TEST_IMAGE = "$(DOCKER_PUSH_REPOSITORY)$(DOCKER_PUSH_DIRECTORY)/connectivity-validator-test:$(DOCKER_TAG)" +TEST_TIMEOUT = "3m" +MAKEFILE_NAME=Makefile.test-application-conn-validator + +.PHONY: test clean +.PHONY: patch-for-validator-test unpatch-after-validator-test test-validator test-validator-debug validator-create-resources clean-validator-test publish-validator-test + +test: test-validator +clean: clean-validator-test + +patch-for-validator-test: + kubectl -n kyma-system patch deployment central-application-connectivity-validator --type json --patch-file resources/patches/central-application-connectivity-validator.json + kubectl rollout status deploy central-application-connectivity-validator -n kyma-system --timeout=1m + +unpatch-after-validator-test: + kubectl rollout undo deployment/central-application-connectivity-validator -n kyma-system + +test-validator: patch-for-validator-test validator-create-resources + if kubectl wait --for=condition=complete --timeout=$(TEST_TIMEOUT) -n $(NAMESPACE) job/application-connectivity-validator-test; then \ + echo "Success! Results:"; \ + ./scripts/check-pod-logs.sh application-connectivity-validator-test; \ + $(MAKE) clean-validator-test -f $(MAKEFILE_NAME); \ + else \ + echo "Tests failed! Results:"; \ + ./scripts/check-pod-logs.sh application-connectivity-validator-test; \ + $(MAKE) clean-validator-test -f $(MAKEFILE_NAME); \ + exit 1; \ + fi + +test-validator-debug: patch-for-validator-test validator-create-resources + kubectl wait --for=condition=complete --timeout=$(TEST_TIMEOUT) -n $(NAMESPACE) job/application-connectivity-validator-test; \ + echo "Results:"; \ + ./scripts/check-pod-logs.sh application-connectivity-validator-test; \ + +validator-create-resources: + kubectl create namespace $(NAMESPACE) --dry-run=client -o yaml | kubectl apply -f - + kubectl label namespace $(NAMESPACE) istio-injection=enabled --overwrite + + helm template resources/charts/application-connectivity-validator-test/charts/echoserver \ + --set global.namespace=$(NAMESPACE) \ + | kubectl apply -f - + kubectl rollout status deployment echoserver -n test --timeout=90s + + @helm template resources/charts/application-connectivity-validator-test/charts/test \ + --set namespace=$(NAMESPACE) \ + --values resources/charts/application-connectivity-validator-test/values.yaml \ + | kubectl apply -f - + +clean-validator-test: unpatch-after-validator-test + helm template resources/charts/application-connectivity-validator-test --set namespace=$(NAMESPACE) | kubectl delete -f - + kubectl delete ns $(NAMESPACE) --ignore-not-found + + + diff --git a/tests/Makefile.test-application-gateway b/tests/Makefile.test-application-gateway new file mode 100644 index 00000000..7ec930ae --- /dev/null +++ b/tests/Makefile.test-application-gateway @@ -0,0 +1,82 @@ +# -*- mode: PWDmakefile -*- + +NAMESPACE ?= test +GOPATH ?= $(shell go env GOPATH) + +MOCK_SERVICE_NAME="mock-application" +APP_URL = "$(MOCK_SERVICE_NAME).$(NAMESPACE).svc.cluster.local" +TEST_TIMEOUT = "3m" +MAKEFILE_NAME=Makefile.test-application-gateway + +.PHONY: test clean +.PHONY: test-gateway test-gateway-debug clean-gateway-test disable-sidecar-for-mtls-test enable-sidecar-after-mtls-test generate-certs + +test: test-gateway +clean: clean-gateway-test + +test-gateway: disable-sidecar-for-mtls-test generate-certs create-resources + @echo "::group::test-gateway" + if kubectl wait --for=condition=complete --timeout=$(TEST_TIMEOUT) -n $(NAMESPACE) job/application-gateway-test; then \ + echo "Success! Results:"; \ + ${PWD}/scripts/check-pod-logs.sh application-gateway-test; \ + $(MAKE) clean-gateway-test -f ${PWD}/tests/$(MAKEFILE_NAME); \ + else \ + echo "Tests failed! Results:"; \ + ${PWD}/scripts/check-pod-logs.sh application-gateway-test; \ + $(MAKE) clean-gateway-test -f ${PWD}/tests/$(MAKEFILE_NAME); \ + exit 1; \ + fi + @echo "::endgroup::" + +test-gateway-debug: disable-sidecar-for-mtls-test generate-certs create-resources + @echo "::group::test-gateway-debug" + kubectl wait --for=condition=complete --timeout=$(TEST_TIMEOUT) -n $(NAMESPACE) job/application-gateway-test + @echo "Results:" + ${PWD}/scripts/check-pod-logs.sh application-gateway-test + @echo "::endgroup::" + +create-resources: + @echo "::group::create-resources" + kubectl create namespace $(NAMESPACE) --dry-run=client -o yaml | kubectl apply -f - + kubectl label namespace $(NAMESPACE) istio-injection=enabled --overwrite + @echo "::group::create-resources::install-mock-app" + helm template ${PWD}/tests/resources/charts/gateway-test/charts/mock-app \ + --set global.namespace=$(NAMESPACE) \ + --set mockServiceName=$(MOCK_SERVICE_NAME) \ + --values ${PWD}/tests/resources/charts/gateway-test/values.yaml \ + | kubectl apply -f - + kubectl rollout status deployment mock-application -n test --timeout=90s + @echo "::endgroup::" + @echo "::group::create-resources::install-mock-app" + helm template ${PWD}/tests/resources/charts/gateway-test/charts/test \ + --set namespace=$(NAMESPACE) \ + --set mockServiceName=$(MOCK_SERVICE_NAME) \ + --values ${PWD}/tests/resources/charts/gateway-test/values.yaml \ + | kubectl apply -f - + @echo "::endgroup::" + @echo "::endgroup::" + +clean-gateway-test: + @echo "::group::clean-gateway-test" + helm template ${PWD}/../../resources/charts/gateway-test --set namespace=$(NAMESPACE) | kubectl delete -f - + kubectl delete ns $(NAMESPACE) --ignore-not-found + @echo "::endgroup::" + +disable-sidecar-for-mtls-test: + @echo "::group::disable-sidecar-for-mtls-test" + kubectl -n kyma-system patch deployment central-application-gateway -p '{"spec":{"template":{"metadata":{"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts": "8090,8091"}}}}}' + kubectl rollout status deploy central-application-gateway -n kyma-system --timeout=1m + @echo "::endgroup::" + +enable-sidecar-after-mtls-test: + @echo "::group::enable-sidecar-for-mtls-test" + kubectl -n kyma-system patch deployment central-application-gateway --type=json --patch '[{ "op": "remove", "path": "/spec/template/metadata/annotations/traffic.sidecar.istio.io~1excludeOutboundPorts"}]' + @echo "::endgroup::" + +generate-certs: + @echo "::group::generate-certs" + ${PWD}/tests/scripts/generate-self-signed-certs.sh $(APP_URL) ${PWD}/tests/resources/charts/gateway-test/charts/test/certs/positive + ${PWD}/tests/scripts/generate-self-signed-certs.sh $(APP_URL) ${PWD}/tests/resources/charts/gateway-test/charts/test/certs/negative + ${PWD}/tests/scripts/generate-self-signed-certs.sh test-other-ca ${PWD}/tests/resources/charts/gateway-test/charts/test/certs/invalid-ca + cp -p -R ${PWD}/tests/resources/charts/gateway-test/charts/test/certs ${PWD}/tests/resources/charts/gateway-test/charts/mock-app + @echo "::endgroup::" diff --git a/tests/Makefile.test-compass-runtime-agent b/tests/Makefile.test-compass-runtime-agent new file mode 100644 index 00000000..f2adc01d --- /dev/null +++ b/tests/Makefile.test-compass-runtime-agent @@ -0,0 +1,38 @@ +# -*- mode: Makefile -*- + +NAMESPACE ?= test +GOPATH ?= $(shell go env GOPATH) +DIRECTOR_URL=https://compass-gateway-auth-oauth.$(COMPASS_HOST)/director/graphql +TOKENS_ENDPOINT=https://oauth2.${COMPASS_HOST}/oauth2/token + +COMPASS_TEST_IMAGE = "$(DOCKER_PUSH_REPOSITORY)$(DOCKER_PUSH_DIRECTORY)/compass-runtime-agent-test:$(DOCKER_TAG)" + +.PHONY: release test image clean +.PHONY: test-compass-runtime-agent test-compass-runtime-agent-debug clean-compass-runtime-agent-test image-compass-runtime-agent-test publish-compass-runtime-agent-test + +test: test-compass-runtime-agent +clean: clean-compass-runtime-agent-test + +test-compass-runtime-agent: test-compass-runtime-agent-debug clean-compass-runtime-agent-test + +test-compass-runtime-agent-debug: + @echo $(GOPATH)/bin/go-junit-report --help + kubectl create namespace $(NAMESPACE) --dry-run=client -o yaml | kubectl apply -f - + kubectl label namespace $(NAMESPACE) istio-injection=enabled --overwrite + + @helm template resources/charts/compass-runtime-agent-test \ + --set namespace=$(NAMESPACE) \ + --set compassCredentials.clientID=$(COMPASS_CLIENT_ID) \ + --set compassCredentials.clientSecret=$(COMPASS_CLIENT_SECRET) \ + --set compassCredentials.tokensEndpoint=$(TOKENS_ENDPOINT) \ + --set directorUrl=$(DIRECTOR_URL) \ + | kubectl apply -f - + + @echo "" + @echo "Compass test results:" + + ./scripts/check-pod-logs.sh compass-runtime-agent-test + +clean-compass-runtime-agent-test: + helm template resources/charts/compass-runtime-agent-test | kubectl delete -f - + kubectl delete ns $(NAMESPACE) --ignore-not-found diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 00000000..861a55c3 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,6 @@ +# Component tests for Application Connector + +There are the following component tests for Application Connector: +- [Application Gateway](docs/application-gateway-tests.md) +- [Application Connectivity Validator](docs/application-connectivity-validator-tests.md) +- [Compass Runtime Agent](docs/compass-runtime-agent-tests.md) diff --git a/tests/docs/application-connectivity-validator-tests.md b/tests/docs/application-connectivity-validator-tests.md new file mode 100644 index 00000000..c293701f --- /dev/null +++ b/tests/docs/application-connectivity-validator-tests.md @@ -0,0 +1,86 @@ +# Application Connectivity Validator + +**Table of Contents** + +- [Application Connectivity Validator](#application-connectivity-validator) + - [Design and Architecture](#design-and-architecture) + - [Building](#building) + - [Running](#running) + - [Deploy a Kyma Cluster Locally](#deploy-a-kyma-cluster-locally) + - [Run the Tests](#run-the-tests) + - [Debugging](#debugging) + - [Running Without Cleanup](#running-without-cleanup) + +## Design and Architecture + +The tests consist of: +- [Test resources](../resources/charts/application-connectivity-validator-test/) used to perform the test +- [Test runner](../test/application-connectivity-validator/) with all the test cases + +The tests are executed as a Kubernetes Job in a Kyma cluster where the tested Application Connectivity Validator is installed. The test Job is deployed in the `test` namespace. + +![Connectivity Validator tests architecture](assets/connectivity-validator-tests-architecture.svg) + +> **NOTE:** Port `8080` must be excluded from redirection to Envoy, otherwise the Connectivity Validator test Pod cannot pass the `X-Forwarded-Client-Cert` header to Connectivity Validator. + +## Building + +Pipelines build the Application Connectivity Validator test using the **release** target from the `Makefile`. + +To build **and push** the Docker images of the tests, run: + +``` sh +./scripts/local-build.sh {DOCKER_TAG} {DOCKER_PUSH_REPOSITORY} +``` + +This will build the following images: +- `{DOCKER_PUSH_REPOSITORY}/connectivity-validator-test:{DOCKER_TAG}` + +## Running + +Tests can be run on any Kyma cluster with Application Connectivity Validator. + +Pipelines run the tests using the **test-validator** target from the `Makefile`. + +### Deploy a Kyma Cluster Locally + +1. Provision a local Kubernetes cluster with k3d: + ```sh + kyma provision k3d + ``` + +2. Install the minimal set of components required to run Application Connectivity Validator **for Kyma SKR (Compass mode)**: + + ```bash + kyma deploy --components-file ./resources/installation-config/mini-kyma-skr.yaml --value global.disableLegacyConnectivity=true + ``` + + >**TIP:** Read more about Kyma installation in the [official Kyma documentation](https://kyma-project.io/#/02-get-started/01-quick-install). + +### Run the Tests + +``` sh +make -f Makefile.test-application-conn-validator test-validator +``` + +By default, the tests clean up after themselves, removing all the previously created resources and the `test` namespace. + +> **CAUTION:** If the names of your existing resources are the same as the names used in the tests, running this command overrides or removes the existing resources. + +## Debugging + +### Running Without Cleanup + +To run the tests without removing all the created resources afterwards, run them in the debugging mode. + +1. To start the tests in the debugging mode, run: + + ``` shell + make -f Makefile.test-application-conn-validator test-validator-debug + ``` + +2. Once you've finished debugging, run: + + ``` shell + make -f Makefile.test-application-conn-validator clean-validator-test + ``` diff --git a/tests/docs/application-gateway-tests.md b/tests/docs/application-gateway-tests.md new file mode 100644 index 00000000..d8c08a37 --- /dev/null +++ b/tests/docs/application-gateway-tests.md @@ -0,0 +1,233 @@ +# Application Gateway + +**Table of Contents** + +- [Application Gateway](#application-gateway) + - [Design and Architecture](#design-and-architecture) + - [Mock Application](#mock-application) + - [Certificates](#certificates) + - [API Exposed on Port `8080`](#api-exposed-on-port-8080) + - [API Exposed on Port `8090`](#api-exposed-on-port-8090) + - [API Exposed on Port `8091`](#api-exposed-on-port-8091) + - [Building](#building) + - [Running](#running) + - [Deploy a Kyma Cluster Locally](#deploy-a-kyma-cluster-locally) + - [Run the Tests](#run-the-tests) + - [Debugging](#debugging) + - [Running Locally](#running-locally) + - [Running Without Cleanup](#running-without-cleanup) + +## Design and Architecture + +The tests consist of: +- [Application CRs](../resources/charts/gateway-test/charts/test/templates/applications/) describing the test cases +- [Secrets](../resources/charts/gateway-test/charts/test/templates/applications/credentials) referenced by the Application CRs +- [Test runners](../test/application-gateway/) with various checks for the subsets of cases, grouped by the Application CRs +- [Mock application](../tools/external-api-mock-app/) which simulates the remote endpoints + +Additionally, the following resources are created in the cluster: +- [Service Account](../resources/charts/gateway-test/charts/test/templates/service-account.yml) used by the tests to read the Application CRs +- [Secrets](../resources/charts/gateway-test/charts/test/templates/applications/credentials) used by the Mock application to configure mTLS servers + +The tests are executed as a Kubernetes Job in a Kyma cluster where the tested Application Gateway is installed. +The test Job and the mock application deployment are in the `test` namespace. + +![Application Gateway tests architecture](assets/app-gateway-tests-architecture.svg) + +## Mock Application + +Mock application exposes the following APIs: +- API on port `8080` implementing various authentication methods and returning the `OAuth` and `CSRF` tokens +- API on port `8090` implementing the `mTLS` authentication and returning the `OAuth` tokens +- API on port `8091` implementing the `mTLS` authentication and using an expired server certificate + +### Certificates + +To test mTLS-related authentication methods, you need: +- Server certificate, key, and the CA certificate for the mock application +- Client certificate and key stored in a Secret accessed by Application Gateway + +All certificates are generated using the **generate-certs** target from the `Makefile`. +The target is executed before the tests are run, and it invokes [`generate-self-signed-certs.sh`](../scripts/generate-self-signed-certs.sh), which creates the CA root, server, and client certificates and keys. + +> **NOTE:** Since self-signed certificates are used, Application CRs have the **skipVerify: true** property set to `true` to force Application Gateway to skip certificate verification. + +### API Exposed on Port `8080` + +To get tokens for the `OAuth` and `CSRF` protected endpoints, we have the following API: +![8080 token API](assets/api-tokens.png) + +To test authentication methods, we have the following API: +![8080 authorisation methods API](assets/api-auth-methods.png) + +The credentials used for authentication, such as `user` and `password`, are [hardcoded](../tools/external-api-mock-app/config.go). + +### API Exposed on Port `8090` + +To get tokens for the `OAuth` protected endpoints, we have the following API: +![8090 token API](assets/api-tokens-mtls.png) + +To test authentication methods, we have the following API: +![8090 authorisation methods API](assets/api-auth-methods-mtls.png) + +The credentials used for authentication, such as `clientID`, are [hardcoded](../tools/external-api-mock-app/config.go). +The server key, server certificate, and the CA root certificate for port `8090` are defined in [this Secret](../resources/charts/gateway-test/charts/mock-app/templates/credentials/mtls-cert-secret.yml). + +> **NOTE:** Port `8090` must be excluded from redirection to Envoy, otherwise Application Gateway cannot pass the client certificate to the mock application. + +### API Exposed on Port `8091` + +This API is identical to the one exposed on port `8090`. +The HTTPS server on port `8091` uses an expired server certificate. +The server key, server certificate, and the CA root certificate for port `8091` are defined in [this Secret](../resources/charts/gateway-test/charts/mock-app/templates/credentials/expired-mtls-cert-secret.yaml). + +> **NOTE:** Port `8091` must be excluded from redirection to Envoy, otherwise Application Gateway cannot pass the client certificate to the mock application. + +## Building + +Pipelines build the mock application and the Gateway test using the **release** target from the `Makefile`. + +To build **and push** the Docker images of the tests and the mock application, run: + +``` sh +./scripts/local-build.sh {DOCKER_TAG} {DOCKER_PUSH_REPOSITORY} +``` + +This will build the following images: +- `{DOCKER_PUSH_REPOSITORY}/gateway-test:{DOCKER_TAG}` +- `{DOCKER_PUSH_REPOSITORY}/mock-app:{DOCKER_TAG}` + +## Running + +Tests can be run on any Kyma cluster with Application Gateway. + +Pipelines run the tests using the **test-gateway** target from the `Makefile`. + +### Deploy a Kyma Cluster Locally + +1. Provision a local Kubernetes cluster with k3d: + ```sh + kyma provision k3d + ``` + +2. Install the minimal set of components required to run Application Gateway for either Kyma OS or SKR: + +
+
+ + Kyma OS (standalone mode) + + + ```sh + kyma deploy --components-file ./resources/installation-config/mini-kyma-os.yaml + ``` + +
+
+ + SKR (Compass mode) + + + ```bash + kyma deploy --components-file ./resources/installation-config/mini-kyma-skr.yaml + ``` + +
+
+ + >**TIP:** Read more about Kyma installation in the [official Kyma documentation](https://kyma-project.io/#/02-get-started/01-quick-install). + +### Run the Tests + +``` sh +make -f Makefile.test-application-gateway test-gateway +``` + +By default, the tests clean up after themselves, removing all the previously created resources and the `test` namespace. + +> **CAUTION:** If the names of your existing resources are the same as the names used in the tests, running this command overrides or removes the existing resources. + +## Debugging + +### Running Locally + +> **CAUTION:** Because of the way it accesses the Application CRs, the test Job must run **on a cluster**. +> Application Gateway and the mock application can both be run locally. + +To run the mock application locally, follow these steps: + +1. Change all the **targetUrl** values in the [Application CRs](../resources/charts/gateway-test/charts/test/templates/applications/) to reflect the new application URL. For example, `http://localhost:8081/v1/api/unsecure/ok`. +2. Change all the **centralGatewayUrl** values to reflect the new Application Gateway URL. For example, `http://localhost:8080/positive-authorisation/unsecure-always-ok`. +3. Deploy all the resources in the cluster. + > **NOTE:** You can omit the test Job and the Central Gateway, but it's easier to just let them fail. +4. Build the mock application: + +
+
+ + Docker + + + ```shell + export DOCKER_TAG="local" + export DOCKER_PUSH_REPOSITORY="{DOCKER_USERNAME}" + make image-mock-app + ``` + +
+
+ + Local + + + Change the hardcoded application port in [`config.go`](../tools/external-api-mock-app/config.go), and run: + ```shell + go build ./tools/external-api-mock-app/ + ``` +
+
+5. Run the mock application: + +
+
+ + Docker + + + ```shell + docker run -p 8180:8080 -p 8190:8090 -v "$PWD/resources/charts/gateway-test/charts/test/certs/positive:/etc/secret-volume:ro" -v "$PWD/resources/charts/gateway-test/charts/test/certs/negative:/etc/expired-server-cert-volume:ro" "$DOCKER_PUSH_REPOSITORY/mock-app:$DOCKER_TAG" + ``` + +
+
+ + Local + + + ```shell + ./external-api-mock-app + ``` + > **CAUTION:** For the certificates to work, you must copy them from `./k8s/gateway-test/certs` to `/etc/secret-volume`. + +
+
+6. Run [Application Gateway](https://github.com/kyma-project/kyma/tree/main/components/central-application-gateway) with the `-kubeConfig {PATH_TO_YOUR_KUBECONFIG_FILE}` parameter. + +You can now send requests to Application Gateway, and debug its behavior locally. + +### Running Without Cleanup + +To run the tests without removing all the created resources afterwards, run them in the debugging mode. + +1. To start the tests in the debugging mode, run: + + ``` shell + make disable-sidecar-for-mtls-test test-gateway-debug + ``` + +2. Once you've finished debugging, run: + + ``` shell + make clean-gateway-test enable-sidecar-after-mtls-test + ``` + diff --git a/tests/docs/assets/api-auth-methods-mtls.png b/tests/docs/assets/api-auth-methods-mtls.png new file mode 100644 index 0000000000000000000000000000000000000000..743a1d4c28be5ead8722f50e0ced382777f8f39d GIT binary patch literal 24949 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

-~%E?Yl7Aj$b&2#0#3kr}hhtS~463yyjz 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 literal 0 HcmV?d00001 diff --git a/tests/docs/assets/api-auth-methods.png b/tests/docs/assets/api-auth-methods.png new file mode 100644 index 0000000000000000000000000000000000000000..29566ba072b0466be9502d6fd0323a319247d798 GIT binary patch literal 314875 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


5jifDKMXQ2eRL5S_fC^o|^wV#Pe_@ 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

2Oy3B1-#)ai3M}+z@$vLpn>MOQWXCkN6Dt`+I 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

q2`Soe^D+2vBz7dp0;%b5Lywl$3f0Y~KvB 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 PKBsk8_fEN*P5A!;%3#=+ literal 0 HcmV?d00001 diff --git a/tests/docs/assets/api-tokens.png b/tests/docs/assets/api-tokens.png new file mode 100644 index 0000000000000000000000000000000000000000..386096e8b2504373583c2907af6d5d7c1f76bdf6 GIT binary patch literal 89916 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 + + +
Kyma cluster
Kyma cluster
4
4
kyma-system Namespace
kyma-system Namespace
Application Gateway
Application Gateway
test Namespace
test Namespace
Application CR
- service 1 (test case 1)
- service 2 (test case 2)
- ...                                
Application CR...
mock application (remote endpoints)
mock application (remo...
App Gateway test Pod
App Gateway test...
1
1
2
2
Text is not SVG - cannot display
\ No newline at end of file diff --git a/tests/docs/assets/compass-runtime-agent-tests-architecture.svg b/tests/docs/assets/compass-runtime-agent-tests-architecture.svg new file mode 100644 index 00000000..dc02a5e5 --- /dev/null +++ b/tests/docs/assets/compass-runtime-agent-tests-architecture.svg @@ -0,0 +1,4 @@ + + + +
Kyma cluster
Kyma cluster
4
4
kyma-system Namespace
kyma-system Namespace
Compass Runtime Agent
Compass Runtime Agent
test Namespace
test Namespace
Application CR
- service 1 (test case 1)
- service 2 (test case 2)
- ...                                
Application CR...
Compass Runtime Agent test Pod
Compass Runtime Agent test Pod
CompassConnection CR
CompassConnection CR
Compass Runtime Agent certificates Secret
Compass Runtime Agent certi...
1
1
3
3
4
4
5
5
2
2
6
6
CA root certificate Secret
CA root certificate Secret
7
7
4
4
Compass test environment
Compass test environment
Connector
Connector
Director
Director
Text is not SVG - cannot display
\ No newline at end of file diff --git a/tests/docs/assets/connectivity-validator-tests-architecture.svg b/tests/docs/assets/connectivity-validator-tests-architecture.svg new file mode 100644 index 00000000..8b852313 --- /dev/null +++ b/tests/docs/assets/connectivity-validator-tests-architecture.svg @@ -0,0 +1,4 @@ + + + +
Kyma cluster
Kyma cluster
4
4
kyma-system Namespace
kyma-system Namespace
Connectivity Validator
Connectivity Validator
test Namespace
test Namespace
mock service (echoservice)
mock service (echose...
Connectivity Validator test Pod
Connectivity Validat...
1
1
2
2
Text is not SVG - cannot display
\ No newline at end of file diff --git a/tests/docs/assets/mock-app-mtls-spec.yaml b/tests/docs/assets/mock-app-mtls-spec.yaml new file mode 100644 index 00000000..2852c73d --- /dev/null +++ b/tests/docs/assets/mock-app-mtls-spec.yaml @@ -0,0 +1,51 @@ +openapi: 3.0.3 +info: + title: Mock Application for testing Application Gateway + description: |- + This is an API of Mock Application supporting Application Gateway Tests. + version: 1.0.11 +tags: + - name: OAuth tokens + description: Endpoints returning OAuth tokens + - name: CSRF + description: Endpoints protected by CSRF method + - name: No authentication + description: Endpoints not protected by any authentication method +paths: + /v1/api/mtls-oauth/token: + post: + tags: + - OAuth tokens + summary: Returns valid OAuth token + operationId: oauthToken + responses: + '200': + description: client_id and grant_type values correct + '401': + description: Bad client_id or grant_type value + + /v1/api/mtls/ok: + get: + tags: + - No authentication + summary: Returns status 200 OK if authorisation is successful + operationId: onBasicAuth + responses: + '200': + description: Authorisation successful + '401': + description: Client certificate is not valid + /v1/api/csrf-mtls/ok: + get: + tags: + - CSRF + summary: Returns status 200 OK if authorisation is successful + operationId: onCsrfOAuth + responses: + '200': + description: Authorisation successful + '401': + description: Client certificate is not valid + '403': + description: Username or password doesn't match or invalid CSRF token passed + diff --git a/tests/docs/assets/mock-app-spec.yaml b/tests/docs/assets/mock-app-spec.yaml new file mode 100644 index 00000000..53877cb2 --- /dev/null +++ b/tests/docs/assets/mock-app-spec.yaml @@ -0,0 +1,186 @@ +openapi: 3.0.3 +info: + title: Mock Application for testing Application Gateway + description: |- + This is an API of Mock Application supporting Application Gateway Tests. + version: 1.0.11 +tags: + - name: OAuth tokens + description: Endpoints returning OAuth tokens + - name: CSRF tokens + description: Endpoints returning CSRF tokens + - name: No authentication + description: Endpoints not protected by any authentication method + - name: Basic Authentication + description: Endpoints protected by Basic Authentication + - name: OAuth + description: Endpoints protected by OAuth method expecting valid OAuth token + - name: Basic Authentication and CSRF token + description: Endpoints protected by Basic Authentication and CSRF methods + - name: OAuth and CSRF + description: Endpoints protected by OAuth and CSRF methods + - name: Basic Authentication and request parameters + description: Endpoints protected by Basic Authentication and additional request parameters +paths: + /v1/api/oauth/token: + post: + tags: + - OAuth tokens + summary: Returns valid OAuth token + operationId: oauthToken + responses: + '200': + description: client_id, client_secret and grant_type values correct + '401': + description: Bad client_id, client_secret or grant_type value + /v1/api/oauth/bad-token: + post: + tags: + - OAuth tokens + summary: Returns invalid OAuth token + operationId: oauthBadToken + responses: + '200': + description: client_id, client_secret and grant_type values correct + '401': + description: Bad client_id, client_secret or grant_type value + /v1/api/csrf/token: + get: + tags: + - CSRF tokens + summary: Returns valid CSRF token + operationId: csrfToken + responses: + '200': + description: Token generated successfully + /v1/api/csrf/bad-token: + get: + tags: + - CSRF tokens + summary: Returns invalid CSRF token + responses: + '200': + description: Token generated successfully + + + /v1/api/unsecure/ok: + get: + tags: + - No authentication + summary: Returns status 200 OK + operationId: okNoAuth + responses: + '200': + description: Successful operation + /v1/api/unsecure/echo: + put: + tags: + - No authentication + summary: Responds with request body sent to the endpoint + operationId: echoNoAuthPut + responses: + '200': + description: Successful operation + post: + tags: + - No authentication + summary: Responds with request body sent to the endpoint + operationId: echoNoAuthPost + responses: + '200': + description: Successful operation + delete: + tags: + - No authentication + summary: Responds with request body sent to the endpoint + operationId: echoNoAuthDelete + responses: + '200': + description: Successful operation + /v1/api/unsecure/code/{code}: + get: + tags: + - No authentication + parameters: + - in: path + name: code + schema: + type: integer + required: true + summary: Responds with status code specified in the {code} parameter + operationId: codeNoAuth + responses: + '200': + description: Successful operation + /v1/api/unsecure/timeout: + get: + tags: + - No authentication + summary: Sleeps for 2 minutes before responding + operationId: timeoutNoAuth + responses: + '200': + description: Successful operation + /v1/api/basic/ok: + get: + tags: + - Basic Authentication + summary: Returns status 200 OK if authentication is successful + operationId: onBasicAuth + responses: + '200': + description: Authentication successful + '403': + description: Username or password doesn't match + /v1/api/oauth/ok: + get: + tags: + - OAuth + summary: Returns status 200 OK if authentication is successful + operationId: onOAuth + responses: + '200': + description: Authentication successful + '401': + description: Authorization header missing or contains invalid token + + /v1/api/csrf-basic/ok: + get: + tags: + - Basic Authentication and CSRF token + summary: Returns status 200 OK if authentication is successful + operationId: onCsrfBasic + responses: + '200': + description: Authentication successful + '403': + description: Username or password doesn't match or invalid CSRF token passed + + /v1/api/csrf-oauth/ok: + get: + tags: + - OAuth and CSRF + summary: Returns status 200 OK if authentication is successful + operationId: onCsrfOAuth + responses: + '200': + description: Authentication successful + '401': + description: Authorization header missing or contains invalid token + '403': + description: Username or password doesn't match or invalid CSRF token passed + + /v1/api/request-parameters-basic/ok: + get: + tags: + - Basic Authentication and request parameters + summary: Returns status 200 OK if authentication is successful + operationId: onRequestParamsBasic + responses: + '200': + description: Authentication successful + '400': + description: Expected headers and request params not passed + '403': + description: Username or password doesn't match + diff --git a/tests/docs/compass-runtime-agent-tests.md b/tests/docs/compass-runtime-agent-tests.md new file mode 100644 index 00000000..afc21a24 --- /dev/null +++ b/tests/docs/compass-runtime-agent-tests.md @@ -0,0 +1,138 @@ +# Compass Runtime Agent + +**Table of Contents** + +- [Compass Runtime Agent](#compass-runtime-agent) + - [Design and Architecture](#design-and-architecture) + - [Building](#building) + - [Running](#running) + - [Deploy a Kyma Cluster Locally](#deploy-a-kyma-cluster-locally) + - [Test Setup - Compass Runtime Agent Configuration](#test-setup---compass-runtime-agent-configuration) + - [Run the Tests](#run-the-tests) + - [Debugging](#debugging) + - [Running Without Cleanup](#running-without-cleanup) + - [Debugging in the IDE](#debugging-in-the-ide) + +## Design and Architecture + +The tests consist of: +- [Test resources](../resources/charts/compass-runtime-agent-test/) used to perform the test +- [Test runner](../test/application-connectivity-validator/) with all the test cases + +The tests are executed as a Kubernetes Job in a Kyma cluster where the tested Compass Runtime Agent is installed. The test Job is deployed in the `test` namespace. + +![Compass Runtime Agent tests architecture](assets/compass-runtime-agent-tests-architecture.svg) + +The interactions between components are the following: + +1. Compass Runtime Agent periodically fetches certificates from Compass Connector. +2. Compass Runtime Agent periodically fetches applications from Compass Director. +3. Compass Runtime Agent Test sends GraphQL mutations to Compass Director to create, modify, or delete Applications. +4. Compass Runtime Agent Test verifies whether corresponding Application CRs were created, modified, or deleted. +5. Compass Runtime Agent Test verifies whether the Secret with certificates used for communication with Director was created. +6. Compass Runtime Agent Test verifies whether the Secret with the CA root certificate used by Istio Gateway was created. +7. Compass Runtime Agent Test verifies the content of the CompassConnection CR. + +## Building + +Pipelines build the Compass Runtime Agent test using the **release** target from the `Makefile`. + +To build **and push** the Docker images of the tests, run: + +```bash +./scripts/local-build.sh {DOCKER_TAG} {DOCKER_PUSH_REPOSITORY} +``` + +This builds the following images: +- `{DOCKER_PUSH_REPOSITORY}/compass-runtime-agent-test:{DOCKER_TAG}` + +## Running + +Tests can be run on any Kyma cluster with Compass Runtime Agent. + +Pipelines run the tests using the **test-compass-runtime-agent** target from the `Makefile`. + +### Deploy a Kyma Cluster Locally + +1. Provision a local Kubernetes cluster with k3d: + ```bash + kyma provision k3d + ``` + +2. Install the minimal set of components required to run Compass Runtime Agent **for Kyma SKR (Compass mode)**: + + ```bash + kyma deploy --components-file ./resources/installation-config/mini-kyma-skr.yaml --value global.disableLegacyConnectivity=true --value compassRuntimeAgent.director.proxy.insecureSkipVerify=true + ``` + + >**TIP:** Read more about [Kyma installation](https://kyma-project.io/#/02-get-started/01-quick-install). + +### Test Setup - Compass Runtime Agent Configuration + +The [`values.yaml`](../resources/charts/compass-runtime-agent-test/values.yaml) file contains environment variables that are used in the Compass Runtime Agent tests. These values can be modified as needed. + +- **APP_DIRECTOR_URL** - Compass Director URL +- **APP_TESTING_TENANT** - Tenant used in GraphQL calls +- **APP_SKIP_DIRECTOR_CERT_VERIFICATION** - Skip certificate verification on the Director side +- **APP_OAUTH_CREDENTIALS_SECRET_NAME** - Secret name for Compass OAuth credentials +- **APP_OAUTH_CREDENTIALS_NAMESPACE** - Namespace for Compass OAuth credentials + +### Run the Tests + +1. Before running the test export the following environment variables + - **COMPASS_HOST** - host running Compass + - **COMPASS_CLIENT_ID** - client ID used for fetching authorization tokens + - **COMPASS_CLIENT_SECRET** - client Secret used for fetching authorization tokens + +2. To start the tests, run: + + ```bash + make test-compass-runtime-agent + ``` + +By default, the tests clean up after themselves, removing all the previously created resources and the `test` namespace. + +> **CAUTION:** If the names of your existing resources are the same as the names used in the tests, running this command overrides or removes the existing resources. + +## Debugging + +### Running Without Cleanup + +To run the tests without removing all the created resources afterwards, run them in the debugging mode. + +1. To start the tests in the debugging mode, run: + + ```bash + make test-compass-runtime-agent-debug + ``` + +2. Once you've finished debugging, run: + + ```bash + make clean-test-compass-runtime-agent-test + ``` + +### Debugging in the IDE + +To run the test in your IDE, perform the following steps. + +1. To prepare the cluster for debugging, run the test without cleanup: + + ```bash + make test-compass-runtime-agent-debug + ``` + +2. Before starting debugger in your IDE export the following environment variables: + - `KUBECONFIG={Your cluster kubeconfig}` + - `APP_DIRECTOR_URL=https://compass-gateway-auth-oauth.{COMPASS_HOST}/director/graphql` + - `APP_TESTING_TENANT=3e64ebae-38b5-46a0-b1ed-9ccee153a0ae` + - `APP_OAUTH_CREDENTIALS_SECRET_NAME=oauth-compass-credentials` + - `APP_OAUTH_CREDENTIALS_NAMESPACE=test` + +3. Start the debugging session. + +4. Once you've finished debugging, run: + + ```bash + make clean-test-compass-runtime-agent-test + ``` \ No newline at end of file diff --git a/tests/go.mod b/tests/go.mod new file mode 100644 index 00000000..e69d3ee7 --- /dev/null +++ b/tests/go.mod @@ -0,0 +1,89 @@ +module github.com/kyma-project/kyma/tests/components/application-connector + +go 1.18 + +require ( + github.com/avast/retry-go v3.0.0+incompatible + github.com/go-http-utils/logger v0.0.0-20161128092850-f3a42dcdeae6 + github.com/google/uuid v1.3.0 + github.com/gorilla/mux v1.8.0 + github.com/hashicorp/go-multierror v1.1.1 + github.com/kyma-incubator/compass/components/director v0.0.0-20220126084901-92232f5eced0 + github.com/kyma-project/kyma/components/central-application-gateway v0.0.0-20230130154909-4c81ab2cee61 + github.com/kyma-project/kyma/components/compass-runtime-agent v0.0.0-20220927112044-a548531152a1 + github.com/matryer/is v1.4.0 + github.com/pkg/errors v0.9.1 + github.com/sirupsen/logrus v1.9.0 + github.com/stretchr/testify v1.8.1 + github.com/vrischmann/envconfig v1.3.0 + k8s.io/api v0.26.0 + k8s.io/apimachinery v0.26.0 + k8s.io/client-go v0.26.0 +) + +require ( + github.com/99designs/gqlgen v0.11.3 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.1.1 // indirect + github.com/Masterminds/sprig/v3 v3.2.2 // indirect + github.com/agnivade/levenshtein v1.1.0 // indirect + github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/emicklei/go-restful/v3 v3.9.0 // indirect + github.com/evanphx/json-patch v4.12.0+incompatible // indirect + github.com/go-logr/logr v1.2.3 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.20.0 // indirect + github.com/go-openapi/swag v0.19.15 // indirect + github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/google/gnostic v0.5.7-v3refs // indirect + github.com/google/go-cmp v0.5.9 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/huandu/xstrings v1.3.2 // indirect + github.com/imdario/mergo v0.3.12 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mailru/easyjson v0.7.6 // indirect + github.com/mitchellh/copystructure v1.1.2 // indirect + github.com/mitchellh/reflectwalk v1.0.1 // 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/onrik/logrus v0.9.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/shopspring/decimal v1.2.0 // indirect + github.com/spf13/cast v1.4.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/objx v0.5.0 // indirect + github.com/vektah/gqlparser/v2 v2.1.0 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect + golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd // indirect + golang.org/x/net v0.3.1-0.20221206200815-1e63c2f08a10 // indirect + golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect + golang.org/x/sys v0.4.0 // indirect + golang.org/x/term v0.4.0 // indirect + golang.org/x/text v0.6.0 // indirect + golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.28.1 // 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/klog/v2 v2.80.1 // indirect + k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 // indirect + k8s.io/utils v0.0.0-20221107191617-1a15be271d1d // indirect + sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect + sigs.k8s.io/yaml v1.3.0 // indirect +) + +replace ( + golang.org/x/crypto => golang.org/x/crypto v0.0.0-20221012134737-56aed061732a + golang.org/x/net => golang.org/x/net v0.0.0-20221014081412-f15817d10f9b + golang.org/x/text => golang.org/x/text v0.3.8 +) diff --git a/tests/go.sum b/tests/go.sum new file mode 100644 index 00000000..d7602a66 --- /dev/null +++ b/tests/go.sum @@ -0,0 +1,574 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/99designs/gqlgen v0.11.3 h1:oFSxl1DFS9X///uHV3y6CEfpcXWrDUxVblR4Xib2bs4= +github.com/99designs/gqlgen v0.11.3/go.mod h1:RgX5GRRdDWNkh4pBrdzNpNPFVsdoUFY2+adM6nb1N+4= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmyxvxX8= +github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= +github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= +github.com/agnivade/levenshtein v1.0.3/go.mod h1:4SFRZbbXWLF4MU1T9Qg0pGgH3Pjs+t6ie5efyrwRJXs= +github.com/agnivade/levenshtein v1.1.0 h1:n6qGwyHG61v3ABce1rPVZklEYRT8NFpCMrpZdBUbYGM= +github.com/agnivade/levenshtein v1.1.0/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= +github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= +github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ= +github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0= +github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +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/dgryski/trifles v0.0.0-20190318185328-a8d75aae118c/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= +github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g= +github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE= +github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= +github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/go-chi/chi v3.3.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-http-utils/logger v0.0.0-20161128092850-f3a42dcdeae6 h1:R/ypabUA7vskKTRSlgP6rMUHTU6PBRgIcHVSU9qQ6qM= +github.com/go-http-utils/logger v0.0.0-20161128092850-f3a42dcdeae6/go.mod h1:CpBLxS3WrxouNECP/Y1A3i6qDnUYs8BvcXjgOW4Vqcw= +github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= +github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es= +github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew= +github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54= +github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/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.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= +github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/jmoiron/sqlx v1.3.4 h1:wv+0IJZfL5z0uZoUjlpKgHkgaFSYD+r9CfrXjEXsO7w= +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/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +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.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +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/kyma-incubator/compass/components/director v0.0.0-20220126084901-92232f5eced0 h1:5pDxnqY8TK59Zp+9PqsC8uEG6ZeeURJKHxUo19ez+jY= +github.com/kyma-incubator/compass/components/director v0.0.0-20220126084901-92232f5eced0/go.mod h1:62mrVWDkGdPxAqj+X97FKiA/e8jYSZ/MARgzoThk9AU= +github.com/kyma-project/kyma/components/central-application-gateway v0.0.0-20230130154909-4c81ab2cee61 h1:iviPUIyUTMKA322amhFURlXbIbj9NrojpvJFDI+DtnQ= +github.com/kyma-project/kyma/components/central-application-gateway v0.0.0-20230130154909-4c81ab2cee61/go.mod h1:NL5E+cv7oyD8xJtDywLrHnkublvqifMBt5HFdw94adc= +github.com/kyma-project/kyma/components/compass-runtime-agent v0.0.0-20220927112044-a548531152a1 h1:zhIQX99vZIS5nlWIQZE6nIVB3w7W+vSgE5r9+VxLZGE= +github.com/kyma-project/kyma/components/compass-runtime-agent v0.0.0-20220927112044-a548531152a1/go.mod h1:D80/HUyVanrVfAcUOt8xRWp5oZwd1IK4SAg0A9Hlj+8= +github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk= +github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/copystructure v1.1.2 h1:Th2TIvG1+6ma3e/0/bopBKohOTY7s4dA8V2q4EUcBJ0= +github.com/mitchellh/copystructure v1.1.2/go.mod h1:EBArHfARyrSWO/+Wyr9zwEkc6XMFB9XyNgFNmRkZZU4= +github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mitchellh/reflectwalk v1.0.1 h1:FVzMWA5RllMAKIdUSC8mdWo3XtwoecrH79BY70sEEpE= +github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +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/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/onrik/logrus v0.9.0 h1:oT7VstCUxWBoX7fswYK61fi9bzRBSpROq5CR2b7wxQo= +github.com/onrik/logrus v0.9.0/go.mod h1:qfe9NeZVAJfIxviw3cYkZo3kvBtLoPRJriAO8zl7qTk= +github.com/onsi/ginkgo/v2 v2.4.0 h1:+Ig9nvqgS5OBSACXNk15PLdp0U9XPYROt9CFzVdFGIs= +github.com/onsi/gomega v1.23.0 h1:/oxKu9c2HVap+F3PfKort2Hw5DEU+HGlW8n+tguWsys= +github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= +github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= +github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +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/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +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 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.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 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/tidwall/gjson v1.12.1 h1:ikuZsLdhr8Ws0IdROXUS1Gi4v9Z4pGqpX/CvJkxvfpo= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/sjson v1.2.4 h1:cuiLzLnaMeBhRmEv00Lpk3tkYrcxpmbU81tAY4Dw0tc= +github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= +github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U= +github.com/vektah/gqlparser/v2 v2.0.1/go.mod h1:SyUiHgLATUR8BiYURfTirrTcGpcE+4XkV2se04Px1Ms= +github.com/vektah/gqlparser/v2 v2.1.0 h1:uiKJ+T5HMGGQM2kRKQ8Pxw8+Zq9qhhZhz/lieYvCMns= +github.com/vektah/gqlparser/v2 v2.1.0/go.mod h1:SyUiHgLATUR8BiYURfTirrTcGpcE+4XkV2se04Px1Ms= +github.com/vrischmann/envconfig v1.3.0 h1:4XIvQTXznxmWMnjouj0ST5lFo/WAYf5Exgl3x82crEk= +github.com/vrischmann/envconfig v1.3.0/go.mod h1:bbvxFYJdRSpXrhS63mBFtKJzkDiNkyArOLXtY6q0kuI= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/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= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +golang.org/x/crypto v0.0.0-20221012134737-56aed061732a h1:NmSIgad6KjE6VvHciPZuNRTKxGhlPfD6OA87W/PLkqg= +golang.org/x/crypto v0.0.0-20221012134737-56aed061732a/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +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/net v0.0.0-20221014081412-f15817d10f9b h1:tvrvnPFcdzp294diPnrdZZZ8XUt2Tyj7svb7X52iDuU= +golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b h1:clP8eMhB30EHdc0bd2Twtq6kgU7yl5ub2cQLSdrv1Dg= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +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-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/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/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/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-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= +golang.org/x/sys v0.4.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.4.0 h1:O7UWfv5+A2qiuulQk30kVinPoMtoIPeVaKLEgLpVkvg= +golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= +golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200114235610-7ae403b6b589/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/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-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +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/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/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +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-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +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.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/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-20180628173108-788fd7840127/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-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +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.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/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.0-20200615113413-eeeca48fe776/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= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +k8s.io/api v0.26.0 h1:IpPlZnxBpV1xl7TGk/X6lFtpgjgntCg8PJ+qrPHAC7I= +k8s.io/api v0.26.0/go.mod h1:k6HDTaIFC8yn1i6pSClSqIwLABIcLV9l5Q4EcngKnQg= +k8s.io/apimachinery v0.26.0 h1:1feANjElT7MvPqp0JT6F3Ss6TWDwmcjLypwoPpEf7zg= +k8s.io/apimachinery v0.26.0/go.mod h1:tnPmbONNJ7ByJNz9+n9kMjNP8ON+1qoAIIC70lztu74= +k8s.io/client-go v0.26.0 h1:lT1D3OfO+wIi9UFolCrifbjUUgu7CpLca0AD8ghRLI8= +k8s.io/client-go v0.26.0/go.mod h1:I2Sh57A79EQsDmn7F7ASpmru1cceh3ocVT9KlX2jEZg= +k8s.io/klog/v2 v2.80.1 h1:atnLQ121W371wYYFawwYx1aEY2eUfs4l3J72wtgAwV4= +k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 h1:+70TFaan3hfJzs+7VK2o+OGxg8HsuBr/5f6tVAjDu6E= +k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280/go.mod h1:+Axhij7bCpeqhklhUTe3xmOn6bWxolyZEeyaFpjGtl4= +k8s.io/utils v0.0.0-20221107191617-1a15be271d1d h1:0Smp/HP1OH4Rvhe+4B8nWGERtlqAGSftbSbbmm45oFs= +k8s.io/utils v0.0.0-20221107191617-1a15be271d1d/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 h1:iXTIw73aPyC+oRdyqqvVJuloN1p0AC/kzH07hu3NE+k= +sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= +sourcegraph.com/sourcegraph/appdash v0.0.0-20180110180208-2cc67fd64755/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= +sourcegraph.com/sourcegraph/appdash-data v0.0.0-20151005221446-73f23eafcf67/go.mod h1:L5q+DGLGOQFpo1snNEkLOJT2d1YTW66rWNzatr3He1k= diff --git a/tests/hack/ci/.srl b/tests/hack/ci/.srl new file mode 100644 index 00000000..0ef5e90b --- /dev/null +++ b/tests/hack/ci/.srl @@ -0,0 +1 @@ +8FC09AB8ECD2BADC diff --git a/tests/hack/ci/Makefile b/tests/hack/ci/Makefile new file mode 100644 index 00000000..c4003146 --- /dev/null +++ b/tests/hack/ci/Makefile @@ -0,0 +1,109 @@ +K3D_URL=https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh + +PROJECT_ROOT ?= ../../.. +CLUSTER_NAME ?= kyma +REGISTRY_PORT ?= 5001 +REGISTRY_NAME ?= ${CLUSTER_NAME}-registry + +# Operating system architecture +OS_ARCH ?= $(shell uname -m) + +# Operating system type +OS_TYPE ?= $(shell uname) + +.PHONY: setup-environment run-gateway-tests run-validator-tests run-agent-test + +.PHONY: application-connector-module-image +application-connector-module-image: + make -C ${PROJECT_ROOT}/hack/common module-image + +.PHONY: application-connector-deploy +application-connector-deploy: + @make -C ${PROJECT_ROOT}/hack/common deploy \ + IMG=k3d-${REGISTRY_NAME}:${REGISTRY_PORT}/${MANAGER_IMAGE_NAME}:${MANAGER_IMAGE_TAG} + +.PHONY: install-application-connector +install-application-connector: application-connector-module-image application-connector-deploy apply-appcon + @echo "::group::install-application-connector" + kubectl wait -n kyma-system \ + applicationconnectors/applicationconnector-sample \ + --for=jsonpath='{.status.state}'=Ready \ + --timeout=300s + @echo "::endgroup::" + +.PHONY: install-istio +install-istio: create-kyma-system-ns + @echo "::group::install-istio" + kubectl apply -f https://github.com/kyma-project/istio/releases/latest/download/istio-manager.yaml + kubectl apply -f https://github.com/kyma-project/istio/releases/latest/download/istio-default-cr.yaml + kubectl wait -n kyma-system istios/default --for=jsonpath='{.status.state}'=Ready --timeout=300s + @echo "::endgroup::" + +.PHONY: create-kyma-system-ns +create-kyma-system-ns: ## Create kyma-system namespace. + @echo "::group::create-kyma-system-ns" + kubectl create ns kyma-system + kubectl label namespaces kyma-system istio-injection=enabled --overwrite=true + @echo "::endgroup::" + +.PHONY: create-k3d +create-k3d: ## Create k3d with kyma CRDs. + @echo "::group::create-k3d" + k3d cluster create ${CLUSTER_NAME} \ + --api-port 6550 \ + -p 8080:80@loadbalancer \ + -p 8443:443@loadbalancer \ + -p 9090-9099:9090-9099@loadbalancer \ + --agents 2 \ + --registry-create k3d-${REGISTRY_NAME}:${REGISTRY_PORT} + @echo "::endgroup::" + +.PHONY: apply-appcon +apply-appcon: ## Apply the k3d application-connector CR + make -C ${PROJECT_ROOT}/hack/common apply-appcon + +.PHONY: gateway-tests +gateway-tests: + kubectl apply -f ${PROJECT_ROOT}/tests/hack/ci/deps/applications.applicationconnector.crd.yaml + make -f ${PROJECT_ROOT}/tests/Makefile.test-application-gateway test + +.PHONY: k3d-gateway-tests +k3d-gateway-tests: create-k3d install-istio install-application-connector gateway-tests + +k3d-validator-tests: setup-environment + ${KYMA} deploy --ci --components-file ${PROJECT_ROOT}/resources/installation-config/mini-kyma-skr.yaml --value global.disableLegacyConnectivity=true --source=local --workspace ${KYMA_ROOT_CI} + cd ${PROJECT_ROOT} + make -f Makefile.test-application-conn-validator test + k3d cluster delete + +k3d-agent-tests: setup-environment + ${KYMA} deploy --ci --components-file ${PROJECT_ROOT}/resources/installation-config/mini-kyma-skr.yaml --value global.disableLegacyConnectivity=true --value compassRuntimeAgent.director.proxy.insecureSkipVerify=true --source=local --workspace ${KYMA_ROOT_CI} + kubectl apply -f ${PROJECT_ROOT}/resources/patches/coredns.yaml + kubectl -n kube-system delete pods -l k8s-app=kube-dns + cd ${PROJECT_ROOT} + make -f Makefile.test-compass-runtime-agent test + k3d cluster delete + +##@ Tools + +## Location to install dependencies to +LOCALBIN ?= $(shell pwd)/bin +$(LOCALBIN): + mkdir -p $(LOCALBIN) + +########## Kyma CLI ########### +KYMA_STABILITY ?= unstable + +define os_error +$(error Error: unsuported platform OS_TYPE:$1, OS_ARCH:$2; to mitigate this problem set variable KYMA with absolute path to kyma-cli binary compatible with your operating system and architecture) +endef + +KYMA_FILE_NAME ?= kyma-linux + +KYMA ?= $(LOCALBIN)/kyma-$(KYMA_STABILITY) +kyma: $(LOCALBIN) $(KYMA) ## Download kyma locally if necessary. +$(KYMA): + $(if $(KYMA_FILE_NAME),,$(call os_error, ${OS_TYPE}, ${OS_ARCH})) + test -f $@ || curl -s -Lo $(KYMA) https://storage.googleapis.com/kyma-cli-$(KYMA_STABILITY)/$(KYMA_FILE_NAME) + chmod 0100 $(KYMA) + diff --git a/tests/hack/ci/deps/application-connector-cr.yaml b/tests/hack/ci/deps/application-connector-cr.yaml new file mode 100644 index 00000000..494d8f6b --- /dev/null +++ b/tests/hack/ci/deps/application-connector-cr.yaml @@ -0,0 +1,13 @@ +apiVersion: operator.kyma-project.io/v1alpha1 +kind: ApplicationConnector +metadata: + namespace: kyma-system + labels: + app.kubernetes.io/name: applicationconnector + app.kubernetes.io/instance: applicationconnector-sample + app.kubernetes.io/part-of: application-connector-manager + app.kuberentes.io/managed-by: kustomize + app.kubernetes.io/created-by: application-connector-manager + name: applicationconnector-sample +spec: + domainName: kyma.test.cluster.com diff --git a/tests/hack/ci/deps/application-connector-manager.yaml b/tests/hack/ci/deps/application-connector-manager.yaml new file mode 100644 index 00000000..65a7d727 --- /dev/null +++ b/tests/hack/ci/deps/application-connector-manager.yaml @@ -0,0 +1,665 @@ +apiVersion: v1 +kind: Namespace +metadata: + labels: + app.kubernetes.io/component: manager + app.kubernetes.io/created-by: application-connector-manager + app.kubernetes.io/instance: system + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: namespace + app.kubernetes.io/part-of: application-connector-manager + control-plane: controller-manager + name: kyma-system +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.10.0 + creationTimestamp: null + name: applicationconnectors.operator.kyma-project.io +spec: + group: operator.kyma-project.io + names: + kind: ApplicationConnector + listKind: ApplicationConnectorList + plural: applicationconnectors + singular: applicationconnector + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: ApplicationConnector is the Schema for the applicationconnectors + API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + properties: + appConnValidator: + default: + logFormat: json + logLevel: info + properties: + logFormat: + enum: + - json + - text + type: string + logLevel: + enum: + - debug + - panic + - fatal + - error + - warn + - info + - debug + type: string + required: + - logFormat + - logLevel + type: object + appGateway: + default: + logLevel: info + proxyTimeout: 10s + requestTimeout: 10s + properties: + logLevel: + enum: + - debug + - panic + - fatal + - error + - warn + - info + - debug + type: string + proxyTimeout: + type: string + requestTimeout: + type: string + required: + - logLevel + - proxyTimeout + - requestTimeout + type: object + domainName: + type: string + type: object + status: + properties: + conditions: + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + served: + type: string + state: + type: string + required: + - served + - state + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: application-connector-manager + app.kubernetes.io/instance: controller-manager-sa + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: serviceaccount + app.kubernetes.io/part-of: application-connector-manager + name: application-connector-controller-manager + namespace: kyma-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: application-connector-manager + app.kubernetes.io/instance: leader-election-role + app.kubernetes.io/name: role + app.kubernetes.io/part-of: application-connector-manager + app.kubernets.io/managed-by: kustomize + name: application-connector-leader-election-role + namespace: kyma-system +rules: + - apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + name: application-connector-manager-role +rules: + - apiGroups: + - "" + resources: + - configmaps + verbs: + - create + - delete + - get + - list + - update + - watch + - apiGroups: + - "" + resources: + - limitranges + verbs: + - create + - delete + - get + - list + - update + - apiGroups: + - "" + resources: + - namespaces + verbs: + - create + - delete + - apiGroups: + - "" + resources: + - nodes + verbs: + - get + - list + - apiGroups: + - "" + resources: + - persistentvolumes + verbs: + - get + - list + - apiGroups: + - "" + resources: + - pods + verbs: + - get + - list + - patch + - apiGroups: + - "" + resourceNames: + - cluster-client-certificates + - compass-agent-configuration + resources: + - secrets + verbs: + - delete + - get + - apiGroups: + - "" + resources: + - serviceaccounts + verbs: + - '*' + - apiGroups: + - "" + resources: + - services + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - '*' + resources: + - secrets + verbs: + - create + - delete + - get + - list + - update + - watch + - apiGroups: + - apiextensions.k8s.io + resources: + - customresourcedefinitions + verbs: + - create + - delete + - get + - list + - patch + - watch + - apiGroups: + - applicationconnector.kyma-project.io + resources: + - applications + verbs: + - create + - delete + - get + - list + - update + - watch + - apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - apps + resources: + - replicasets + verbs: + - delete + - list + - watch + - apiGroups: + - autoscaling + resources: + - horizontalpodautoscalers + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - compass.kyma-project.io + resources: + - compassconnections + verbs: + - create + - delete + - get + - list + - update + - watch + - apiGroups: + - metrics.k8s.io + resources: + - nodes + verbs: + - get + - list + - apiGroups: + - networking.istio.io + resources: + - gateways + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - networking.istio.io + resources: + - virtualservices + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - operator.kyma-project.io + resources: + - applicationconnectors + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - operator.kyma-project.io + resources: + - applicationconnectors/finalizers + verbs: + - update + - apiGroups: + - operator.kyma-project.io + resources: + - applicationconnectors/status + verbs: + - get + - patch + - update + - apiGroups: + - rbac.authorization.k8s.io + resources: + - clusterrolebindings + - clusterroles + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - rbac.authorization.k8s.io + resources: + - rolebindings + - roles + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - scheduling.k8s.io + resources: + - priorityclasses + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: application-connector-manager + app.kubernetes.io/instance: leader-election-rolebinding + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: rolebinding + app.kubernetes.io/part-of: application-connector-manager + name: application-connector-leader-election-rolebinding + namespace: kyma-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: application-connector-leader-election-role +subjects: + - kind: ServiceAccount + name: application-connector-controller-manager + namespace: kyma-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: application-connector-manager + app.kubernetes.io/instance: manager-rolebinding + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: clusterrolebinding + app.kubernetes.io/part-of: application-connector-manager + name: application-connector-manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: application-connector-manager-role +subjects: + - kind: ServiceAccount + name: application-connector-controller-manager + namespace: kyma-system +--- +apiVersion: v1 +data: + details: "header:\n - name: Ready \n source: status.state\n widget: Badge\n + \ highlights:\n positive:\n - 'Ready'\nbody:\n - name: Configuration\n + \ widget: Panel\n children:\n - name: Domain name\n source: spec.domainName\n + \ placeholder: Detected automatically\n - widget: Columns\n children:\n + \ - name: Application Connector Validator\n widget: Panel\n children: + \n - source: spec.appConnValidator.logLevel\n name: Validator + log level\n - source: spec.appConnValidator.logFormat\n name: + Validator log format\n - name: Application Connector Gateway\n widget: + Panel\n children:\n - source: spec.appGateway.proxyTimeout\n name: + Proxy timeout duration\n - source: spec.appGateway.requestTimeout\n name: + Request timeout duration\n - source: spec.appGateway.logLevel\n name: + Gateway log level\n\n - source: status.conditions\n widget: Table\n name: + Reconciliation Conditions\n children:\n - source: type\n name: + Type\n - source: status\n name: Status\n widget: Badge\n highlights:\n + \ positive:\n - 'True'\n negative:\n - + 'False'\n - source: reason\n name: Reason\n - source: message\n + \ name: Message\n - source: '$readableTimestamp(lastTransitionTime)'\n + \ name: Last transition\n sort: true\n\n - widget: EventList\n filter: + '$matchEvents($$, $root.kind, $root.metadata.name)'\n name: events\n defaultType: + information\n" + form: "- path: spec.domainName\n name: Domain name\n\n- path: spec.appConnValidator\n + \ widget: FormGroup\n name: Application Connector Validator Configuration\n children:\n + \ - widget: KeyValuePair\n path: \n keyEnum: ['logLevel', 'logFormat']\n\n- + path: spec.appGateway\n widget: FormGroup\n name: Application Connector Gateway + Configuration\n children:\n - widget: KeyValuePair\n path: requests\n keyEnum: + ['proxyTimeout', 'requestTimeout','logLevel']\n\n" + general: | + resource: + kind: ApplicationConnector + group: operator.kyma-project.io + version: v1alpha1 + urlPath: applicationconnectors + category: Kyma + name: ApplicationConnector + scope: namespace + features: + actions: + disableCreate: true + disableDelete: true + description: >- + {{[ApplicationConnector CR](https://github.com/kyma-project/application-connector-manager/blob/main/config/samples/operator_v1alpha1_applicationconnector.yaml)}} + configures application connector installation. + list: "- name: Ready \n source: status.state\n widget: Badge\n highlights:\n + \ positive:\n - 'Ready'\n" +kind: ConfigMap +metadata: + labels: + app.kubernetes.io/name: applicationconnectors.operator.kyma-project.io + busola.io/extension: resource + busola.io/extension-version: "0.5" + name: application-connector-applicationconnectors.operator.kyma-project.io + namespace: kyma-system +--- +apiVersion: scheduling.k8s.io/v1 +description: Scheduling priority of application-connector-manager component. Must + not be blocked by unschedulable user workloads. +globalDefault: false +kind: PriorityClass +metadata: + name: application-connector-priority-class +value: 2100000 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app.kubernetes.io/component: manager + app.kubernetes.io/created-by: application-connector-manager + app.kubernetes.io/instance: controller-manager + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: deployment + app.kubernetes.io/part-of: application-connector-manager + control-plane: controller-manager + name: application-connector-controller-manager + namespace: kyma-system +spec: + replicas: 1 + selector: + matchLabels: + control-plane: controller-manager + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + labels: + control-plane: controller-manager + spec: + containers: + - args: + - --leader-elect + command: + - /manager + image: europe-docker.pkg.dev/kyma-project/prod/application-connector-manager:v20240212-61947b88 + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + name: manager + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 10m + memory: 128Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + priorityClassName: application-connector-priority-class + securityContext: + runAsNonRoot: true + serviceAccountName: application-connector-controller-manager + terminationGracePeriodSeconds: 10 diff --git a/tests/hack/ci/deps/applications.applicationconnector.crd.yaml b/tests/hack/ci/deps/applications.applicationconnector.crd.yaml new file mode 100644 index 00000000..1a7b6518 --- /dev/null +++ b/tests/hack/ci/deps/applications.applicationconnector.crd.yaml @@ -0,0 +1,183 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + "helm.sh/resource-policy": keep + name: applications.applicationconnector.kyma-project.io +spec: + group: applicationconnector.kyma-project.io + preserveUnknownFields: false + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + properties: + compassMetadata: + type: object + required: + - "authentication" + properties: + applicationId: + type: string + authentication: + type: object + required: + - "clientIds" + properties: + clientIds: + type: array + items: + type: string + accessLabel: + type: string + maxLength: 63 + pattern: '^([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]$' + description: + type: string + skipInstallation: + type: boolean + skipVerify: + type: boolean + encodeUrl: + type: boolean + default: true + labels: + nullable: true + additionalProperties: + type: string + type: object + tenant: + type: string + group: + type: string + tags: + nullable: true + description: New fields used by V2 version + items: + type: string + type: array + displayName: + type: string + providerDisplayName: + type: string + longDescription: + type: string + services: + type: array + items: + type: object + required: + - "id" + - "name" + - "displayName" + - "providerDisplayName" + - "description" + - "entries" + properties: + id: + type: string + name: + type: string + identifier: + type: string + labels: + nullable: true + additionalProperties: + type: string + description: Deprecated + type: object + displayName: + type: string + description: + type: string + longDescription: + type: string + providerDisplayName: + type: string + authCreateParameterSchema: + description: New fields used by V2 version + type: string + entries: + type: array + items: + type: object + required: + - "type" + properties: + apiType: + type: string + type: + type: string + enum: + - "API" + - "Events" + gatewayUrl: + type: string + centralGatewayUrl: + type: string + accessLabel: + type: string + maxLength: 63 + pattern: '^([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]$' + targetUrl: + type: string + id: + type: string + name: + description: New fields used by V2 version + type: string + requestParametersSecretName: + type: string + specificationUrl: + type: string + credentials: + type: object + required: + - "type" + - "secretName" + properties: + type: + type: string + secretName: + type: string + authenticationUrl: + type: string + csrfInfo: + type: object + required: + - "tokenEndpointURL" + properties: + tokenEndpointURL: + type: string + tags: + type: array + items: + type: string + type: object + status: + properties: + installationStatus: + description: Represents the status of Application release installation + properties: + description: + type: string + status: + type: string + required: + - status + type: object + required: + - installationStatus + type: object + scope: Cluster + names: + plural: applications + singular: application + kind: Application + shortNames: + - app diff --git a/tests/hack/ci/deps/istio-default-cr.yaml b/tests/hack/ci/deps/istio-default-cr.yaml new file mode 100644 index 00000000..9e3607aa --- /dev/null +++ b/tests/hack/ci/deps/istio-default-cr.yaml @@ -0,0 +1,7 @@ +apiVersion: operator.kyma-project.io/v1alpha2 +kind: Istio +metadata: + name: default + namespace: kyma-system + labels: + app.kubernetes.io/name: default diff --git a/tests/hack/ci/deps/istio-manager.yaml b/tests/hack/ci/deps/istio-manager.yaml new file mode 100644 index 00000000..f9e2951a --- /dev/null +++ b/tests/hack/ci/deps/istio-manager.yaml @@ -0,0 +1,7401 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.10.0 + creationTimestamp: null + labels: + app.kubernetes.io/component: istio-operator.kyma-project.io + name: istios.operator.kyma-project.io +spec: + group: operator.kyma-project.io + names: + kind: Istio + listKind: IstioList + plural: istios + singular: istio + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.state + name: State + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: Contains Istio CR specification and current status. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: Defines the desired specification for installing or updating + Istio. + properties: + components: + properties: + cni: + description: Cni defines component configuration for Istio CNI + DaemonSet + properties: + k8s: + description: CniK8sConfig is a subset of https://istio.io/latest/docs/reference/config/istio.operator.v1alpha1/#KubernetesResourcesSpec + properties: + affinity: + description: Affinity is a group of affinity scheduling + rules. + properties: + nodeAffinity: + description: Describes node affinity scheduling rules + for the pod. + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: The scheduler will prefer to schedule + pods to nodes that satisfy the affinity expressions + specified by this field, but it may choose a + node that violates one or more of the expressions. + The node that is most preferred is the one with + the greatest sum of weights, i.e. for each node + that meets all of the scheduling requirements + (resource request, requiredDuringScheduling + affinity expressions, etc.), compute a sum by + iterating through the elements of this field + and adding "weight" to the sum if the node matches + the corresponding matchExpressions; the node(s) + with the highest sum are the most preferred. + items: + description: An empty preferred scheduling term + matches all objects with implicit weight 0 + (i.e. it's a no-op). A null preferred scheduling + term matches no objects (i.e. is also a no-op). + properties: + preference: + description: A node selector term, associated + with the corresponding weight. + properties: + matchExpressions: + description: A list of node selector + requirements by node's labels. + items: + description: A node selector requirement + is a selector that contains values, + a key, and an operator that relates + the key and values. + properties: + key: + description: The label key that + the selector applies to. + type: string + operator: + description: Represents a key's + relationship to a set of values. + Valid operators are In, NotIn, + Exists, DoesNotExist. Gt, and + Lt. + type: string + values: + description: An array of string + values. If the operator is In + or NotIn, the values array must + be non-empty. If the operator + is Exists or DoesNotExist, the + values array must be empty. + If the operator is Gt or Lt, + the values array must have a + single element, which will be + interpreted as an integer. This + array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector + requirements by node's fields. + items: + description: A node selector requirement + is a selector that contains values, + a key, and an operator that relates + the key and values. + properties: + key: + description: The label key that + the selector applies to. + type: string + operator: + description: Represents a key's + relationship to a set of values. + Valid operators are In, NotIn, + Exists, DoesNotExist. Gt, and + Lt. + type: string + values: + description: An array of string + values. If the operator is In + or NotIn, the values array must + be non-empty. If the operator + is Exists or DoesNotExist, the + values array must be empty. + If the operator is Gt or Lt, + the values array must have a + single element, which will be + interpreted as an integer. This + array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + weight: + description: Weight associated with matching + the corresponding nodeSelectorTerm, in + the range 1-100. + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: If the affinity requirements specified + by this field are not met at scheduling time, + the pod will not be scheduled onto the node. + If the affinity requirements specified by this + field cease to be met at some point during pod + execution (e.g. due to an update), the system + may or may not try to eventually evict the pod + from its node. + properties: + nodeSelectorTerms: + description: Required. A list of node selector + terms. The terms are ORed. + items: + description: A null or empty node selector + term matches no objects. The requirements + of them are ANDed. The TopologySelectorTerm + type implements a subset of the NodeSelectorTerm. + properties: + matchExpressions: + description: A list of node selector + requirements by node's labels. + items: + description: A node selector requirement + is a selector that contains values, + a key, and an operator that relates + the key and values. + properties: + key: + description: The label key that + the selector applies to. + type: string + operator: + description: Represents a key's + relationship to a set of values. + Valid operators are In, NotIn, + Exists, DoesNotExist. Gt, and + Lt. + type: string + values: + description: An array of string + values. If the operator is In + or NotIn, the values array must + be non-empty. If the operator + is Exists or DoesNotExist, the + values array must be empty. + If the operator is Gt or Lt, + the values array must have a + single element, which will be + interpreted as an integer. This + array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector + requirements by node's fields. + items: + description: A node selector requirement + is a selector that contains values, + a key, and an operator that relates + the key and values. + properties: + key: + description: The label key that + the selector applies to. + type: string + operator: + description: Represents a key's + relationship to a set of values. + Valid operators are In, NotIn, + Exists, DoesNotExist. Gt, and + Lt. + type: string + values: + description: An array of string + values. If the operator is In + or NotIn, the values array must + be non-empty. If the operator + is Exists or DoesNotExist, the + values array must be empty. + If the operator is Gt or Lt, + the values array must have a + single element, which will be + interpreted as an integer. This + array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + type: array + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + description: Describes pod affinity scheduling rules + (e.g. co-locate this pod in the same node, zone, + etc. as some other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: The scheduler will prefer to schedule + pods to nodes that satisfy the affinity expressions + specified by this field, but it may choose a + node that violates one or more of the expressions. + The node that is most preferred is the one with + the greatest sum of weights, i.e. for each node + that meets all of the scheduling requirements + (resource request, requiredDuringScheduling + affinity expressions, etc.), compute a sum by + iterating through the elements of this field + and adding "weight" to the sum if the node has + pods which matches the corresponding podAffinityTerm; + the node(s) with the highest sum are the most + preferred. + items: + description: The weights of all of the matched + WeightedPodAffinityTerm fields are added per-node + to find the most preferred node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, + associated with the corresponding weight. + properties: + labelSelector: + description: A label query over a set + of resources, in this case pods. + properties: + matchExpressions: + description: matchExpressions is + a list of label selector requirements. + The requirements are ANDed. + items: + description: A label selector + requirement is a selector that + contains values, a key, and + an operator that relates the + key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: operator represents + a key's relationship to + a set of values. Valid operators + are In, NotIn, Exists and + DoesNotExist. + type: string + values: + description: values is an + array of string values. + If the operator is In or + NotIn, the values array + must be non-empty. If the + operator is Exists or DoesNotExist, + the values array must be + empty. This array is replaced + during a strategic merge + patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map + of {key,value} pairs. A single + {key,value} in the matchLabels + map is equivalent to an element + of matchExpressions, whose key + field is "key", the operator is + "In", and the values array contains + only "value". The requirements + are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaceSelector: + description: A label query over the + set of namespaces that the term applies + to. The term is applied to the union + of the namespaces selected by this + field and the ones listed in the namespaces + field. null selector and null or empty + namespaces list means "this pod's + namespace". An empty selector ({}) + matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is + a list of label selector requirements. + The requirements are ANDed. + items: + description: A label selector + requirement is a selector that + contains values, a key, and + an operator that relates the + key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: operator represents + a key's relationship to + a set of values. Valid operators + are In, NotIn, Exists and + DoesNotExist. + type: string + values: + description: values is an + array of string values. + If the operator is In or + NotIn, the values array + must be non-empty. If the + operator is Exists or DoesNotExist, + the values array must be + empty. This array is replaced + during a strategic merge + patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map + of {key,value} pairs. A single + {key,value} in the matchLabels + map is equivalent to an element + of matchExpressions, whose key + field is "key", the operator is + "In", and the values array contains + only "value". The requirements + are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a + static list of namespace names that + the term applies to. The term is applied + to the union of the namespaces listed + in this field and the ones selected + by namespaceSelector. null or empty + namespaces list and null namespaceSelector + means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: This pod should be co-located + (affinity) or not co-located (anti-affinity) + with the pods matching the labelSelector + in the specified namespaces, where + co-located is defined as running on + a node whose value of the label with + key topologyKey matches that of any + node on which any of the selected + pods is running. Empty topologyKey + is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: weight associated with matching + the corresponding podAffinityTerm, in + the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: If the affinity requirements specified + by this field are not met at scheduling time, + the pod will not be scheduled onto the node. + If the affinity requirements specified by this + field cease to be met at some point during pod + execution (e.g. due to a pod label update), + the system may or may not try to eventually + evict the pod from its node. When there are + multiple elements, the lists of nodes corresponding + to each podAffinityTerm are intersected, i.e. + all terms must be satisfied. + items: + description: Defines a set of pods (namely those + matching the labelSelector relative to the + given namespace(s)) that this pod should be + co-located (affinity) or not co-located (anti-affinity) + with, where co-located is defined as running + on a node whose value of the label with key + matches that of any node on + which a pod of the set of pods is running + properties: + labelSelector: + description: A label query over a set of + resources, in this case pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: A label selector requirement + is a selector that contains values, + a key, and an operator that relates + the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: operator represents + a key's relationship to a set + of values. Valid operators are + In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array + of string values. If the operator + is In or NotIn, the values array + must be non-empty. If the operator + is Exists or DoesNotExist, the + values array must be empty. + This array is replaced during + a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. A single {key,value} + in the matchLabels map is equivalent + to an element of matchExpressions, + whose key field is "key", the operator + is "In", and the values array contains + only "value". The requirements are + ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaceSelector: + description: A label query over the set + of namespaces that the term applies to. + The term is applied to the union of the + namespaces selected by this field and + the ones listed in the namespaces field. + null selector and null or empty namespaces + list means "this pod's namespace". An + empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: A label selector requirement + is a selector that contains values, + a key, and an operator that relates + the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: operator represents + a key's relationship to a set + of values. Valid operators are + In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array + of string values. If the operator + is In or NotIn, the values array + must be non-empty. If the operator + is Exists or DoesNotExist, the + values array must be empty. + This array is replaced during + a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. A single {key,value} + in the matchLabels map is equivalent + to an element of matchExpressions, + whose key field is "key", the operator + is "In", and the values array contains + only "value". The requirements are + ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static + list of namespace names that the term + applies to. The term is applied to the + union of the namespaces listed in this + field and the ones selected by namespaceSelector. + null or empty namespaces list and null + namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: This pod should be co-located + (affinity) or not co-located (anti-affinity) + with the pods matching the labelSelector + in the specified namespaces, where co-located + is defined as running on a node whose + value of the label with key topologyKey + matches that of any node on which any + of the selected pods is running. Empty + topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + podAntiAffinity: + description: Describes pod anti-affinity scheduling + rules (e.g. avoid putting this pod in the same node, + zone, etc. as some other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: The scheduler will prefer to schedule + pods to nodes that satisfy the anti-affinity + expressions specified by this field, but it + may choose a node that violates one or more + of the expressions. The node that is most preferred + is the one with the greatest sum of weights, + i.e. for each node that meets all of the scheduling + requirements (resource request, requiredDuringScheduling + anti-affinity expressions, etc.), compute a + sum by iterating through the elements of this + field and adding "weight" to the sum if the + node has pods which matches the corresponding + podAffinityTerm; the node(s) with the highest + sum are the most preferred. + items: + description: The weights of all of the matched + WeightedPodAffinityTerm fields are added per-node + to find the most preferred node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, + associated with the corresponding weight. + properties: + labelSelector: + description: A label query over a set + of resources, in this case pods. + properties: + matchExpressions: + description: matchExpressions is + a list of label selector requirements. + The requirements are ANDed. + items: + description: A label selector + requirement is a selector that + contains values, a key, and + an operator that relates the + key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: operator represents + a key's relationship to + a set of values. Valid operators + are In, NotIn, Exists and + DoesNotExist. + type: string + values: + description: values is an + array of string values. + If the operator is In or + NotIn, the values array + must be non-empty. If the + operator is Exists or DoesNotExist, + the values array must be + empty. This array is replaced + during a strategic merge + patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map + of {key,value} pairs. A single + {key,value} in the matchLabels + map is equivalent to an element + of matchExpressions, whose key + field is "key", the operator is + "In", and the values array contains + only "value". The requirements + are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaceSelector: + description: A label query over the + set of namespaces that the term applies + to. The term is applied to the union + of the namespaces selected by this + field and the ones listed in the namespaces + field. null selector and null or empty + namespaces list means "this pod's + namespace". An empty selector ({}) + matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is + a list of label selector requirements. + The requirements are ANDed. + items: + description: A label selector + requirement is a selector that + contains values, a key, and + an operator that relates the + key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: operator represents + a key's relationship to + a set of values. Valid operators + are In, NotIn, Exists and + DoesNotExist. + type: string + values: + description: values is an + array of string values. + If the operator is In or + NotIn, the values array + must be non-empty. If the + operator is Exists or DoesNotExist, + the values array must be + empty. This array is replaced + during a strategic merge + patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map + of {key,value} pairs. A single + {key,value} in the matchLabels + map is equivalent to an element + of matchExpressions, whose key + field is "key", the operator is + "In", and the values array contains + only "value". The requirements + are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a + static list of namespace names that + the term applies to. The term is applied + to the union of the namespaces listed + in this field and the ones selected + by namespaceSelector. null or empty + namespaces list and null namespaceSelector + means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: This pod should be co-located + (affinity) or not co-located (anti-affinity) + with the pods matching the labelSelector + in the specified namespaces, where + co-located is defined as running on + a node whose value of the label with + key topologyKey matches that of any + node on which any of the selected + pods is running. Empty topologyKey + is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: weight associated with matching + the corresponding podAffinityTerm, in + the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: If the anti-affinity requirements + specified by this field are not met at scheduling + time, the pod will not be scheduled onto the + node. If the anti-affinity requirements specified + by this field cease to be met at some point + during pod execution (e.g. due to a pod label + update), the system may or may not try to eventually + evict the pod from its node. When there are + multiple elements, the lists of nodes corresponding + to each podAffinityTerm are intersected, i.e. + all terms must be satisfied. + items: + description: Defines a set of pods (namely those + matching the labelSelector relative to the + given namespace(s)) that this pod should be + co-located (affinity) or not co-located (anti-affinity) + with, where co-located is defined as running + on a node whose value of the label with key + matches that of any node on + which a pod of the set of pods is running + properties: + labelSelector: + description: A label query over a set of + resources, in this case pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: A label selector requirement + is a selector that contains values, + a key, and an operator that relates + the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: operator represents + a key's relationship to a set + of values. Valid operators are + In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array + of string values. If the operator + is In or NotIn, the values array + must be non-empty. If the operator + is Exists or DoesNotExist, the + values array must be empty. + This array is replaced during + a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. A single {key,value} + in the matchLabels map is equivalent + to an element of matchExpressions, + whose key field is "key", the operator + is "In", and the values array contains + only "value". The requirements are + ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaceSelector: + description: A label query over the set + of namespaces that the term applies to. + The term is applied to the union of the + namespaces selected by this field and + the ones listed in the namespaces field. + null selector and null or empty namespaces + list means "this pod's namespace". An + empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: A label selector requirement + is a selector that contains values, + a key, and an operator that relates + the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: operator represents + a key's relationship to a set + of values. Valid operators are + In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array + of string values. If the operator + is In or NotIn, the values array + must be non-empty. If the operator + is Exists or DoesNotExist, the + values array must be empty. + This array is replaced during + a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. A single {key,value} + in the matchLabels map is equivalent + to an element of matchExpressions, + whose key field is "key", the operator + is "In", and the values array contains + only "value". The requirements are + ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static + list of namespace names that the term + applies to. The term is applied to the + union of the namespaces listed in this + field and the ones selected by namespaceSelector. + null or empty namespaces list and null + namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: This pod should be co-located + (affinity) or not co-located (anti-affinity) + with the pods matching the labelSelector + in the specified namespaces, where co-located + is defined as running on a node whose + value of the label with key topologyKey + matches that of any node on which any + of the selected pods is running. Empty + topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + type: object + resources: + description: 'Resources define Kubernetes resources configuration: + https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + properties: + limits: + properties: + cpu: + pattern: ^([0-9]+m?|[0-9]\.[0-9]{1,3})$ + type: string + memory: + pattern: ^[0-9]+(((\.[0-9]+)?(E|P|T|G|M|k|Ei|Pi|Ti|Gi|Mi|Ki|m)?)|(e[0-9]+))$ + type: string + type: object + requests: + properties: + cpu: + pattern: ^([0-9]+m?|[0-9]\.[0-9]{1,3})$ + type: string + memory: + pattern: ^[0-9]+(((\.[0-9]+)?(E|P|T|G|M|k|Ei|Pi|Ti|Gi|Mi|Ki|m)?)|(e[0-9]+))$ + type: string + type: object + type: object + type: object + required: + - k8s + type: object + ingressGateway: + description: IngressGateway defines component configurations for + Istio Ingress Gateway + properties: + k8s: + description: KubernetesResourcesConfig is a subset of https://istio.io/latest/docs/reference/config/istio.operator.v1alpha1/#KubernetesResourcesSpec + properties: + hpaSpec: + description: HPASpec defines configuration for HorizontalPodAutoscaler + properties: + maxReplicas: + format: int32 + maximum: 2147483647 + minimum: 0 + type: integer + minReplicas: + format: int32 + maximum: 2147483647 + minimum: 0 + type: integer + type: object + resources: + description: 'Resources define Kubernetes resources configuration: + https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + properties: + limits: + properties: + cpu: + pattern: ^([0-9]+m?|[0-9]\.[0-9]{1,3})$ + type: string + memory: + pattern: ^[0-9]+(((\.[0-9]+)?(E|P|T|G|M|k|Ei|Pi|Ti|Gi|Mi|Ki|m)?)|(e[0-9]+))$ + type: string + type: object + requests: + properties: + cpu: + pattern: ^([0-9]+m?|[0-9]\.[0-9]{1,3})$ + type: string + memory: + pattern: ^[0-9]+(((\.[0-9]+)?(E|P|T|G|M|k|Ei|Pi|Ti|Gi|Mi|Ki|m)?)|(e[0-9]+))$ + type: string + type: object + type: object + strategy: + description: Strategy defines rolling update strategy + properties: + rollingUpdate: + description: 'RollingUpdate defines configuration + for rolling updates: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#rolling-update-deployment' + properties: + maxSurge: + anyOf: + - type: integer + - type: string + pattern: ^[0-9]+%?$ + x-kubernetes-int-or-string: true + x-kubernetes-validations: + - message: must not be negative, more than 2147483647 + or an empty string + rule: '(type(self) == int ? self >= 0 && self + <= 2147483647: self.size() >= 0)' + maxUnavailable: + anyOf: + - type: integer + - type: string + pattern: ^((100|[0-9]{1,2})%|[0-9]+)$ + x-kubernetes-int-or-string: true + x-kubernetes-validations: + - message: must not be negative, more than 2147483647 + or an empty string + rule: '(type(self) == int ? self >= 0 && self + <= 2147483647: self.size() >= 0)' + type: object + required: + - rollingUpdate + type: object + type: object + required: + - k8s + type: object + pilot: + description: Pilot defines component configuration for Istiod + properties: + k8s: + description: KubernetesResourcesConfig is a subset of https://istio.io/latest/docs/reference/config/istio.operator.v1alpha1/#KubernetesResourcesSpec + properties: + hpaSpec: + description: HPASpec defines configuration for HorizontalPodAutoscaler + properties: + maxReplicas: + format: int32 + maximum: 2147483647 + minimum: 0 + type: integer + minReplicas: + format: int32 + maximum: 2147483647 + minimum: 0 + type: integer + type: object + resources: + description: 'Resources define Kubernetes resources configuration: + https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + properties: + limits: + properties: + cpu: + pattern: ^([0-9]+m?|[0-9]\.[0-9]{1,3})$ + type: string + memory: + pattern: ^[0-9]+(((\.[0-9]+)?(E|P|T|G|M|k|Ei|Pi|Ti|Gi|Mi|Ki|m)?)|(e[0-9]+))$ + type: string + type: object + requests: + properties: + cpu: + pattern: ^([0-9]+m?|[0-9]\.[0-9]{1,3})$ + type: string + memory: + pattern: ^[0-9]+(((\.[0-9]+)?(E|P|T|G|M|k|Ei|Pi|Ti|Gi|Mi|Ki|m)?)|(e[0-9]+))$ + type: string + type: object + type: object + strategy: + description: Strategy defines rolling update strategy + properties: + rollingUpdate: + description: 'RollingUpdate defines configuration + for rolling updates: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#rolling-update-deployment' + properties: + maxSurge: + anyOf: + - type: integer + - type: string + pattern: ^[0-9]+%?$ + x-kubernetes-int-or-string: true + x-kubernetes-validations: + - message: must not be negative, more than 2147483647 + or an empty string + rule: '(type(self) == int ? self >= 0 && self + <= 2147483647: self.size() >= 0)' + maxUnavailable: + anyOf: + - type: integer + - type: string + pattern: ^((100|[0-9]{1,2})%|[0-9]+)$ + x-kubernetes-int-or-string: true + x-kubernetes-validations: + - message: must not be negative, more than 2147483647 + or an empty string + rule: '(type(self) == int ? self >= 0 && self + <= 2147483647: self.size() >= 0)' + type: object + required: + - rollingUpdate + type: object + type: object + required: + - k8s + type: object + proxy: + description: Proxy defines component configuration for Istio proxy + sidecar + properties: + k8s: + description: ProxyK8sConfig is a subset of https://istio.io/latest/docs/reference/config/istio.operator.v1alpha1/#KubernetesResourcesSpec + properties: + resources: + description: 'Resources define Kubernetes resources configuration: + https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + properties: + limits: + properties: + cpu: + pattern: ^([0-9]+m?|[0-9]\.[0-9]{1,3})$ + type: string + memory: + pattern: ^[0-9]+(((\.[0-9]+)?(E|P|T|G|M|k|Ei|Pi|Ti|Gi|Mi|Ki|m)?)|(e[0-9]+))$ + type: string + type: object + requests: + properties: + cpu: + pattern: ^([0-9]+m?|[0-9]\.[0-9]{1,3})$ + type: string + memory: + pattern: ^[0-9]+(((\.[0-9]+)?(E|P|T|G|M|k|Ei|Pi|Ti|Gi|Mi|Ki|m)?)|(e[0-9]+))$ + type: string + type: object + type: object + type: object + required: + - k8s + type: object + type: object + config: + description: Config is the configuration for the Istio installation. + properties: + numTrustedProxies: + description: Defines the number of trusted proxies deployed in + front of the Istio gateway proxy. + maximum: 4294967295 + minimum: 0 + type: integer + type: object + type: object + status: + description: IstioStatus defines the observed state of IstioCR. + properties: + conditions: + description: Conditions associated with IstioStatus. + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + description: + description: Description of Istio status + type: string + state: + description: State signifies current state of CustomObject. Value + can be one of ("Ready", "Processing", "Error", "Deleting", "Warning"). + enum: + - Processing + - Deleting + - Ready + - Error + - Warning + type: string + required: + - state + type: object + type: object + served: true + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - jsonPath: .status.state + name: State + type: string + name: v1alpha2 + schema: + openAPIV3Schema: + description: Contains Istio CR specification and current status. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: Defines the desired specification for installing or updating + Istio. + properties: + components: + properties: + cni: + description: Cni defines component configuration for Istio CNI + DaemonSet + properties: + k8s: + description: CniK8sConfig is a subset of https://istio.io/latest/docs/reference/config/istio.operator.v1alpha1/#KubernetesResourcesSpec + properties: + affinity: + description: Affinity is a group of affinity scheduling + rules. + properties: + nodeAffinity: + description: Describes node affinity scheduling rules + for the pod. + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: The scheduler will prefer to schedule + pods to nodes that satisfy the affinity expressions + specified by this field, but it may choose a + node that violates one or more of the expressions. + The node that is most preferred is the one with + the greatest sum of weights, i.e. for each node + that meets all of the scheduling requirements + (resource request, requiredDuringScheduling + affinity expressions, etc.), compute a sum by + iterating through the elements of this field + and adding "weight" to the sum if the node matches + the corresponding matchExpressions; the node(s) + with the highest sum are the most preferred. + items: + description: An empty preferred scheduling term + matches all objects with implicit weight 0 + (i.e. it's a no-op). A null preferred scheduling + term matches no objects (i.e. is also a no-op). + properties: + preference: + description: A node selector term, associated + with the corresponding weight. + properties: + matchExpressions: + description: A list of node selector + requirements by node's labels. + items: + description: A node selector requirement + is a selector that contains values, + a key, and an operator that relates + the key and values. + properties: + key: + description: The label key that + the selector applies to. + type: string + operator: + description: Represents a key's + relationship to a set of values. + Valid operators are In, NotIn, + Exists, DoesNotExist. Gt, and + Lt. + type: string + values: + description: An array of string + values. If the operator is In + or NotIn, the values array must + be non-empty. If the operator + is Exists or DoesNotExist, the + values array must be empty. + If the operator is Gt or Lt, + the values array must have a + single element, which will be + interpreted as an integer. This + array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector + requirements by node's fields. + items: + description: A node selector requirement + is a selector that contains values, + a key, and an operator that relates + the key and values. + properties: + key: + description: The label key that + the selector applies to. + type: string + operator: + description: Represents a key's + relationship to a set of values. + Valid operators are In, NotIn, + Exists, DoesNotExist. Gt, and + Lt. + type: string + values: + description: An array of string + values. If the operator is In + or NotIn, the values array must + be non-empty. If the operator + is Exists or DoesNotExist, the + values array must be empty. + If the operator is Gt or Lt, + the values array must have a + single element, which will be + interpreted as an integer. This + array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + weight: + description: Weight associated with matching + the corresponding nodeSelectorTerm, in + the range 1-100. + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: If the affinity requirements specified + by this field are not met at scheduling time, + the pod will not be scheduled onto the node. + If the affinity requirements specified by this + field cease to be met at some point during pod + execution (e.g. due to an update), the system + may or may not try to eventually evict the pod + from its node. + properties: + nodeSelectorTerms: + description: Required. A list of node selector + terms. The terms are ORed. + items: + description: A null or empty node selector + term matches no objects. The requirements + of them are ANDed. The TopologySelectorTerm + type implements a subset of the NodeSelectorTerm. + properties: + matchExpressions: + description: A list of node selector + requirements by node's labels. + items: + description: A node selector requirement + is a selector that contains values, + a key, and an operator that relates + the key and values. + properties: + key: + description: The label key that + the selector applies to. + type: string + operator: + description: Represents a key's + relationship to a set of values. + Valid operators are In, NotIn, + Exists, DoesNotExist. Gt, and + Lt. + type: string + values: + description: An array of string + values. If the operator is In + or NotIn, the values array must + be non-empty. If the operator + is Exists or DoesNotExist, the + values array must be empty. + If the operator is Gt or Lt, + the values array must have a + single element, which will be + interpreted as an integer. This + array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector + requirements by node's fields. + items: + description: A node selector requirement + is a selector that contains values, + a key, and an operator that relates + the key and values. + properties: + key: + description: The label key that + the selector applies to. + type: string + operator: + description: Represents a key's + relationship to a set of values. + Valid operators are In, NotIn, + Exists, DoesNotExist. Gt, and + Lt. + type: string + values: + description: An array of string + values. If the operator is In + or NotIn, the values array must + be non-empty. If the operator + is Exists or DoesNotExist, the + values array must be empty. + If the operator is Gt or Lt, + the values array must have a + single element, which will be + interpreted as an integer. This + array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + type: array + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + description: Describes pod affinity scheduling rules + (e.g. co-locate this pod in the same node, zone, + etc. as some other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: The scheduler will prefer to schedule + pods to nodes that satisfy the affinity expressions + specified by this field, but it may choose a + node that violates one or more of the expressions. + The node that is most preferred is the one with + the greatest sum of weights, i.e. for each node + that meets all of the scheduling requirements + (resource request, requiredDuringScheduling + affinity expressions, etc.), compute a sum by + iterating through the elements of this field + and adding "weight" to the sum if the node has + pods which matches the corresponding podAffinityTerm; + the node(s) with the highest sum are the most + preferred. + items: + description: The weights of all of the matched + WeightedPodAffinityTerm fields are added per-node + to find the most preferred node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, + associated with the corresponding weight. + properties: + labelSelector: + description: A label query over a set + of resources, in this case pods. + properties: + matchExpressions: + description: matchExpressions is + a list of label selector requirements. + The requirements are ANDed. + items: + description: A label selector + requirement is a selector that + contains values, a key, and + an operator that relates the + key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: operator represents + a key's relationship to + a set of values. Valid operators + are In, NotIn, Exists and + DoesNotExist. + type: string + values: + description: values is an + array of string values. + If the operator is In or + NotIn, the values array + must be non-empty. If the + operator is Exists or DoesNotExist, + the values array must be + empty. This array is replaced + during a strategic merge + patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map + of {key,value} pairs. A single + {key,value} in the matchLabels + map is equivalent to an element + of matchExpressions, whose key + field is "key", the operator is + "In", and the values array contains + only "value". The requirements + are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaceSelector: + description: A label query over the + set of namespaces that the term applies + to. The term is applied to the union + of the namespaces selected by this + field and the ones listed in the namespaces + field. null selector and null or empty + namespaces list means "this pod's + namespace". An empty selector ({}) + matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is + a list of label selector requirements. + The requirements are ANDed. + items: + description: A label selector + requirement is a selector that + contains values, a key, and + an operator that relates the + key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: operator represents + a key's relationship to + a set of values. Valid operators + are In, NotIn, Exists and + DoesNotExist. + type: string + values: + description: values is an + array of string values. + If the operator is In or + NotIn, the values array + must be non-empty. If the + operator is Exists or DoesNotExist, + the values array must be + empty. This array is replaced + during a strategic merge + patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map + of {key,value} pairs. A single + {key,value} in the matchLabels + map is equivalent to an element + of matchExpressions, whose key + field is "key", the operator is + "In", and the values array contains + only "value". The requirements + are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a + static list of namespace names that + the term applies to. The term is applied + to the union of the namespaces listed + in this field and the ones selected + by namespaceSelector. null or empty + namespaces list and null namespaceSelector + means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: This pod should be co-located + (affinity) or not co-located (anti-affinity) + with the pods matching the labelSelector + in the specified namespaces, where + co-located is defined as running on + a node whose value of the label with + key topologyKey matches that of any + node on which any of the selected + pods is running. Empty topologyKey + is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: weight associated with matching + the corresponding podAffinityTerm, in + the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: If the affinity requirements specified + by this field are not met at scheduling time, + the pod will not be scheduled onto the node. + If the affinity requirements specified by this + field cease to be met at some point during pod + execution (e.g. due to a pod label update), + the system may or may not try to eventually + evict the pod from its node. When there are + multiple elements, the lists of nodes corresponding + to each podAffinityTerm are intersected, i.e. + all terms must be satisfied. + items: + description: Defines a set of pods (namely those + matching the labelSelector relative to the + given namespace(s)) that this pod should be + co-located (affinity) or not co-located (anti-affinity) + with, where co-located is defined as running + on a node whose value of the label with key + matches that of any node on + which a pod of the set of pods is running + properties: + labelSelector: + description: A label query over a set of + resources, in this case pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: A label selector requirement + is a selector that contains values, + a key, and an operator that relates + the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: operator represents + a key's relationship to a set + of values. Valid operators are + In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array + of string values. If the operator + is In or NotIn, the values array + must be non-empty. If the operator + is Exists or DoesNotExist, the + values array must be empty. + This array is replaced during + a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. A single {key,value} + in the matchLabels map is equivalent + to an element of matchExpressions, + whose key field is "key", the operator + is "In", and the values array contains + only "value". The requirements are + ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaceSelector: + description: A label query over the set + of namespaces that the term applies to. + The term is applied to the union of the + namespaces selected by this field and + the ones listed in the namespaces field. + null selector and null or empty namespaces + list means "this pod's namespace". An + empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: A label selector requirement + is a selector that contains values, + a key, and an operator that relates + the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: operator represents + a key's relationship to a set + of values. Valid operators are + In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array + of string values. If the operator + is In or NotIn, the values array + must be non-empty. If the operator + is Exists or DoesNotExist, the + values array must be empty. + This array is replaced during + a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. A single {key,value} + in the matchLabels map is equivalent + to an element of matchExpressions, + whose key field is "key", the operator + is "In", and the values array contains + only "value". The requirements are + ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static + list of namespace names that the term + applies to. The term is applied to the + union of the namespaces listed in this + field and the ones selected by namespaceSelector. + null or empty namespaces list and null + namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: This pod should be co-located + (affinity) or not co-located (anti-affinity) + with the pods matching the labelSelector + in the specified namespaces, where co-located + is defined as running on a node whose + value of the label with key topologyKey + matches that of any node on which any + of the selected pods is running. Empty + topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + podAntiAffinity: + description: Describes pod anti-affinity scheduling + rules (e.g. avoid putting this pod in the same node, + zone, etc. as some other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: The scheduler will prefer to schedule + pods to nodes that satisfy the anti-affinity + expressions specified by this field, but it + may choose a node that violates one or more + of the expressions. The node that is most preferred + is the one with the greatest sum of weights, + i.e. for each node that meets all of the scheduling + requirements (resource request, requiredDuringScheduling + anti-affinity expressions, etc.), compute a + sum by iterating through the elements of this + field and adding "weight" to the sum if the + node has pods which matches the corresponding + podAffinityTerm; the node(s) with the highest + sum are the most preferred. + items: + description: The weights of all of the matched + WeightedPodAffinityTerm fields are added per-node + to find the most preferred node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, + associated with the corresponding weight. + properties: + labelSelector: + description: A label query over a set + of resources, in this case pods. + properties: + matchExpressions: + description: matchExpressions is + a list of label selector requirements. + The requirements are ANDed. + items: + description: A label selector + requirement is a selector that + contains values, a key, and + an operator that relates the + key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: operator represents + a key's relationship to + a set of values. Valid operators + are In, NotIn, Exists and + DoesNotExist. + type: string + values: + description: values is an + array of string values. + If the operator is In or + NotIn, the values array + must be non-empty. If the + operator is Exists or DoesNotExist, + the values array must be + empty. This array is replaced + during a strategic merge + patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map + of {key,value} pairs. A single + {key,value} in the matchLabels + map is equivalent to an element + of matchExpressions, whose key + field is "key", the operator is + "In", and the values array contains + only "value". The requirements + are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaceSelector: + description: A label query over the + set of namespaces that the term applies + to. The term is applied to the union + of the namespaces selected by this + field and the ones listed in the namespaces + field. null selector and null or empty + namespaces list means "this pod's + namespace". An empty selector ({}) + matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is + a list of label selector requirements. + The requirements are ANDed. + items: + description: A label selector + requirement is a selector that + contains values, a key, and + an operator that relates the + key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: operator represents + a key's relationship to + a set of values. Valid operators + are In, NotIn, Exists and + DoesNotExist. + type: string + values: + description: values is an + array of string values. + If the operator is In or + NotIn, the values array + must be non-empty. If the + operator is Exists or DoesNotExist, + the values array must be + empty. This array is replaced + during a strategic merge + patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map + of {key,value} pairs. A single + {key,value} in the matchLabels + map is equivalent to an element + of matchExpressions, whose key + field is "key", the operator is + "In", and the values array contains + only "value". The requirements + are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a + static list of namespace names that + the term applies to. The term is applied + to the union of the namespaces listed + in this field and the ones selected + by namespaceSelector. null or empty + namespaces list and null namespaceSelector + means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: This pod should be co-located + (affinity) or not co-located (anti-affinity) + with the pods matching the labelSelector + in the specified namespaces, where + co-located is defined as running on + a node whose value of the label with + key topologyKey matches that of any + node on which any of the selected + pods is running. Empty topologyKey + is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: weight associated with matching + the corresponding podAffinityTerm, in + the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: If the anti-affinity requirements + specified by this field are not met at scheduling + time, the pod will not be scheduled onto the + node. If the anti-affinity requirements specified + by this field cease to be met at some point + during pod execution (e.g. due to a pod label + update), the system may or may not try to eventually + evict the pod from its node. When there are + multiple elements, the lists of nodes corresponding + to each podAffinityTerm are intersected, i.e. + all terms must be satisfied. + items: + description: Defines a set of pods (namely those + matching the labelSelector relative to the + given namespace(s)) that this pod should be + co-located (affinity) or not co-located (anti-affinity) + with, where co-located is defined as running + on a node whose value of the label with key + matches that of any node on + which a pod of the set of pods is running + properties: + labelSelector: + description: A label query over a set of + resources, in this case pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: A label selector requirement + is a selector that contains values, + a key, and an operator that relates + the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: operator represents + a key's relationship to a set + of values. Valid operators are + In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array + of string values. If the operator + is In or NotIn, the values array + must be non-empty. If the operator + is Exists or DoesNotExist, the + values array must be empty. + This array is replaced during + a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. A single {key,value} + in the matchLabels map is equivalent + to an element of matchExpressions, + whose key field is "key", the operator + is "In", and the values array contains + only "value". The requirements are + ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaceSelector: + description: A label query over the set + of namespaces that the term applies to. + The term is applied to the union of the + namespaces selected by this field and + the ones listed in the namespaces field. + null selector and null or empty namespaces + list means "this pod's namespace". An + empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: A label selector requirement + is a selector that contains values, + a key, and an operator that relates + the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: operator represents + a key's relationship to a set + of values. Valid operators are + In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array + of string values. If the operator + is In or NotIn, the values array + must be non-empty. If the operator + is Exists or DoesNotExist, the + values array must be empty. + This array is replaced during + a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. A single {key,value} + in the matchLabels map is equivalent + to an element of matchExpressions, + whose key field is "key", the operator + is "In", and the values array contains + only "value". The requirements are + ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static + list of namespace names that the term + applies to. The term is applied to the + union of the namespaces listed in this + field and the ones selected by namespaceSelector. + null or empty namespaces list and null + namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: This pod should be co-located + (affinity) or not co-located (anti-affinity) + with the pods matching the labelSelector + in the specified namespaces, where co-located + is defined as running on a node whose + value of the label with key topologyKey + matches that of any node on which any + of the selected pods is running. Empty + topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + type: object + resources: + description: 'Resources define Kubernetes resources configuration: + https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + properties: + limits: + properties: + cpu: + pattern: ^([0-9]+m?|[0-9]\.[0-9]{1,3})$ + type: string + memory: + pattern: ^[0-9]+(((\.[0-9]+)?(E|P|T|G|M|k|Ei|Pi|Ti|Gi|Mi|Ki|m)?)|(e[0-9]+))$ + type: string + type: object + requests: + properties: + cpu: + pattern: ^([0-9]+m?|[0-9]\.[0-9]{1,3})$ + type: string + memory: + pattern: ^[0-9]+(((\.[0-9]+)?(E|P|T|G|M|k|Ei|Pi|Ti|Gi|Mi|Ki|m)?)|(e[0-9]+))$ + type: string + type: object + type: object + type: object + required: + - k8s + type: object + ingressGateway: + description: IngressGateway defines component configurations for + Istio Ingress Gateway + properties: + k8s: + description: KubernetesResourcesConfig is a subset of https://istio.io/latest/docs/reference/config/istio.operator.v1alpha1/#KubernetesResourcesSpec + properties: + hpaSpec: + description: HPASpec defines configuration for HorizontalPodAutoscaler + properties: + maxReplicas: + format: int32 + maximum: 2147483647 + minimum: 0 + type: integer + minReplicas: + format: int32 + maximum: 2147483647 + minimum: 0 + type: integer + type: object + resources: + description: 'Resources define Kubernetes resources configuration: + https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + properties: + limits: + properties: + cpu: + pattern: ^([0-9]+m?|[0-9]\.[0-9]{1,3})$ + type: string + memory: + pattern: ^[0-9]+(((\.[0-9]+)?(E|P|T|G|M|k|Ei|Pi|Ti|Gi|Mi|Ki|m)?)|(e[0-9]+))$ + type: string + type: object + requests: + properties: + cpu: + pattern: ^([0-9]+m?|[0-9]\.[0-9]{1,3})$ + type: string + memory: + pattern: ^[0-9]+(((\.[0-9]+)?(E|P|T|G|M|k|Ei|Pi|Ti|Gi|Mi|Ki|m)?)|(e[0-9]+))$ + type: string + type: object + type: object + strategy: + description: Strategy defines rolling update strategy + properties: + rollingUpdate: + description: 'RollingUpdate defines configuration + for rolling updates: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#rolling-update-deployment' + properties: + maxSurge: + anyOf: + - type: integer + - type: string + pattern: ^[0-9]+%?$ + x-kubernetes-int-or-string: true + x-kubernetes-validations: + - message: must not be negative, more than 2147483647 + or an empty string + rule: '(type(self) == int ? self >= 0 && self + <= 2147483647: self.size() >= 0)' + maxUnavailable: + anyOf: + - type: integer + - type: string + pattern: ^((100|[0-9]{1,2})%|[0-9]+)$ + x-kubernetes-int-or-string: true + x-kubernetes-validations: + - message: must not be negative, more than 2147483647 + or an empty string + rule: '(type(self) == int ? self >= 0 && self + <= 2147483647: self.size() >= 0)' + type: object + required: + - rollingUpdate + type: object + type: object + required: + - k8s + type: object + pilot: + description: Pilot defines component configuration for Istiod + properties: + k8s: + description: KubernetesResourcesConfig is a subset of https://istio.io/latest/docs/reference/config/istio.operator.v1alpha1/#KubernetesResourcesSpec + properties: + hpaSpec: + description: HPASpec defines configuration for HorizontalPodAutoscaler + properties: + maxReplicas: + format: int32 + maximum: 2147483647 + minimum: 0 + type: integer + minReplicas: + format: int32 + maximum: 2147483647 + minimum: 0 + type: integer + type: object + resources: + description: 'Resources define Kubernetes resources configuration: + https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + properties: + limits: + properties: + cpu: + pattern: ^([0-9]+m?|[0-9]\.[0-9]{1,3})$ + type: string + memory: + pattern: ^[0-9]+(((\.[0-9]+)?(E|P|T|G|M|k|Ei|Pi|Ti|Gi|Mi|Ki|m)?)|(e[0-9]+))$ + type: string + type: object + requests: + properties: + cpu: + pattern: ^([0-9]+m?|[0-9]\.[0-9]{1,3})$ + type: string + memory: + pattern: ^[0-9]+(((\.[0-9]+)?(E|P|T|G|M|k|Ei|Pi|Ti|Gi|Mi|Ki|m)?)|(e[0-9]+))$ + type: string + type: object + type: object + strategy: + description: Strategy defines rolling update strategy + properties: + rollingUpdate: + description: 'RollingUpdate defines configuration + for rolling updates: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#rolling-update-deployment' + properties: + maxSurge: + anyOf: + - type: integer + - type: string + pattern: ^[0-9]+%?$ + x-kubernetes-int-or-string: true + x-kubernetes-validations: + - message: must not be negative, more than 2147483647 + or an empty string + rule: '(type(self) == int ? self >= 0 && self + <= 2147483647: self.size() >= 0)' + maxUnavailable: + anyOf: + - type: integer + - type: string + pattern: ^((100|[0-9]{1,2})%|[0-9]+)$ + x-kubernetes-int-or-string: true + x-kubernetes-validations: + - message: must not be negative, more than 2147483647 + or an empty string + rule: '(type(self) == int ? self >= 0 && self + <= 2147483647: self.size() >= 0)' + type: object + required: + - rollingUpdate + type: object + type: object + required: + - k8s + type: object + proxy: + description: Proxy defines component configuration for Istio proxy + sidecar + properties: + k8s: + description: ProxyK8sConfig is a subset of https://istio.io/latest/docs/reference/config/istio.operator.v1alpha1/#KubernetesResourcesSpec + properties: + resources: + description: 'Resources define Kubernetes resources configuration: + https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + properties: + limits: + properties: + cpu: + pattern: ^([0-9]+m?|[0-9]\.[0-9]{1,3})$ + type: string + memory: + pattern: ^[0-9]+(((\.[0-9]+)?(E|P|T|G|M|k|Ei|Pi|Ti|Gi|Mi|Ki|m)?)|(e[0-9]+))$ + type: string + type: object + requests: + properties: + cpu: + pattern: ^([0-9]+m?|[0-9]\.[0-9]{1,3})$ + type: string + memory: + pattern: ^[0-9]+(((\.[0-9]+)?(E|P|T|G|M|k|Ei|Pi|Ti|Gi|Mi|Ki|m)?)|(e[0-9]+))$ + type: string + type: object + type: object + type: object + required: + - k8s + type: object + type: object + config: + description: Config is the configuration for the Istio installation. + properties: + numTrustedProxies: + description: Defines the number of trusted proxies deployed in + front of the Istio gateway proxy. + maximum: 4294967295 + minimum: 0 + type: integer + type: object + type: object + status: + description: IstioStatus defines the observed state of IstioCR. + properties: + conditions: + description: Conditions associated with IstioStatus. + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + description: + description: Description of Istio status + type: string + state: + description: State signifies current state of CustomObject. Value + can be one of ("Ready", "Processing", "Error", "Deleting", "Warning"). + enum: + - Processing + - Deleting + - Ready + - Error + - Warning + type: string + required: + - state + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/component: istio-operator.kyma-project.io + app.kubernetes.io/created-by: istio + app.kubernetes.io/instance: controller-manager-sa + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: serviceaccount + app.kubernetes.io/part-of: istio + name: istio-controller-manager + namespace: kyma-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/component: istio-operator.kyma-project.io + app.kubernetes.io/created-by: istio + app.kubernetes.io/instance: leader-election-role + app.kubernetes.io/name: role + app.kubernetes.io/part-of: istio + app.kubernets.io/managed-by: kustomize + name: istio-leader-election-role + namespace: kyma-system +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + labels: + app.kubernetes.io/component: istio-operator.kyma-project.io + name: istio-manager-role +rules: +- apiGroups: + - "" + resources: + - namespaces + verbs: + - create + - get + - patch + - update +- apiGroups: + - "" + resources: + - nodes + verbs: + - get + - list + - watch +- apiGroups: + - operator.kyma-project.io + resources: + - istios + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - operator.kyma-project.io + resources: + - istios/finalizers + verbs: + - update +- apiGroups: + - operator.kyma-project.io + resources: + - istios/status + verbs: + - get + - patch + - update +- apiGroups: + - authentication.istio.io + resources: + - '*' + verbs: + - '*' +- apiGroups: + - config.istio.io + resources: + - '*' + verbs: + - '*' +- apiGroups: + - install.istio.io + resources: + - '*' + verbs: + - '*' +- apiGroups: + - networking.istio.io + resources: + - '*' + verbs: + - '*' +- apiGroups: + - security.istio.io + resources: + - '*' + verbs: + - '*' +- apiGroups: + - telemetry.istio.io + resources: + - '*' + verbs: + - '*' +- apiGroups: + - extensions.istio.io + resources: + - '*' + verbs: + - '*' +- apiGroups: + - admissionregistration.k8s.io + resources: + - mutatingwebhookconfigurations + - validatingwebhookconfigurations + verbs: + - '*' +- apiGroups: + - apiextensions.k8s.io + resources: + - customresourcedefinitions.apiextensions.k8s.io + - customresourcedefinitions + verbs: + - '*' +- apiGroups: + - apps + - extensions + resources: + - daemonsets + - deployments + - deployments/finalizers + - replicasets + - statefulsets + verbs: + - '*' +- apiGroups: + - autoscaling + resources: + - horizontalpodautoscalers + verbs: + - '*' +- apiGroups: + - monitoring.coreos.com + resources: + - servicemonitors + verbs: + - get + - create + - update +- apiGroups: + - policy + resources: + - poddisruptionbudgets + verbs: + - '*' +- apiGroups: + - rbac.authorization.k8s.io + resources: + - clusterrolebindings + - clusterroles + - roles + - rolebindings + verbs: + - '*' +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - create + - update +- apiGroups: + - "" + resources: + - configmaps + - endpoints + - events + - namespaces + - pods + - pods/proxy + - pods/portforward + - persistentvolumeclaims + - secrets + - services + - serviceaccounts + - resourcequotas + verbs: + - '*' +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/component: istio-operator.kyma-project.io + app.kubernetes.io/created-by: istio + app.kubernetes.io/instance: leader-election-rolebinding + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: rolebinding + app.kubernetes.io/part-of: istio + name: istio-leader-election-rolebinding + namespace: kyma-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: istio-leader-election-role +subjects: +- kind: ServiceAccount + name: istio-controller-manager + namespace: kyma-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/component: istio-operator.kyma-project.io + app.kubernetes.io/created-by: istio + app.kubernetes.io/instance: manager-rolebinding + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: clusterrolebinding + app.kubernetes.io/part-of: istio + name: istio-manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: istio-manager-role +subjects: +- kind: ServiceAccount + name: istio-controller-manager + namespace: kyma-system +--- +apiVersion: v1 +data: + dataSources: |- + podSelector: + resource: + kind: Pod + version: v1 + filter: $matchByLabelSelector($item, $root.spec.selector.matchLabels) + details: | + header: + - source: spec.action + name: spec.action + - source: spec.provider + widget: Labels + name: spec.provider + resourceGraph: + colorVariant: 3 + dataSources: + - source: podSelector + body: + - widget: Table + name: spec.rules + source: spec.rules + showHeader: false + visibility: $exists($value) + collapsibleTitle: '"Rule #" & $string($index + 1) & (" " & $join($keys($item), " "))' + collapsible: + - source: $item.from + widget: Table + name: spec.rules.from + disablePadding: true + showHeader: false + visibility: $exists($value) + collapsibleTitle: '"From #" & $string($index + 1) & (" " & $join($keys($item.source), " "))' + collapsible: + - source: source + widget: Panel + name: spec.rules.from.source + children: + - source: principals + name: spec.rules.from.source.principals + widget: JoinedArray + visibility: $exists($value) + - source: notPrincipals + name: spec.rules.from.source.notPrincipals + widget: Labels + visibility: $exists($value) + - source: requestPrincipals + name: spec.rules.from.source.requestPrincipals + widget: Labels + visibility: $exists($value) + - source: notRequestPrincipals + name: spec.rules.from.source.notRequestPrincipals + widget: Labels + visibility: $exists($value) + - source: namespaces + name: spec.rules.from.source.namespaces + widget: Labels + visibility: $exists($value) + - source: notNamespaces + name: spec.rules.from.source.notNamespaces + widget: Labels + visibility: $exists($value) + - source: ipBlocks + name: spec.rules.from.source.ipBlocks + widget: Labels + visibility: $exists($value) + - source: notIpBlocks + name: spec.rules.from.source.notIpBlocks + widget: Labels + visibility: $exists($value) + - source: remoteIpBlocks + name: spec.rules.from.source.remoteIpBlocks + widget: Labels + visibility: $exists($value) + - source: notRemoteIpBlocks + name: spec.rules.from.source.notRemoteIpBlocks + widget: Labels + visibility: $exists($value) + - source: $item.to + widget: Table + name: spec.rules.to + disablePadding: true + showHeader: false + visibility: $exists($value) + collapsibleTitle: '"To #" & $string($index + 1) & (" " & $join($keys($item.operation), " "))' + collapsible: + - source: operation + widget: Panel + name: spec.rules.to.operation + children: + - source: Hosts + name: spec.rules.to.operation.hosts + widget: Labels + visibility: $exists($value) + - source: notHosts + name: spec.rules.to.operation.notHosts + widget: Labels + visibility: $exists($value) + - source: ports + name: spec.rules.to.operation.ports + widget: Labels + visibility: $exists($value) + - source: notPorts + name: spec.rules.to.operation.notPorts + widget: Labels + visibility: $exists($value) + - source: methods + name: spec.rules.to.operation.methods + widget: Labels + visibility: $exists($value) + - source: notMethods + name: spec.rules.to.operation.notMethods + widget: Labels + visibility: $exists($value) + - source: paths + name: spec.rules.to.operation.paths + widget: Labels + visibility: $exists($value) + - source: notPaths + name: spec.rules.to.operation.notPaths + widget: Labels + visibility: $exists($value) + - source: when + widget: Table + name: spec.rules.when + visibility: $exists($value) + children: + - source: key + name: spec.rules.when.key + visibility: $exists($value) + - source: values + name: spec.rules.when.values + widget: JoinedArray + separator: break + visibility: $exists($value) + - source: notValues + name: spec.rules.when.notValues + widget: JoinedArray + separator: break + visibility: $exists($value) + - widget: Panel + name: spec.selector.matchLabels + disablePadding: true + children: + - source: $podSelector() + widget: ResourceList + disableCreate: true + visibility: $exists($root.spec.selector.matchLabels) and $boolean($root.spec.selector.matchLabels) + - source: spec.selector + widget: Panel + name: selector.matchesAllPods + visibility: $not($exists($value)) or $not($boolean($value)) + header: + - source: spec.selector.matchLabels + widget: Labels + name: spec.selector.matchLabels + visibility: $exists($value) and $boolean($value) + form: | + - path: spec.selector.matchLabels + widget: KeyValuePair + defaultExpanded: true + - path: spec.action + placeholder: placeholders.dropdown + simple: true + description: description.action + - path: spec.provider + widget: FormGroup + children: + - path: name + - path: spec.rules + widget: GenericList + simple: true + children: + - path: '[].from' + simple: true + widget: GenericList + children: + - path: '[].source' + simple: true + widget: FormGroup + defaultExpanded: true + children: + - path: principals + simple: true + widget: SimpleList + description: description.rules.from.principals + children: + - path: '[]' + simple: true + - path: notPrincipals + simple: true + widget: SimpleList + description: description.rules.from.notPrincipals + children: + - path: '[]' + simple: true + - path: requestPrincipals + simple: true + widget: SimpleList + description: description.rules.from.requestPrincipals + children: + - path: '[]' + simple: true + - path: notRequestPrincipals + simple: true + widget: SimpleList + description: description.rules.from.notRequestPrincipals + children: + - path: '[]' + simple: true + - path: namespaces + simple: true + widget: SimpleList + description: description.rules.from.namespaces + children: + - path: '[]' + simple: true + - path: notNamespaces + simple: true + widget: SimpleList + description: description.rules.from.notNamespaces + children: + - path: '[]' + simple: true + - path: ipBlocks + simple: true + widget: SimpleList + description: description.rules.from.ipBlocks + children: + - path: '[]' + simple: true + - path: notIpBlocks + simple: true + widget: SimpleList + description: description.rules.from.notIpBlocks + children: + - path: '[]' + simple: true + - path: remoteIpBlocks + simple: true + widget: SimpleList + description: description.rules.from.remoteIpBlocks + children: + - path: '[]' + simple: true + - path: notRemoteIpBlocks + simple: true + widget: SimpleList + description: description.rules.from.notRemoteIpBlocks + children: + - path: '[]' + simple: true + - path: '[].to' + simple: true + widget: GenericList + children: + - path: '[].operation' + simple: true + widget: FormGroup + defaultExpanded: true + children: + - path: hosts + simple: true + widget: SimpleList + description: description.rules.to.hosts + children: + - path: '[]' + simple: true + - path: notHosts + simple: true + widget: SimpleList + description: description.rules.to.notHosts + children: + - path: '[]' + simple: true + - path: ports + simple: true + widget: SimpleList + description: description.rules.to.ports + children: + - path: '[]' + simple: true + - path: notPorts + simple: true + widget: SimpleList + description: description.rules.to.notPorts + children: + - path: '[]' + simple: true + - path: methods + simple: true + widget: SimpleList + description: description.rules.to.methods + children: + - path: '[]' + simple: true + - path: notMethods + simple: true + widget: SimpleList + description: description.rules.to.notMethods + children: + - path: '[]' + simple: true + - path: paths + simple: true + widget: SimpleList + description: description.rules.to.paths + children: + - path: '[]' + simple: true + - path: notPaths + simple: true + widget: SimpleList + description: description.rules.to.notPaths + children: + - path: '[]' + simple: true + - path: '[].when' + simple: true + widget: GenericList + children: + - path: '[].key' + simple: true + widget: Text + description: description.rules.when.key + - path: '[].values' + simple: true + widget: SimpleList + description: description.rules.when.values + children: + - path: '[]' + simple: true + - path: '[].notValues' + simple: true + widget: SimpleList + description: description.rules.when.notValues + children: + - path: '[]' + simple: true + general: |- + resource: + kind: AuthorizationPolicy + group: security.istio.io + version: v1beta1 + name: Authorization Policies + category: Istio + urlPath: authorizationpolicies + scope: namespace + description: >- + {{[Istio Authorization + Policy](https://istio.io/latest/docs/reference/config/security/authorization-policy/)}} + allows for workload access management in the mesh. + list: |- + - name: action + source: spec.action + translations: | + en: + description.action: Optional. The action to take if the request is matched with the rules. Default is ALLOW if not specified. + description.rules.from.principals: Optional. A list of peer identities derived from the peer certificate. The peer identity is in the format of ' /ns/ /sa/ ', for example, 'cluster.local/ns/default/sa/productpage'. If not set, any principal is allowed. + description.rules.from.notPrincipals: Optional. A list of negative match of peer identities. + description.rules.from.requestPrincipals: Optional. A list of request identities derived from the JWT. The request identity is in the format of '/', for example, 'example.com/sub-1'. If not set, any request principal is allowed. + description.rules.from.notRequestPrincipals: Optional. A list of negative match of request identities. + description.rules.from.namespaces: Optional. A list of namespaces derived from the peer certificate. If not set, any namespace is allowed. + description.rules.from.notNamespaces: Optional. A list of negative match of namespaces. + description.rules.from.ipBlocks: Optional. A list of IP blocks, populated from the source address of the IP packet. Single IP (e.g. '1.2.3.4') and CIDR (e.g. '1.2.3.0/24') are supported. If not set, any IP is allowed. + description.rules.from.notIpBlocks: Optional. A list of negative match of IP blocks. + description.rules.from.remoteIpBlocks: Optional. A list of IP blocks, populated from X-Forwarded-For header or proxy protocol. To make use of this field, you must configure the numTrustedProxies field of the gatewayTopology under the meshConfig when you install Istio or using an annotation on the ingress gateway. If not set, any IP is allowed. + description.rules.from.notRemoteIpBlocks: Optional. A list of negative match of remote IP blocks. + description.rules.to.hosts: Optional. A list of hosts as specified in the HTTP request. The match is case-insensitive. If not set, any host is allowed. Must be used only with HTTP. + description.rules.to.notHosts: Optional. A list of negative match of hosts as specified in the HTTP request. The match is case-insensitive. + description.rules.to.ports: Optional. A list of ports as specified in the connection. If not set, any port is allowed. + description.rules.to.notPorts: Optional. A list of negative match of ports as specified in the connection. + description.rules.to.methods: Optional. A list of methods as specified in the HTTP request. If not set, any method is allowed. Must be used only with HTTP. + description.rules.to.notMethods: Optional. A list of negative match of methods as specified in the HTTP request. + description.rules.to.paths: Optional. A list of paths as specified in the HTTP request. If not set, any path is allowed. Must be used only with HTTP. + description.rules.to.notPaths: Optional. A list of negative match of paths. + description.rules.when.key: The name of an Istio attribute. + description.rules.when.values: Optional. A list of allowed values for the attribute. At least one of values or notValues must be set. + description.rules.when.notValues: Optional. A list of negative match of values for the attribute. At least one of values or notValues must be set. + placeholders.dropdown: Type or choose an option. + spec.action: Action + spec.provider: Provider + spec.rules: Rules + spec.rules.from: From + spec.rules.from.source: Source + spec.rules.from.source.principals: Principals + spec.rules.from.source.notPrincipals: NotPrincipals + spec.rules.from.source.requestPrincipals: RequestPrincipals + spec.rules.from.source.notRequestPrincipals: NotRequestPrincipals + spec.rules.from.source.namespaces: Namespaces + spec.rules.from.source.notNamespaces: NotNamespaces + spec.rules.from.source.ipBlocks: IpBlocks + spec.rules.from.source.notIpBlocks: NotIpBlocks + spec.rules.from.source.remoteIpBlocks: RemoteIpBlocks + spec.rules.from.source.notRemoteIpBlocks: NotRemoteIpBlocks + spec.rules.to: To + spec.rules.to.operation: Operation + spec.rules.to.operation.hosts: Hosts + spec.rules.to.operation.notHosts: NotHosts + spec.rules.to.operation.ports: Ports + spec.rules.to.operation.notPorts: NotPorts + spec.rules.to.operation.methods: Methods + spec.rules.to.operation.notMethods: NotMethods + spec.rules.to.operation.paths: Paths + spec.rules.to.operation.notPaths: NotPaths + spec.rules.when: When + spec.rules.when.key: Key + spec.rules.when.values: Values + spec.rules.when.notValues: NotValues + spec.selector.matchLabels: Selector + selector.matchesAllPods: Matches all Pods in the Namespace +kind: ConfigMap +metadata: + labels: + app.kubernetes.io/component: istio-operator.kyma-project.io + app.kubernetes.io/name: istios.operator.kyma-project.io + busola.io/extension: resource + busola.io/extension-version: "0.5" + name: istio-authorizationpolicies-ui.operator.kyma-project.io + namespace: kyma-system +--- +apiVersion: v1 +data: + details: | + header: [] + body: + - name: References + widget: Panel + children: + - source: spec.host + name: Host + - source: spec.exportTo + widget: Labels + name: Export To + visibility: $exists($value) + - source: spec.workloadSelector.matchLabels + widget: Labels + name: Workload Selector Match Labels + visibility: $exists($value) + - source: spec.trafficPolicy + name: Traffic Policy + disablePadding: true + visibility: $exists($value) + widget: Panel + children: + - source: loadBalancer + name: Load Balancer + visibility: $exists($value) + widget: Panel + children: + - source: simple + name: Simple + visibility: $exists($value) + widget: Badge + - source: warmupDurationSecs + name: Warmup Duration Secs + visibility: $exists($value) + - source: consistentHash + name: Consistent Hash + visibility: $exists($value) + widget: Panel + children: + - source: httpHeaderName + name: HTTP Header Name + visibility: $exists($value) + - source: useSourceIp + name: Use Source IP + visibility: $exists($value) + widget: Badge + highlights: + positive: + - 'true' + negative: + - 'false' + - source: httpQueryParameterName + name: HTTP Query Parameter Name + visibility: $exists($value) + - source: minimumRingSize + name: Minimum Ring Size + visibility: $exists($value) + - source: httpCookie + name: HTTP Cookie + visibility: $exists($value) + widget: Panel + children: + - source: name + name: Name + - source: path + name: Path + - source: ttl + name: TTL + - source: localityLbSetting + name: Locality LB Settings + visibility: $exists($value) + widget: Panel + children: + - name: Enabled + source: enabled + widget: Badge + highlights: + positive: + - 'true' + negative: + - 'false' + - widget: Table + disablePadding: true + name: Distribute + visibility: $exists($value) + source: distribute + children: + - source: $item.from + name: From + - source: $item.to + name: To + widget: Labels + - widget: Table + disablePadding: true + name: Failover + visibility: $exists($value) + source: failover + children: + - source: $item.from + name: From + - source: $item.to + name: To + - name: Failover Priority + source: failoverPriority + widget: JoinedArray + visibility: $exists($value) + - source: connectionPool + name: Connection Pool + visibility: $exists($value) + widget: Panel + children: + - source: tcp + name: TCP + visibility: $exists($value) + widget: Panel + children: + - source: maxConnections + name: Max Connections + visibility: $exists($value) + - source: connectTimeout + name: Connect Timeout + visibility: $exists($value) + - source: tcpKeepalive + name: TCP Keep Alive + visibility: $exists($value) + widget: Panel + children: + - source: probes + name: Probes + - source: time + name: Time + - source: interval + name: Interval + - source: http + name: HTTP + visibility: $exists($value) + widget: Panel + children: + - source: http1MaxPendingRequests + name: HTTP1 Max Pending Requests + visibility: $exists($value) + - source: http2MaxRequests + name: HTTP2 Max Requests + visibility: $exists($value) + - source: maxRequestsPerConnection + name: Max Requests Per Connection + visibility: $exists($value) + - source: maxRetries + name: Max Retries + visibility: $exists($value) + - source: idleTimeout + name: Idle Timeout + visibility: $exists($value) + - source: h2UpgradePolicy + name: H2 Upgrade Policy + visibility: $exists($value) + widget: Badge + - source: useClientProtocol + name: Use Client Protocol + visibility: $exists($value) + widget: Badge + highlights: + positive: + - 'true' + negative: + - 'false' + - source: outlierDetection + name: outlierDetection + visibility: $exists($value) + widget: Panel + children: + - source: splitExternalLocalOriginErrors + name: Split External Local Origin Errors + visibility: $exists($value) + widget: Badge + highlights: + positive: + - 'true' + negative: + - 'false' + - source: consecutiveLocalOriginFailures + name: Consecutive Local Origin Failures + visibility: $exists($value) + - source: consecutiveGatewayErrors + name: Consecutive Gateway Errors + visibility: $exists($value) + type: number + - source: consecutive5xxErrors + name: Consecutive 5xx Errors + visibility: $exists($value) + - source: interval + name: Interval + visibility: $exists($value) + - source: baseEjectionTime + name: Base Ejection Time + visibility: $exists($value) + - source: maxEjectionPercent + name: Max Ejection Percent + visibility: $exists($value) + - source: minHealthPercent + name: Min Health Percent + visibility: $exists($value) + - source: tls + name: TLS + visibility: $exists($value) + widget: Panel + children: + - source: mode + name: Mode + visibility: $exists($value) + widget: Badge + - source: clientCertificate + name: Client Certificate + visibility: $exists($value) + - source: privateKey + name: Private Key + visibility: $exists($value) + type: number + - source: caCertificates + name: CA Certificates + visibility: $exists($value) + - source: credentialName + name: Credential Name + visibility: $exists($value) + - source: subjectAltNames + name: Subject Alt Names + visibility: $exists($value) + widget: Labels + - source: sni + name: SNI + visibility: $exists($value) + - source: insecureSkipVerify + name: Insecure Skip Verify + visibility: $exists($value) + widget: Badge + - source: portLevelSettings + name: portLevelSettings + widget: Table + disablePadding: true + children: + - source: $item.port.number + name: port + visibility: $exists($value) + collapsible: + - source: $item.loadBalancer + name: Load Balancer + visibility: $exists($value) + widget: Panel + children: + - source: simple + name: Simple + visibility: $exists($value) + widget: Badge + - source: warmupDurationSecs + name: Warmup Duration Secs + visibility: $exists($value) + - source: consistentHash + name: Consistent Hash + visibility: $exists($value) + widget: Panel + children: + - source: httpHeaderName + name: HTTP Header Name + visibility: $exists($value) + - source: useSourceIp + name: Use Source IP + visibility: $exists($value) + widget: Badge + highlights: + positive: + - 'true' + negative: + - 'false' + - source: httpQueryParameterName + name: HTTP Query Parameter Name + visibility: $exists($value) + - source: minimumRingSize + name: Minimum Ring Size + visibility: $exists($value) + - source: httpCookie + name: HTTP Cookie + visibility: $exists($value) + widget: Panel + children: + - source: name + name: Name + - source: path + name: Path + - source: ttl + name: TTL + - source: localityLbSetting + name: Locality LB Settings + visibility: $exists($value) + widget: Panel + children: + - name: Enabled + source: enabled + widget: Badge + highlights: + positive: + - 'true' + negative: + - 'false' + - widget: Table + disablePadding: true + name: Distribute + visibility: $exists($value) + source: distribute + children: + - source: $item.from + name: From + - source: $item.to + name: To + widget: Labels + - widget: Table + disablePadding: true + name: Failover + visibility: $exists($value) + source: failover + children: + - source: $item.from + name: From + - source: $item.to + name: To + - name: Failover Priority + source: failoverPriority + widget: JoinedArray + visibility: $exists($value) + - source: $item.connectionPool + name: Connection Pool + visibility: $exists($value) + widget: Panel + children: + - source: tcp + name: TCP + visibility: $exists($value) + widget: Panel + children: + - source: maxConnections + name: Max Connections + visibility: $exists($value) + - source: connectTimeout + name: Connect Timeout + visibility: $exists($value) + - source: tcpKeepalive + name: TCP Keep Alive + visibility: $exists($value) + widget: Panel + children: + - source: probes + name: Probes + - source: time + name: Time + - source: interval + name: Interval + - source: http + name: HTTP + visibility: $exists($value) + widget: Panel + children: + - source: http1MaxPendingRequests + name: HTTP1 Max Pending Requests + visibility: $exists($value) + - source: http2MaxRequests + name: HTTP2 Max Requests + visibility: $exists($value) + - source: maxRequestsPerConnection + name: Max Requests Per Connection + visibility: $exists($value) + - source: maxRetries + name: Max Retries + visibility: $exists($value) + - source: idleTimeout + name: Idle Timeout + visibility: $exists($value) + - source: h2UpgradePolicy + name: H2 Upgrade Policy + visibility: $exists($value) + widget: Badge + - source: useClientProtocol + name: Use Client Protocol + visibility: $exists($value) + widget: Badge + highlights: + positive: + - 'true' + negative: + - 'false' + - source: $item.outlierDetection + name: outlierDetection + visibility: $exists($value) + widget: Panel + children: + - source: splitExternalLocalOriginErrors + name: Split External Local Origin Errors + visibility: $exists($value) + widget: Badge + highlights: + positive: + - 'true' + negative: + - 'false' + - source: consecutiveLocalOriginFailures + name: Consecutive Local Origin Failures + visibility: $exists($value) + - source: consecutiveGatewayErrors + name: Consecutive Gateway Errors + visibility: $exists($value) + type: number + - source: consecutive5xxErrors + name: Consecutive 5xx Errors + visibility: $exists($value) + - source: interval + name: Interval + visibility: $exists($value) + - source: baseEjectionTime + name: Base Ejection Time + visibility: $exists($value) + - source: maxEjectionPercent + name: Max Ejection Percent + visibility: $exists($value) + - source: minHealthPercent + name: Min Health Percent + visibility: $exists($value) + - source: $item.tls + name: TLS + visibility: $exists($value) + widget: Panel + children: + - source: mode + name: Mode + visibility: $exists($value) + widget: Badge + - source: clientCertificate + name: Client Certificate + visibility: $exists($value) + - source: privateKey + name: Private Key + visibility: $exists($value) + type: number + - source: caCertificates + name: CA Certificates + visibility: $exists($value) + - source: credentialName + name: Credential Name + visibility: $exists($value) + - source: subjectAltNames + name: Subject Alt Names + visibility: $exists($value) + widget: Labels + - source: sni + name: SNI + visibility: $exists($value) + - source: insecureSkipVerify + name: Insecure Skip Verify + visibility: $exists($value) + widget: Badge + - source: $item.tunnel + name: Tunnel + visibility: $exists($value) + widget: Panel + children: + - source: protocol + name: Protocol + visibility: $exists($value) + widget: Badge + - source: targetHost + name: Target Host + visibility: $exists($value) + - source: targetPort + name: Target Port + visibility: $exists($value) + - source: tunnel + name: Tunnel + visibility: $exists($value) + widget: Panel + children: + - source: protocol + name: Protocol + visibility: $exists($value) + widget: Badge + - source: targetHost + name: Target Host + visibility: $exists($value) + - source: targetPort + name: Target Port + visibility: $exists($value) + - source: spec.subsets + name: Subsets + widget: Table + disablePadding: true + visibility: $exists($value) + children: + - source: $item.name + name: Name + - source: $item.labels + name: Labels + widget: Labels + collapsible: + - source: $item.trafficPolicy + name: Traffic Policy + disablePadding: true + visibility: $exists($value) + widget: Panel + children: + - source: loadBalancer + name: Load Balancer + visibility: $exists($value) + widget: Panel + children: + - source: simple + name: Simple + visibility: $exists($value) + widget: Badge + - source: warmupDurationSecs + name: Warmup Duration Secs + visibility: $exists($value) + - source: consistentHash + name: Consistent Hash + visibility: $exists($value) + widget: Panel + children: + - source: httpHeaderName + name: HTTP Header Name + visibility: $exists($value) + - source: useSourceIp + name: Use Source IP + visibility: $exists($value) + widget: Badge + highlights: + positive: + - 'true' + negative: + - 'false' + - source: httpQueryParameterName + name: HTTP Query Parameter Name + visibility: $exists($value) + - source: minimumRingSize + name: Minimum Ring Size + visibility: $exists($value) + - source: httpCookie + name: HTTP Cookie + visibility: $exists($value) + widget: Panel + children: + - source: name + name: Name + - source: path + name: Path + - source: ttl + name: TTL + - source: localityLbSetting + name: Locality LB Settings + visibility: $exists($value) + widget: Panel + children: + - name: Enabled + source: enabled + widget: Badge + highlights: + positive: + - 'true' + negative: + - 'false' + - widget: Table + disablePadding: true + name: Distribute + visibility: $exists($value) + source: distribute + children: + - source: $item.from + name: From + - source: $item.to + name: To + widget: Labels + - widget: Table + disablePadding: true + name: Failover + visibility: $exists($value) + source: failover + children: + - source: $item.from + name: From + - source: $item.to + name: To + - name: Failover Priority + source: failoverPriority + widget: JoinedArray + visibility: $exists($value) + - source: connectionPool + name: Connection Pool + visibility: $exists($value) + widget: Panel + children: + - source: tcp + name: TCP + visibility: $exists($value) + widget: Panel + children: + - source: maxConnections + name: Max Connections + visibility: $exists($value) + - source: connectTimeout + name: Connect Timeout + visibility: $exists($value) + - source: tcpKeepalive + name: TCP Keep Alive + visibility: $exists($value) + widget: Panel + children: + - source: probes + name: Probes + - source: time + name: Time + - source: interval + name: Interval + - source: http + name: HTTP + visibility: $exists($value) + widget: Panel + children: + - source: http1MaxPendingRequests + name: HTTP1 Max Pending Requests + visibility: $exists($value) + - source: http2MaxRequests + name: HTTP2 Max Requests + visibility: $exists($value) + - source: maxRequestsPerConnection + name: Max Requests Per Connection + visibility: $exists($value) + - source: maxRetries + name: Max Retries + visibility: $exists($value) + - source: idleTimeout + name: Idle Timeout + visibility: $exists($value) + - source: h2UpgradePolicy + name: H2 Upgrade Policy + visibility: $exists($value) + widget: Badge + - source: useClientProtocol + name: Use Client Protocol + visibility: $exists($value) + widget: Badge + highlights: + positive: + - 'true' + negative: + - 'false' + - source: outlierDetection + name: outlierDetection + visibility: $exists($value) + widget: Panel + children: + - source: splitExternalLocalOriginErrors + name: Split External Local Origin Errors + visibility: $exists($value) + widget: Badge + highlights: + positive: + - 'true' + negative: + - 'false' + - source: consecutiveLocalOriginFailures + name: Consecutive Local Origin Failures + visibility: $exists($value) + - source: consecutiveGatewayErrors + name: Consecutive Gateway Errors + visibility: $exists($value) + type: number + - source: consecutive5xxErrors + name: Consecutive 5xx Errors + visibility: $exists($value) + - source: interval + name: Interval + visibility: $exists($value) + - source: baseEjectionTime + name: Base Ejection Time + visibility: $exists($value) + - source: maxEjectionPercent + name: Max Ejection Percent + visibility: $exists($value) + - source: minHealthPercent + name: Min Health Percent + visibility: $exists($value) + - source: tls + name: TLS + visibility: $exists($value) + widget: Panel + children: + - source: mode + name: Mode + visibility: $exists($value) + widget: Badge + - source: clientCertificate + name: Client Certificate + visibility: $exists($value) + - source: privateKey + name: Private Key + visibility: $exists($value) + type: number + - source: caCertificates + name: CA Certificates + visibility: $exists($value) + - source: credentialName + name: Credential Name + visibility: $exists($value) + - source: subjectAltNames + name: Subject Alt Names + visibility: $exists($value) + widget: Labels + - source: sni + name: SNI + visibility: $exists($value) + - source: insecureSkipVerify + name: Insecure Skip Verify + visibility: $exists($value) + widget: Badge + - source: portLevelSettings + name: portLevelSettings + widget: Table + disablePadding: true + children: + - source: $item.port.number + name: port + visibility: $exists($value) + collapsible: + - source: $item.loadBalancer + name: Load Balancer + visibility: $exists($value) + widget: Panel + children: + - source: simple + name: Simple + visibility: $exists($value) + widget: Badge + - source: warmupDurationSecs + name: Warmup Duration Secs + visibility: $exists($value) + - source: consistentHash + name: Consistent Hash + visibility: $exists($value) + widget: Panel + children: + - source: httpHeaderName + name: HTTP Header Name + visibility: $exists($value) + - source: useSourceIp + name: Use Source IP + visibility: $exists($value) + widget: Badge + highlights: + positive: + - 'true' + negative: + - 'false' + - source: httpQueryParameterName + name: HTTP Query Parameter Name + visibility: $exists($value) + - source: minimumRingSize + name: Minimum Ring Size + visibility: $exists($value) + - source: httpCookie + name: HTTP Cookie + visibility: $exists($value) + widget: Panel + children: + - source: name + name: Name + - source: path + name: Path + - source: ttl + name: TTL + - source: localityLbSetting + name: Locality LB Settings + visibility: $exists($value) + widget: Panel + children: + - name: Enabled + source: enabled + widget: Badge + highlights: + positive: + - 'true' + negative: + - 'false' + - widget: Table + disablePadding: true + name: Distribute + visibility: $exists($value) + source: distribute + children: + - source: $item.from + name: From + - source: $item.to + name: To + widget: Labels + - widget: Table + disablePadding: true + name: Failover + visibility: $exists($value) + source: failover + children: + - source: $item.from + name: From + - source: $item.to + name: To + - name: Failover Priority + source: failoverPriority + widget: JoinedArray + visibility: $exists($value) + - source: $item.connectionPool + name: Connection Pool + visibility: $exists($value) + widget: Panel + children: + - source: tcp + name: TCP + visibility: $exists($value) + widget: Panel + children: + - source: maxConnections + name: Max Connections + visibility: $exists($value) + - source: connectTimeout + name: Connect Timeout + visibility: $exists($value) + - source: tcpKeepalive + name: TCP Keep Alive + visibility: $exists($value) + widget: Panel + children: + - source: probes + name: Probes + - source: time + name: Time + - source: interval + name: Interval + - source: http + name: HTTP + visibility: $exists($value) + widget: Panel + children: + - source: http1MaxPendingRequests + name: HTTP1 Max Pending Requests + visibility: $exists($value) + - source: http2MaxRequests + name: HTTP2 Max Requests + visibility: $exists($value) + - source: maxRequestsPerConnection + name: Max Requests Per Connection + visibility: $exists($value) + - source: maxRetries + name: Max Retries + visibility: $exists($value) + - source: idleTimeout + name: Idle Timeout + visibility: $exists($value) + - source: h2UpgradePolicy + name: H2 Upgrade Policy + visibility: $exists($value) + widget: Badge + - source: useClientProtocol + name: Use Client Protocol + visibility: $exists($value) + widget: Badge + highlights: + positive: + - 'true' + negative: + - 'false' + - source: $item.outlierDetection + name: outlierDetection + visibility: $exists($value) + widget: Panel + children: + - source: splitExternalLocalOriginErrors + name: Split External Local Origin Errors + visibility: $exists($value) + widget: Badge + highlights: + positive: + - 'true' + negative: + - 'false' + - source: consecutiveLocalOriginFailures + name: Consecutive Local Origin Failures + visibility: $exists($value) + - source: consecutiveGatewayErrors + name: Consecutive Gateway Errors + visibility: $exists($value) + type: number + - source: consecutive5xxErrors + name: Consecutive 5xx Errors + visibility: $exists($value) + - source: interval + name: Interval + visibility: $exists($value) + - source: baseEjectionTime + name: Base Ejection Time + visibility: $exists($value) + - source: maxEjectionPercent + name: Max Ejection Percent + visibility: $exists($value) + - source: minHealthPercent + name: Min Health Percent + visibility: $exists($value) + - source: $item.tls + name: TLS + visibility: $exists($value) + widget: Panel + children: + - source: mode + name: Mode + visibility: $exists($value) + widget: Badge + - source: clientCertificate + name: Client Certificate + visibility: $exists($value) + - source: privateKey + name: Private Key + visibility: $exists($value) + type: number + - source: caCertificates + name: CA Certificates + visibility: $exists($value) + - source: credentialName + name: Credential Name + visibility: $exists($value) + - source: subjectAltNames + name: Subject Alt Names + visibility: $exists($value) + widget: Labels + - source: sni + name: SNI + visibility: $exists($value) + - source: insecureSkipVerify + name: Insecure Skip Verify + visibility: $exists($value) + widget: Badge + - source: $item.tunnel + name: Tunnel + visibility: $exists($value) + widget: Panel + children: + - source: protocol + name: Protocol + visibility: $exists($value) + widget: Badge + - source: targetHost + name: Target Host + visibility: $exists($value) + - source: targetPort + name: Target Port + visibility: $exists($value) + - source: tunnel + name: Tunnel + visibility: $exists($value) + widget: Panel + children: + - source: protocol + name: Protocol + visibility: $exists($value) + widget: Badge + - source: targetHost + name: Target Host + visibility: $exists($value) + - source: targetPort + name: Target Port + visibility: $exists($value) + form: | + - simple: true + path: spec.host + name: Host + required: true + - widget: FormGroup + path: spec.trafficPolicy + name: Traffic Policy + children: + - widget: FormGroup + path: loadBalancer + name: Load Balancer + children: + - var: mainloadBalancerSelector + name: ChooseLoadBalancerSelector + type: string + enum: + - simple + - consistentHash + - path: simple + name: Simple + required: true + visibility: $mainloadBalancerSelector = 'simple' + - widget: FormGroup + path: consistentHash + name: Consistent Hash + visibility: $mainloadBalancerSelector = 'consistentHash' + children: + - var: mainconsistentHashSelector + name: ChooseConsistentHashSelector + type: string + enum: + - httpHeaderName + - httpCookie + - useSourceIp + - httpQueryParameterName + - path: httpHeaderName + name: HTTP Header Name + required: true + visibility: $mainconsistentHashSelector = 'httpHeaderName' + - path: httpCookie + name: HTTP Cookie + widget: FormGroup + visibility: $mainconsistentHashSelector = 'httpCookie' + children: + - path: name + name: Name + required: true + - path: path + name: Path + - path: ttl + name: TTL + required: true + - path: useSourceIp + name: Use Source IP + required: true + visibility: $mainconsistentHashSelector = 'useSourceIp' + - path: httpQueryParameterName + name: HTTP Query Parameter Name + required: true + visibility: $mainconsistentHashSelector= 'httpQueryParameterName' + - path: minimumRingSize + name: Minimum Ring Size + - path: localityLbSetting + name: Locality LB Settings + widget: FormGroup + children: + - path: enabled + name: Enabled + type: boolean + - var: mainLbSelector + name: ChooseLbSelector + type: string + enum: + - distribute + - failover + - path: distribute + name: Distribute + widget: GenericList + visibility: $mainLbSelector = 'distribute' + - path: distribute[].from + name: From + - path: distribute[].to + name: To + widget: KeyValuePair + value: + type: number + - path: failover + name: Failover + widget: GenericList + visibility: $mainLbSelector = 'failover' + - path: failover[].from + name: From + - path: failover[].to + name: To + - path: failoverPriority + name: Failover Priority + visibility: $mainLbSelector = 'failover' + widget: SimpleList + children: + - path: '[]' + - path: warmupDurationSecs + name: Warmup Duration Secs + - path: connectionPool + name: Connection Pool + widget: FormGroup + children: + - path: tcp + name: TCP + widget: FormGroup + children: + - path: maxConnections + name: Max Connections + - path: connectTimeout + name: Connect Timeout + - path: tcpKeepalive + name: TCP Keep Alive + widget: FormGroup + children: + - path: probes + name: Probes + - path: time + name: Time + - path: interval + name: Interval + - path: http + name: HTTP + widget: FormGroup + children: + - path: http1MaxPendingRequests + name: HTTP1 Max Pending Requests + - path: http2MaxRequests + name: HTTP2 Max Requests + - path: maxRequestsPerConnection + name: Max Requests Per Connection + - path: maxRetries + name: Max Retries + - path: idleTimeout + name: Idle Timeout + - path: h2UpgradePolicy + name: H2 Upgrade Policy + - path: useClientProtocol + name: Use Client Protocol + - path: outlierDetection + widget: FormGroup + children: + - path: splitExternalLocalOriginErrors + name: Split External Local Origin Errors + - path: consecutiveLocalOriginFailures + name: Consecutive Local Origin Failures + type: number + - path: consecutiveGatewayErrors + name: Consecutive Gateway Errors + type: number + - path: consecutive5xxErrors + name: Consecutive 5xx Errors + type: number + - path: interval + name: Interval + - path: baseEjectionTime + name: Base Ejection Time + - path: maxEjectionPercent + name: Max Ejection Percent + - path: minHealthPercent + name: Min Health Percent + - path: tls + name: TLS + widget: FormGroup + children: + - path: mode + name: Mode + - path: clientCertificate + name: Client Certificate + - path: privateKey + name: Private Key + - path: caCertificates + name: CA Certificates + - path: credentialName + name: Credential Name + - path: subjectAltNames + name: Subject Alt Names + widget: SimpleList + children: + - path: '[]' + - path: sni + name: SNI + - path: insecureSkipVerify + name: Insecure Skip Verify + - path: portLevelSettings + name: Port Level Settings + widget: GenericList + children: + - path: '[].port.number' + name: Port Number + - widget: FormGroup + path: '[].loadBalancer' + name: Load Balancer + children: + - var: portLevelloadBalancerSelector + name: ChooseLoadBalancerSelector + type: string + enum: + - simple + - consistentHash + - path: simple + name: Simple + required: true + visibility: $portLevelloadBalancerSelector = 'simple' + - widget: FormGroup + path: consistentHash + name: Consistent Hash + visibility: $portLevelloadBalancerSelector = 'consistentHash' + children: + - var: portLevelconsistentHashSelector + name: ChooseConsistentHashSelector + type: string + enum: + - httpHeaderName + - httpCookie + - useSourceIp + - httpQueryParameterName + - path: httpHeaderName + name: HTTP Header Name + required: true + visibility: $portLevelconsistentHashSelector = 'httpHeaderName' + - path: httpCookie + name: HTTP Cookie + widget: FormGroup + visibility: $portLevelconsistentHashSelector = 'httpCookie' + children: + - path: name + name: Name + required: true + - path: path + name: Path + - path: ttl + name: TTL + required: true + - path: useSourceIp + name: Use Source IP + required: true + visibility: $portLevelconsistentHashSelector = 'useSourceIp' + - path: httpQueryParameterName + name: HTTP Query Parameter Name + required: true + visibility: $portLevelconsistentHashSelector= 'httpQueryParameterName' + - path: minimumRingSize + name: Minimum Ring Size + - path: localityLbSetting + name: Locality LB Settings + widget: FormGroup + children: + - path: enabled + name: Enabled + type: boolean + - var: portLevelLbSelector + name: ChooseLbSelector + type: string + enum: + - distribute + - failover + - path: distribute + name: Distribute + widget: GenericList + visibility: $portLevelLbSelector = 'distribute' + - path: distribute[].from + name: From + - path: distribute[].to + name: To + widget: KeyValuePair + value: + type: number + - path: failover + name: Failover + widget: GenericList + visibility: $portLevelLbSelector = 'failover' + - path: failover[].from + name: From + - path: failover[].to + name: To + - path: failoverPriority + name: Failover Priority + visibility: $portLevelLbSelector = 'failover' + widget: SimpleList + children: + - path: '[]' + - path: warmupDurationSecs + name: Warmup Duration Secs + - path: '[].connectionPool' + name: Connection Pool + widget: FormGroup + children: + - path: tcp + name: TCP + widget: FormGroup + children: + - path: maxConnections + name: Max Connections + - path: connectTimeout + name: Connect Timeout + - path: tcpKeepalive + name: TCP Keep Alive + widget: FormGroup + children: + - path: probes + name: Probes + - path: time + name: Time + - path: interval + name: Interval + - path: http + name: HTTP + widget: FormGroup + children: + - path: http1MaxPendingRequests + name: HTTP1 Max Pending Requests + - path: http2MaxRequests + name: HTTP2 Max Requests + - path: maxRequestsPerConnection + name: Max Requests Per Connection + - path: maxRetries + name: Max Retries + - path: idleTimeout + name: Idle Timeout + - path: h2UpgradePolicy + name: H2 Upgrade Policy + - path: useClientProtocol + name: Use Client Protocol + - path: '[].outlierDetection' + widget: FormGroup + children: + - path: splitExternalLocalOriginErrors + name: Split External Local Origin Errors + - path: consecutiveLocalOriginFailures + name: Consecutive Local Origin Failures + type: number + - path: consecutiveGatewayErrors + name: Consecutive Gateway Errors + type: number + - path: consecutive5xxErrors + name: Consecutive 5xx Errors + type: number + - path: interval + name: Interval + - path: baseEjectionTime + name: Base Ejection Time + - path: maxEjectionPercent + name: Max Ejection Percent + - path: minHealthPercent + name: Min Health Percent + - path: '[].tls' + name: TLS + widget: FormGroup + children: + - path: mode + name: Mode + - path: clientCertificate + name: Client Certificate + - path: privateKey + name: Private Key + - path: caCertificates + name: CA Certificates + - path: credentialName + name: Credential Name + - path: subjectAltNames + name: Subject Alt Names + widget: SimpleList + children: + - path: '[]' + - path: sni + name: SNI + - path: insecureSkipVerify + name: Insecure Skip Verify + - path: tunnel + name: Tunnel + widget: FormGroup + children: + - path: protocol + name: Protocol + - path: targetHost + name: Target Host + - path: targetPort + name: Target Port + - path: spec.subsets + name: Subsets + widget: GenericList + children: + - path: '[].name' + name: Name + - path: '[].labels' + name: Labels + widget: KeyValuePair + - path: '[].trafficPolicy' + name: Traffic Policy + children: + - widget: FormGroup + path: loadBalancer + name: Load Balancer + children: + - var: subsetsloadBalancerSelector + name: ChooseLoadBalancerSelector + type: string + enum: + - simple + - consistentHash + - path: simple + name: Simple + required: true + visibility: $subsetsloadBalancerSelector = 'simple' + - widget: FormGroup + path: consistentHash + name: Consistent Hash + visibility: $subsetsloadBalancerSelector = 'consistentHash' + children: + - var: subsetsconsistentHashSelector + name: ChooseConsistentHashSelector + type: string + enum: + - httpHeaderName + - httpCookie + - useSourceIp + - httpQueryParameterName + - path: httpHeaderName + name: HTTP Header Name + required: true + visibility: $subsetsconsistentHashSelector = 'httpHeaderName' + - path: httpCookie + name: HTTP Cookie + widget: FormGroup + visibility: $subsetsconsistentHashSelector = 'httpCookie' + children: + - path: name + name: Name + required: true + - path: path + name: Path + - path: ttl + name: TTL + required: true + - path: useSourceIp + name: Use Source IP + required: true + visibility: $subsetsconsistentHashSelector = 'useSourceIp' + - path: httpQueryParameterName + name: HTTP Query Parameter Name + required: true + visibility: $subsetsconsistentHashSelector= 'httpQueryParameterName' + - path: minimumRingSize + name: Minimum Ring Size + - path: localityLbSetting + name: Locality LB Settings + widget: FormGroup + children: + - path: enabled + name: Enabled + type: boolean + - var: subsetsLbSelector + name: ChooseLbSelector + type: string + enum: + - distribute + - failover + - path: distribute + name: Distribute + widget: GenericList + visibility: $subsetsLbSelector = 'distribute' + - path: distribute[].from + name: From + - path: distribute[].to + name: To + widget: KeyValuePair + value: + type: number + - path: failover + name: Failover + widget: GenericList + visibility: $subsetsLbSelector = 'failover' + - path: failover[].from + name: From + - path: failover[].to + name: To + - path: failoverPriority + name: Failover Priority + visibility: $subsetsLbSelector = 'failover' + widget: SimpleList + children: + - path: '[]' + - path: warmupDurationSecs + name: Warmup Duration Secs + - path: connectionPool + name: Connection Pool + widget: FormGroup + children: + - path: tcp + name: TCP + widget: FormGroup + children: + - path: maxConnections + name: Max Connections + - path: connectTimeout + name: Connect Timeout + - path: tcpKeepalive + name: TCP Keep Alive + widget: FormGroup + children: + - path: probes + name: Probes + - path: time + name: Time + - path: interval + name: Interval + - path: http + name: HTTP + widget: FormGroup + children: + - path: http1MaxPendingRequests + name: HTTP1 Max Pending Requests + - path: http2MaxRequests + name: HTTP2 Max Requests + - path: maxRequestsPerConnection + name: Max Requests Per Connection + - path: maxRetries + name: Max Retries + - path: idleTimeout + name: Idle Timeout + - path: h2UpgradePolicy + name: H2 Upgrade Policy + - path: useClientProtocol + name: Use Client Protocol + - path: outlierDetection + widget: FormGroup + children: + - path: splitExternalLocalOriginErrors + name: Split External Local Origin Errors + - path: consecutiveLocalOriginFailures + name: Consecutive Local Origin Failures + type: number + - path: consecutiveGatewayErrors + name: Consecutive Gateway Errors + type: number + - path: consecutive5xxErrors + name: Consecutive 5xx Errors + type: number + - path: interval + name: Interval + - path: baseEjectionTime + name: Base Ejection Time + - path: maxEjectionPercent + name: Max Ejection Percent + - path: minHealthPercent + name: Min Health Percent + - path: tls + name: TLS + widget: FormGroup + children: + - path: mode + name: Mode + - path: clientCertificate + name: Client Certificate + - path: privateKey + name: Private Key + - path: caCertificates + name: CA Certificates + - path: credentialName + name: Credential Name + - path: subjectAltNames + name: Subject Alt Names + widget: SimpleList + children: + - path: '[]' + - path: sni + name: SNI + - path: insecureSkipVerify + name: Insecure Skip Verify + - path: portLevelSettings + name: Port Level Settings + widget: GenericList + children: + - path: '[].port.number' + name: Port Number + - widget: FormGroup + path: '[].loadBalancer' + name: Load Balancer + children: + - var: subsetPortLevelloadBalancerSelector + name: ChooseLoadBalancerSelector + type: string + enum: + - simple + - consistentHash + - path: simple + name: Simple + required: true + visibility: $subsetPortLevelloadBalancerSelector = 'simple' + - widget: FormGroup + path: consistentHash + name: Consistent Hash + visibility: $subsetPortLevelloadBalancerSelector = 'consistentHash' + children: + - var: subsetPortLevelconsistentHashSelector + name: ChooseConsistentHashSelector + type: string + enum: + - httpHeaderName + - httpCookie + - useSourceIp + - httpQueryParameterName + - path: httpHeaderName + name: HTTP Header Name + required: true + visibility: >- + $subsetPortLevelconsistentHashSelector = + 'httpHeaderName' + - path: httpCookie + name: HTTP Cookie + widget: FormGroup + visibility: $subsetPortLevelconsistentHashSelector = 'httpCookie' + children: + - path: name + name: Name + required: true + - path: path + name: Path + - path: ttl + name: TTL + required: true + - path: useSourceIp + name: Use Source IP + required: true + visibility: $subsetPortLevelconsistentHashSelector = 'useSourceIp' + - path: httpQueryParameterName + name: HTTP Query Parameter Name + required: true + visibility: >- + $subsetPortLevelconsistentHashSelector= + 'httpQueryParameterName' + - path: minimumRingSize + name: Minimum Ring Size + - path: localityLbSetting + name: Locality LB Settings + widget: FormGroup + children: + - path: enabled + name: Enabled + type: boolean + - var: subsetPortLevelLbSelector + name: ChooseLbSelector + type: string + enum: + - distribute + - failover + - path: distribute + name: Distribute + widget: GenericList + visibility: $subsetPortLevelLbSelector = 'distribute' + - path: distribute[].from + name: From + - path: distribute[].to + name: To + widget: KeyValuePair + value: + type: number + - path: failover + name: Failover + widget: GenericList + visibility: $subsetPortLevelLbSelector = 'failover' + - path: failover[].from + name: From + - path: failover[].to + name: To + - path: failoverPriority + name: Failover Priority + visibility: $subsetPortLevelLbSelector = 'failover' + widget: SimpleList + children: + - path: '[]' + - path: warmupDurationSecs + name: Warmup Duration Secs + - path: '[].connectionPool' + name: Connection Pool + widget: FormGroup + children: + - path: tcp + name: TCP + widget: FormGroup + children: + - path: maxConnections + name: Max Connections + - path: connectTimeout + name: Connect Timeout + - path: tcpKeepalive + name: TCP Keep Alive + widget: FormGroup + children: + - path: probes + name: Probes + - path: time + name: Time + - path: interval + name: Interval + - path: http + name: HTTP + widget: FormGroup + children: + - path: http1MaxPendingRequests + name: HTTP1 Max Pending Requests + - path: http2MaxRequests + name: HTTP2 Max Requests + - path: maxRequestsPerConnection + name: Max Requests Per Connection + - path: maxRetries + name: Max Retries + - path: idleTimeout + name: Idle Timeout + - path: h2UpgradePolicy + name: H2 Upgrade Policy + - path: useClientProtocol + name: Use Client Protocol + - path: '[].outlierDetection' + widget: FormGroup + children: + - path: splitExternalLocalOriginErrors + name: Split External Local Origin Errors + - path: consecutiveLocalOriginFailures + name: Consecutive Local Origin Failures + type: number + - path: consecutiveGatewayErrors + name: Consecutive Gateway Errors + type: number + - path: consecutive5xxErrors + name: Consecutive 5xx Errors + type: number + - path: interval + name: Interval + - path: baseEjectionTime + name: Base Ejection Time + - path: maxEjectionPercent + name: Max Ejection Percent + - path: minHealthPercent + name: Min Health Percent + - path: '[].tls' + name: TLS + widget: FormGroup + children: + - path: mode + name: Mode + - path: clientCertificate + name: Client Certificate + - path: privateKey + name: Private Key + - path: caCertificates + name: CA Certificates + - path: credentialName + name: Credential Name + - path: subjectAltNames + name: Subject Alt Names + widget: SimpleList + children: + - path: '[]' + - path: sni + name: SNI + - path: insecureSkipVerify + name: Insecure Skip Verify + - path: tunnel + name: Tunnel + widget: FormGroup + children: + - path: protocol + name: Protocol + - path: targetHost + name: Target Host + - path: targetPort + name: Target Port + - path: spec.exportTo + name: Export To + widget: SimpleList + children: + - path: '[]' + - path: spec.workloadSelector.matchLabels + defaultExpanded: true + name: Workload Selector Match Labels + widget: KeyValuePair + general: |- + resource: + kind: DestinationRule + group: networking.istio.io + version: v1beta1 + name: Destination Rules + category: Istio + urlPath: destinationrules + scope: namespace + description: resource.description + list: |- + - source: spec.host + name: Host + translations: |+ + en: + metadata.annotations: Annotations + metadata.labels: Labels + metadata.creationTimestamp: Created at + resource.description: >- + {{[Destination + Rule](https://istio.io/latest/docs/reference/config/networking/destination-rule)}} + specifies rules that apply to traffic intended for a service after routing. + References: References + probes: Probes + Export To: Export To + Workload Selector Match Labels: Workload Selector Match Labels + Traffic Policy: Traffic Policy + Interval: Interval + Name: Name + time: Time + interval: Interval + Host: Host + Connection Pool: Connection Pool + TCP Keep Alive: TCP Keep Alive + Probes: Probes + Time: Time + TCP: TCP + HTTP: HTTP + HTTP1 Max Pending Requests: HTTP1 Max Pending Requests + Max Connections: Max Connections + Connect Timeout: Connect Timeout + HTTP2 Max Requests: HTTP2 Max Requests + Max Requests Per Connection: Max Requests Per Connection + Max Retries: Max Retries + Idle Timeout: Idle Timeout + H2 Upgrade Policy: H2 Upgrade Policy + Use Client Protocol: Use Client Protocol + Locality LB Settings: Locality LB Settings + Enabled: Enabled + Distribute: Distribute + From: From + To: To + Failover: Failover + Failover Priority: Failover Priority + HTTP Cookie: HTTP Cookie + Path: Path + TTL: TTL + Consistent Hash: Consistent Hash + HTTP Header Name: HTTP Header Name + Use Source IP: Use Source IP + HTTP Query Parameter Name: HTTP Query Parameter Name + Minimum Ring Size: Minimum Ring Size + Load Balancer: Load Balancer + Simple: Simple + Warmup Duration Secs: Warmup Duration Secs + ChooseConsistentHashSelector: Select Hash Type + ChooseLoadBalancerSelector: Select Balancer Type + ChooseLbSelector: Select LB Settings + Split External Local Origin Errors: Split External Local Origin Errors + Consecutive Local Origin Failures: Consecutive Local Origin Failures + Consecutive Gateway Errors: Consecutive Gateway Errors + Consecutive 5xx Errors: Consecutive 5xx Errors + Base Ejection Time: Base Ejection Time + Max Ejection Percent: Max Ejection Percent + Min Health Percent: Min Health Percent + Port Level Settings: Port Level Settings + Port Number: Port Number + TLS: TLS + Mode: Mode + Client Certificate: Client Certificate + Private Key: Private Key + CA Certificates: CA Certificates + Credential Name: Credential Name + Subject Alt Names: Subject Alt Names + SNI: SNI + Insecure Skip Verify: Insecure Skip Verify + Tunnel: Tunnel + Protocol: Protocol + Target Host: Target Host + Target Port: Target Port + Subsets: Subsets + Labels: Labels + consistentHash: Consistent Hash + simple: Simple + failover: Failover + distribute: Distribute + +kind: ConfigMap +metadata: + labels: + app.kubernetes.io/component: istio-operator.kyma-project.io + app.kubernetes.io/name: istios.operator.kyma-project.io + busola.io/extension: resource + busola.io/extension-version: "0.5" + name: istio-destinationrules-ui.operator.kyma-project.io + namespace: kyma-system +--- +apiVersion: v1 +data: + dataSources: |- + podSelector: + resource: + kind: Pod + version: v1 + namespace: null + filter: $matchByLabelSelector($item, $root.spec.selector) + relatedVirtualServices: + resource: + kind: VirtualService + group: networking.istio.io + version: v1beta1 + namespace: null + filter: >- + $filter($item.spec.gateways, function($g){$contains($g,'/') ? + ($substringBefore($g,'/') = $root.metadata.namespace and $substringAfter($g, + '/') = $root.metadata.name) : ($substringBefore($g, '.') = + $root.metadata.name and $substringBefore($substringAfter($g, '.'), '.') = + $root.metadata.namespace) }) + details: |- + header: + - source: spec.selector + widget: Labels + name: spec.selector + body: + - widget: Table + source: spec.servers + name: spec.servers + children: + - source: port.name + name: spec.servers.port.name + - widget: JoinedArray + separator: break + source: hosts + name: spec.servers.hosts + - source: port.number + name: spec.servers.port.number + - source: port.protocol + name: spec.servers.port.protocol + - source: tls.mode + name: spec.servers.tls.mode + - widget: ResourceLink + source: tls.credentialName + name: spec.servers.tls.credentialName + resource: + name: tls.credentialName + namespace: '"istio-system"' + kind: '"Secret"' + - widget: Panel + name: spec.selector + disablePadding: true + children: + - source: $podSelector() + widget: ResourceList + disableCreate: true + visibility: $exists($root.spec.selector) and $boolean($root.spec.selector) + - source: spec.selector + widget: Panel + name: selector.matchesAllPods + visibility: $not($exists($value)) or $not($boolean($value)) + header: + - source: spec.selector + widget: Labels + name: spec.selector + visibility: $exists($value) and $boolean($value) + resourceGraph: + depth: 1 + colorVariant: 1 + dataSources: + - source: relatedVirtualServices + form: |- + - path: spec.selector + widget: KeyValuePair + simple: true + required: true + defaultExpanded: true + - path: spec.servers + widget: GenericList + simple: true + required: true + children: + - widget: FormGroup + simple: true + path: '[].port' + defaultExpanded: true + children: + - path: number + simple: true + required: true + inputInfo: inputInfo.spec.servers.port.number + - path: name + widget: Name + inputInfo: null + simple: true + required: true + - path: protocol + simple: true + enum: + - HTTP + - HTTPS + - HTTP2 + - GRPC + - GRPC-WEB + - MONGO + - REDIS + - MYSQL + - TCP + required: true + placeholder: placeholders.dropdown + - widget: FormGroup + simple: true + path: '[].tls' + visibility: $item.port.protocol = 'HTTP' or $item.port.protocol = 'HTTPS' + children: + - path: httpsRedirect + simple: true + visibility: $item.port.protocol = 'HTTP' + - path: mode + simple: true + visibility: $item.port.protocol = 'HTTPS' + required: true + placeholder: placeholders.dropdown + - path: credentialName + simple: true + widget: Resource + resource: + kind: Secret + version: v1 + namespace: istio-system + scope: namespace + filter: >- + $item.type = 'kubernetes.io/tls' or ($item.type = 'Opaque' and + $contains($item.data, 'key') and $contains($item.data, 'cert')) + visibility: $item.port.protocol = 'HTTPS' + - path: serverCertificate + simple: true + visibility: $item.port.protocol = 'HTTPS' + placeholder: placeholders.serverCertificate + - path: privateKey + simple: true + visibility: $item.port.protocol = 'HTTPS' + placeholder: placeholders.privateKey + - path: caCertificates + simple: true + visibility: $item.port.protocol = 'HTTPS' + placeholder: placeholders.caCertificates + - simple: true + widget: Alert + type: warning + alert: '"alert.tls.https"' + visibility: $item.port.protocol = 'HTTPS' + - widget: SimpleList + path: '[].hosts' + required: true + simple: true + placeholder: placeholders.hosts + children: + - path: '[]' + simple: true + general: |- + resource: + kind: Gateway + group: networking.istio.io + version: v1beta1 + urlPath: gateways + category: Istio + name: Gateways + scope: namespace + description: >- + {{[Gateways](https://istio.io/latest/docs/reference/config/networking/gateway/)}} + describes a load balancer that operates at the edge of the mesh and receives + incoming or outgoing HTTP/TCP connections. + list: |- + - name: spec.selector + source: spec.selector + widget: Labels + presets: |- + - name: Default Gateway + default: true + value: + spec: + selector: + istio: ingressgateway + - name: Ingress Gateway + value: + metadata: + name: httpbin-gateway + labels: + app.kubernetes.io/name: httpbin-gateway + spec: + selector: + istio: ingressgateway + servers: + - port: + number: 443 + name: https + protocol: HTTPS + tls: + mode: SIMPLE + credentialName: '' + hosts: [] + translations: |- + en: + alert.tls.https: TLS Server of mode SIMPLE or MUTUAL needs either credential name, or private key and server certificate pair. + spec.selector: Selector + spec.gateways: Gateways + spec.servers: Servers + spec.servers.port: Port + spec.servers.port.name: Port Name + spec.servers.port.protocol: Protocol + spec.servers.port.targetPort: Target Port + spec.servers.port.number: Port Number + spec.servers.tls: TLS + spec.servers.tls.mode: TLS Mode + spec.servers.tls.httpsRedirect: HTTP Redirect + spec.servers.tls.credentialName: Credential Name + spec.servers.tls.serverCertificate: Server Certificate + spec.servers.tls.privateKey: Private Key + spec.servers.tls.caCertificates: CA Certificate + spec.servers.hosts: Hosts + selector.matchesAllPods: Matches all Pods in the Namespace + placeholders.dropdown: Type or choose an option + placeholders.serverCertificate: Enter the certificate path + placeholders.privateKey: Enter the private key path + placeholders.caCertificates: Enter the CA certificates path + placeholders.hosts: For example, *.api.mydomain.com + inputInfo.spec.servers.port.number: Must be a on-negative number. +kind: ConfigMap +metadata: + labels: + app.kubernetes.io/component: istio-operator.kyma-project.io + app.kubernetes.io/name: istios.operator.kyma-project.io + busola.io/extension: resource + busola.io/extension-version: "0.5" + name: istio-gateways-ui.operator.kyma-project.io + namespace: kyma-system +--- +apiVersion: v1 +data: + dataSources: | + podSelector: + resource: + kind: Pod + version: v1 + filter: $matchByLabelSelector($item, $root.spec.workloadSelector.labels) + details: |- + header: + - source: spec.location + name: spec.location + - source: spec.resolution + name: spec.resolution + resourceGraph: + colorVariant: 2 + dataSources: + - source: podSelector + body: + - name: configuration + widget: Panel + source: spec + visibility: >- + $boolean($exists($value.hosts) or $exists($value.addresses) or + $exists($value.subjectAltNames)) + children: + - name: spec.hosts + source: hosts + widget: JoinedArray + visibility: $exists($value) + - name: spec.addresses + source: addresses + widget: JoinedArray + visibility: $exists($value) + - name: spec.exportTo + source: exportTo + widget: Labels + placeholder: Exported to all Namespaces + - name: spec.subjectAltNames + source: subjectAltNames + widget: JoinedArray + visibility: $exists($value) + - name: spec.ports + widget: Table + source: spec.ports + visibility: $exists($value) + children: + - name: spec.ports.number + source: number + sort: true + - name: spec.ports.protocol + source: protocol + sort: true + - name: spec.ports.name + source: name + sort: true + - name: spec.ports.targetPort + source: targetPort + sort: true + - name: spec.endpoints + widget: Table + source: spec.endpoints + visibility: $exists($value) + children: + - name: spec.endpoints.address + source: address + sort: true + - name: spec.endpoints.ports + source: ports + widget: Labels + - name: spec.endpoints.labels + source: labels + widget: Labels + - name: spec.endpoints.network + source: network + sort: true + - name: spec.endpoints.weight + source: weight + - name: spec.endpoints.serviceAccount + source: serviceAccount + - name: spec.workloadSelector + widget: Panel + source: spec.workloadSelector.labels + visibility: $exists($value) + disablePadding: true + children: + - source: $podSelector() + widget: ResourceList + disableCreate: true + header: + - widget: Labels + source: spec.workloadSelector.labels + visibility: $exists($value) + form: |- + - path: spec.hosts + name: spec.hosts + widget: SimpleList + simple: true + required: true + children: + - path: '[]' + simple: true + - path: spec.addresses + name: spec.addresses + widget: SimpleList + placeholder: placeholders.addreses + children: + - path: '[]' + - path: spec.ports + name: Ports + widget: GenericList + children: + - path: '[].number' + name: spec.ports.number + required: true + - path: '[].protocol' + name: spec.ports.protocol + required: true + placeholder: placeholders.dropdown + enum: + - HTTP + - HTTPS + - GRPC + - HTTP2 + - MONGO + - TCP + - TLS + - path: '[].name' + name: spec.ports.name + required: true + - path: '[].targetPort' + name: spec.ports.targetPort + - path: spec.location + name: spec.location + placeholder: placeholders.dropdown + - path: spec.resolution + name: spec.resolution + placeholder: placeholders.dropdown + - path: spec.endpoints + name: spec.endpoints + widget: GenericList + children: + - path: '[].address' + name: spec.endpoints.address + - path: '[].ports' + name: spec.endpoints.ports + widget: KeyValuePair + value: + type: number + - path: '[].labels' + name: spec.endpoints.labels + widget: KeyValuePair + - path: '[].network' + name: spec.endpoints.network + - path: '[].locality' + name: spec.endpoints.locality + - path: '[].weight' + name: spec.endpoints.weight + - path: '[].serviceAccount' + name: spec.endpoints.serviceAccount + - path: spec.workloadSelector.labels + name: spec.workloadSelector + widget: KeyValuePair + defaultExpanded: true + - path: spec.exportTo + name: spec.exportTo + widget: SimpleList + children: + - path: '[]' + - path: spec.subjectAltNames + name: spec.subjectAltNames + widget: SimpleList + children: + - path: '[]' + general: |- + resource: + kind: ServiceEntry + group: networking.istio.io + version: v1beta1 + urlPath: serviceentries + category: Istio + name: Service Entries + scope: namespace + description: >- + {{[ServiceEntry](https://istio.io/latest/docs/reference/config/networking/service-entry/)}} + allows for adding more entries to the internal service registry of Istio. + list: |- + - source: spec.location + name: spec.location + sort: true + - source: spec.resolution + name: spec.resolution + sort: true + translations: |- + en: + configuration: Configuration + spec.hosts: Hosts + spec.addresses: Addresses + spec.ports: Ports + spec.ports.number: Number + spec.ports.protocol: Protocol + spec.ports.name: Name + spec.ports.targetPort: Target Port + spec.location: Location + spec.resolution: Resolution + spec.endpoints: Endpoints + spec.endpoints.address: Address + spec.endpoints.ports: Ports + spec.endpoints.labels: Labels + spec.endpoints.network: Network + spec.endpoints.locality: Locality + spec.endpoints.weight: Weight + spec.endpoints.serviceAccount: Service Account + spec.workloadSelector: Workload Selector + spec.exportTo: Export To + spec.subjectAltNames: Subject Alt Names + placeholders.dropdown: Type or choose an option + placeholders.addreses: For example, 127.0.0.1 +kind: ConfigMap +metadata: + labels: + app.kubernetes.io/component: istio-operator.kyma-project.io + app.kubernetes.io/name: istios.operator.kyma-project.io + busola.io/extension: resource + busola.io/extension-version: "0.5" + name: istio-serviceentries-ui.operator.kyma-project.io + namespace: kyma-system +--- +apiVersion: v1 +data: + dataSources: |- + podSelector: + resource: + kind: Pod + version: v1 + filter: $matchByLabelSelector($item, $root.spec.workloadSelector.labels) + details: |- + header: + - name: Outbound Traffic Policy Mode + source: spec.outboundTrafficPolicy.mode + body: + - widget: Table + source: spec.egress + name: Egress + visibility: $exists($value) + children: + - source: port + name: Port + widget: Panel + visibility: $exists($value) + children: + - source: number + name: Number + - source: name + name: Name + - source: protocol + name: Protocol + - source: targetPoint + name: Target Point + - source: bind + name: Bind + - source: captureMode + name: Capture Mode + - source: hosts + name: Hosts + widget: Labels + - widget: Table + source: spec.ingress + name: Ingress + visibility: $exists($value) + children: + - source: port + name: Port + widget: Panel + visibility: $exists($value) + children: + - source: number + name: Number + - source: name + name: Name + - source: protocol + name: Protocol + - source: targetPoint + name: Target Point + - source: $parent.tls.mode + name: TLS Mode + - source: bind + name: Bind + - source: captureMode + name: Capture Mode + - source: defaultEndpoint + name: Default Endpoint + widget: Labels + - widget: Panel + name: Workload Selector + disablePadding: true + children: + - source: $podSelector() + widget: ResourceList + disableCreate: true + isCompact: true + visibility: $exists($root.spec.workloadSelector.labels) and $boolean($root.spec.workloadSelector.labels) + - source: spec.workloadSelector.labels + widget: Panel + name: Matches all Pods in the Namespace + visibility: $not($exists($value)) or $not($boolean($value)) + header: + - source: spec.workloadSelector.labels + widget: Labels + name: Workload Selector + visibility: $exists($value) and $boolean($value) + resourceGraph: + depth: 1 + colorVariant: 1 + dataSources: + - source: podSelector + form: |- + - path: spec.workloadSelector.labels + name: Workload Selector + widget: KeyValuePair + - widget: FormGroup + path: spec.egress[].port + simple: true + children: + - path: number + simple: true + placeholder: Enter the port number + - path: name + widget: Name + inputInfo: null + simple: true + - path: protocol + simple: true + enum: + - HTTP + - HTTPS + - HTTP2 + - GRPC + - MONGO + - TCP + - TLS + placeholder: Type or choose an option + - path: spec.egress[].bind + placeholder: Enter the IPv4 or IPv6 + simple: true + - path: spec.egress[].captureMode + simple: true + enum: + - DEFAULT + - IPTABLES + - NONE + placeholder: Type or choose an option + - widget: SimpleList + path: spec.egress[].hosts + required: true + simple: true + placeholder: For example, *.api.mydomain.com + children: + - path: '[]' + simple: true + - widget: FormGroup + path: spec.ingress[].port + required: true + simple: true + children: + - path: number + simple: true + required: true + placeholder: Enter the port number + - path: name + widget: Name + inputInfo: null + simple: true + required: true + - path: protocol + simple: true + enum: + - HTTP + - HTTPS + - HTTP2 + - GRPC + - MONGO + - TCP + - TLS + required: true + placeholder: Type or choose an option + - path: spec.ingress[].bind + placeholder: Enter the IPv4 or IPv6 + simple: true + - path: spec.ingress[].captureMode + enum: + - DEFAULT + - IPTABLES + - NONE + simple: true + placeholder: Type or choose an option + - path: spec.ingress[].defaultEndpoint + placeholder: For example, 127.0.0.1:PORT + required: true + simple: true + - widget: FormGroup + simple: true + path: spec.ingress[].tls + name: TLS + visibility: $item.port.protocol = 'HTTPS' + children: + - path: mode + name: TLS Mode + simple: true + visibility: $item.port.protocol = 'HTTPS' + required: true + placeholder: Type or choose an option + - path: serverCertificate + name: Server Certificate + simple: true + visibility: $item.port.protocol = 'HTTPS' + placeholder: Enter the certificate path + - path: privateKey + name: Private Key + simple: true + visibility: $item.port.protocol = 'HTTPS' + placeholder: Enter the private key path + - path: caCertificates + name: CA Certificate + simple: true + visibility: $item.port.protocol = 'HTTPS' + placeholder: Enter the CA certificates path + - widget: FormGroup + path: spec.outboundTrafficPolicy + name: Outbound Traffic Policy + children: + - path: mode + name: Outbound Traffic Policy Mode + enum: + - REGISTRY_ONLY + - ALLOW_ANY + placeholder: Type or choose an option + general: |- + resource: + kind: Sidecar + group: networking.istio.io + version: v1beta1 + urlPath: sidecars + category: Istio + name: Sidecars + scope: namespace + description: >- + {{[Sidecar](https://istio.io/latest/docs/reference/config/networking/sidecar/)}} + manages the incoming and outgoing communication in a workload it is attached + to. + list: |- + - source: spec.outboundTrafficPolicy.mode + name: Outbound Traffic Policy Mode + - source: spec.workloadSelector.labels + name: Workload Selector Labels + widget: Labels +kind: ConfigMap +metadata: + labels: + app.kubernetes.io/component: istio-operator.kyma-project.io + app.kubernetes.io/name: istios.operator.kyma-project.io + busola.io/extension: resource + busola.io/extension-version: "0.5" + name: istio-sidecars-ui.operator.kyma-project.io + namespace: kyma-system +--- +apiVersion: v1 +data: + details: |- + header: + - name: Ready + source: status.state + widget: Badge + description: status.description + highlights: + positive: + - 'Ready' + negative: + - 'Error' + critical: + - 'Warning' + body: + - widget: Tabs + children: + - name: General + children: + - widget: Panel + name: Configuration + children: + - source: spec.config.numTrustedProxies + name: config.numTrustedProxies + visibility: '$exists($value)' + - name: Components + children: + - widget: Panel + name: Pilot + children: + - source: spec.components.pilot.k8s.hpaSpec.minReplicas + name: k8s.hpaSpec.minReplicas + visibility: '$exists($value)' + - source: spec.components.pilot.k8s.hpaSpec.maxReplicas + name: k8s.hpaSpec.maxReplicas + visibility: '$exists($value)' + - source: spec.components.pilot.k8s.strategy.rollingUpdate.maxSurge + name: k8s.strategy.rollingUpdate.maxSurge + visibility: '$exists($value)' + - source: spec.components.pilot.k8s.strategy.rollingUpdate.maxUnavailable + name: k8s.strategy.rollingUpdate.maxUnavailable + visibility: '$exists($value)' + - source: spec.components.pilot.k8s.resources.limits.cpu + name: k8s.resources.limits.cpu + visibility: '$exists($value)' + - source: spec.components.pilot.k8s.resources.limits.memory + name: k8s.resources.limits.memory + visibility: '$exists($value)' + - source: spec.components.pilot.k8s.resources.requests.cpu + name: k8s.resources.requests.cpu + visibility: '$exists($value)' + - source: spec.components.pilot.k8s.resources.requests.memory + name: k8s.resources.requests.memory + visibility: '$exists($value)' + - widget: Panel + name: Ingress Gateway + children: + - source: spec.components.ingressGateway.k8s.hpaSpec.minReplicas + name: k8s.hpaSpec.minReplicas + visibility: '$exists($value)' + - source: spec.components.ingressGateway.k8s.hpaSpec.maxReplicas + name: k8s.hpaSpec.maxReplicas + visibility: '$exists($value)' + - source: spec.components.ingressGateway.k8s.strategy.rollingUpdate.maxSurge + name: k8s.strategy.rollingUpdate.maxSurge + visibility: '$exists($value)' + - source: spec.components.ingressGateway.k8s.strategy.rollingUpdate.maxUnavailable + name: k8s.strategy.rollingUpdate.maxUnavailable + visibility: '$exists($value)' + - source: spec.components.ingressGateway.k8s.resources.limits.cpu + name: k8s.resources.limits.cpu + visibility: '$exists($value)' + - source: spec.components.ingressGateway.k8s.resources.limits.memory + name: k8s.resources.limits.memory + visibility: '$exists($value)' + - source: spec.components.ingressGateway.k8s.resources.requests.cpu + name: k8s.resources.requests.cpu + visibility: '$exists($value)' + - source: spec.components.ingressGateway.k8s.resources.requests.memory + name: k8s.resources.requests.memory + visibility: '$exists($value)' + - widget: Panel + name: CNI + children: + - source: spec.components.cni.k8s.resources.limits.cpu + name: k8s.resources.limits.cpu + visibility: '$exists($value)' + - source: spec.components.cni.k8s.resources.limits.memory + name: k8s.resources.limits.memory + visibility: '$exists($value)' + - source: spec.components.cni.k8s.resources.requests.cpu + name: k8s.resources.requests.cpu + visibility: '$exists($value)' + - source: spec.components.cni.k8s.resources.requests.memory + name: k8s.resources.requests.memory + visibility: '$exists($value)' + - source: spec.components.cni.k8s.affinity + name: k8s.affinity + widget: CodeViewer + description: "Kubernetes documentation for {{[Affinity](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#affinity-and-anti-affinity)}}" + language: "'yaml'" + visibility: '$exists($value)' + - widget: Panel + name: Proxy + children: + - source: spec.components.proxy.k8s.resources.limits.cpu + name: k8s.resources.limits.cpu + visibility: '$exists($value)' + - source: spec.components.proxy.k8s.resources.limits.memory + name: k8s.resources.limits.memory + visibility: '$exists($value)' + - source: spec.components.proxy.k8s.resources.requests.cpu + name: k8s.resources.requests.cpu + visibility: '$exists($value)' + - source: spec.components.proxy.k8s.resources.requests.memory + name: k8s.resources.requests.memory + visibility: '$exists($value)' + - name: Reconciliation + children: + - widget: EventList + filter: '$matchEvents($$, $root.kind, $root.metadata.name)' + name: Events + defaultType: information + form: |- + - path: spec.config + simple: true + name: General + widget: FormGroup + defaultExpanded: true + children: + - path: numTrustedProxies + simple: true + name: config.numTrustedProxies + inputInfo: inputInfo.config.numTrustedProxies + value: + type: number + - path: spec.components + name: Components + widget: FormGroup + defaultExpanded: true + children: + - path: 'pilot' + required: false + name: Pilot + widget: FormGroup + defaultExpanded: false + children: + - path: 'k8s.hpaSpec' + name: k8s.hpaSpec + widget: FormGroup + defaultExpanded: false + children: + - path: 'minReplicas' + name: k8s.hpaSpec.minReplicas + inputInfo: inputInfo.hpaSpec.minReplicas + value: + type: number + - path: 'maxReplicas' + name: k8s.hpaSpec.maxReplicas + inputInfo: inputInfo.hpaSpec.maxReplicas + value: + type: number + - path: 'k8s.strategy.rollingUpdate' + required: false + name: k8s.strategy.rollingUpdate + widget: FormGroup + defaultExpanded: false + type: object + properties: + maxSurge: + type: string + maxUnavailable: + type: string + children: + - path: 'maxSurge' + name: k8s.strategy.rollingUpdate.maxSurge + inputInfo: inputInfo.rollingUpdate.maxSurge + value: + type: string + pattern: ^\d+%?$ + - path: 'maxUnavailable' + name: k8s.strategy.rollingUpdate.maxUnavailable + inputInfo: inputInfo.rollingUpdate.maxUnvailable + value: + type: string + pattern: ^\d+%?$ + - path: 'k8s.resources.limits' + name: Resource Limits + widget: FormGroup + defaultExpanded: false + children: + - path: 'cpu' + name: k8s.resources.cpu + inputInfo: inputInfo.limits.cpu + - path: 'memory' + name: k8s.resources.memory + inputInfo: inputInfo.limits.memory + - path: 'k8s.resources.requests' + name: Resource Requests + widget: FormGroup + defaultExpanded: false + children: + - path: 'cpu' + name: k8s.resources.cpu + inputInfo: inputInfo.requests.cpu + - path: 'memory' + name: k8s.resources.memory + inputInfo: inputInfo.requests.memory + - path: 'ingressGateway' + required: false + name: Ingress Gateway + widget: FormGroup + defaultExpanded: false + children: + - path: 'k8s.hpaSpec' + name: k8s.hpaSpec + widget: FormGroup + defaultExpanded: false + children: + - path: 'minReplicas' + name: k8s.hpaSpec.minReplicas + inputInfo: inputInfo.hpaSpec.minReplicas + value: + type: number + - path: 'maxReplicas' + name: k8s.hpaSpec.maxReplicas + inputInfo: inputInfo.hpaSpec.maxReplicas + value: + type: number + - path: 'k8s.strategy.rollingUpdate' + required: false + name: k8s.strategy.rollingUpdate + widget: FormGroup + defaultExpanded: false + type: object + properties: + maxSurge: + type: string + maxUnavailable: + type: string + children: + - path: 'maxSurge' + name: k8s.strategy.rollingUpdate.maxSurge + inputInfo: inputInfo.rollingUpdate.maxSurge + value: + type: string + pattern: ^\d+%?$ + - path: 'maxUnavailable' + name: k8s.strategy.rollingUpdate.maxUnavailable + inputInfo: inputInfo.rollingUpdate.maxUnvailable + value: + type: string + pattern: ^\d+%?$ + - path: 'k8s.resources.limits' + name: Resource Limits + widget: FormGroup + defaultExpanded: false + children: + - path: 'cpu' + name: k8s.resources.cpu + inputInfo: inputInfo.limits.cpu + - path: 'memory' + name: k8s.resources.memory + inputInfo: inputInfo.limits.memory + - path: 'k8s.resources.requests' + name: Resource Requests + widget: FormGroup + defaultExpanded: false + children: + - path: 'cpu' + name: k8s.resources.cpu + inputInfo: inputInfo.requests.cpu + - path: 'memory' + name: k8s.resources.memory + inputInfo: inputInfo.requests.memory + - path: 'cni' + required: false + name: CNI + widget: FormGroup + defaultExpanded: false + children: + - path: 'k8s.affinity' + widget: CodeEditor + inputInfo: k8s.affinity + language: "'yaml'" + - path: 'k8s.resources.limits' + name: Resource Limits + widget: FormGroup + defaultExpanded: false + children: + - path: 'cpu' + name: k8s.resources.cpu + inputInfo: inputInfo.limits.cpu + - path: 'memory' + name: k8s.resources.memory + inputInfo: inputInfo.limits.memory + - path: 'k8s.resources.requests' + name: Resource Requests + widget: FormGroup + defaultExpanded: false + children: + - path: 'cpu' + name: k8s.resources.cpu + inputInfo: inputInfo.requests.cpu + - path: 'memory' + name: k8s.resources.memory + inputInfo: inputInfo.requests.memory + - path: 'proxy' + required: false + name: Proxy + widget: FormGroup + defaultExpanded: false + children: + - path: 'k8s.resources.limits' + name: Resource Limits + widget: FormGroup + defaultExpanded: false + children: + - path: 'cpu' + name: k8s.resources.cpu + inputInfo: inputInfo.limits.cpu + - path: 'memory' + name: k8s.resources.memory + inputInfo: inputInfo.limits.memory + - path: 'k8s.resources.requests' + name: Resource Requests + widget: FormGroup + defaultExpanded: false + children: + - path: 'cpu' + name: k8s.resources.cpu + inputInfo: inputInfo.requests.cpu + - path: 'memory' + name: k8s.resources.memory + inputInfo: inputInfo.requests.memory + general: |- + resource: + kind: Istio + group: operator.kyma-project.io + version: v1alpha2 + urlPath: istios + category: Kyma + name: Istio + scope: namespace + features: + actions: + disableCreate: true + disableDelete: true + description: >- + {{[Istio CR](https://github.com/kyma-project/istio/blob/main/config/samples/operator_v1alpha2_istio.yaml)}} + describes the Istio module + list: | + - name: Ready + source: status.state + widget: Badge + description: status.description + highlights: + positive: + - 'Ready' + negative: + - 'Error' + critical: + - 'Warning' + translations: |- + en: + config.numTrustedProxies: Number of trusted proxies + inputInfo.config.numTrustedProxies: Number of trusted proxies deployed in front of the Istio gateway proxy + k8s.hpaSpec: Horizontal Pod Autoscaler + k8s.hpaSpec.minReplicas: Minimum number of replicas + k8s.hpaSpec.maxReplicas: Maximum number of replicas + k8s.strategy.rollingUpdate: Rolling update strategy + k8s.strategy.rollingUpdate.maxSurge: Maximum surge + k8s.strategy.rollingUpdate.maxUnavailable: Maximum unavailable + k8s.resources.cpu: CPU + k8s.resources.memory: Memory + k8s.resources.limits.cpu: CPU limits + k8s.resources.limits.memory: Memory limits + k8s.resources.requests.cpu: CPU requests + k8s.resources.requests.memory: Memory requests + k8s.affinity: Affinity (YAML) + inputInfo.hpaSpec.minReplicas: Minimum number of replicas for this deployment + inputInfo.hpaSpec.maxReplicas: Maximum number of replicas for this deployment + inputInfo.rollingUpdate.maxSurge: Maximum number of Pods, or the percentage of Pods that can be created on top during an update + inputInfo.rollingUpdate.maxUnvailable: Maximum number of Pods, or the percentage of Pods that can be unavailable during an update + inputInfo.limits.cpu: Total CPU limits of all Pods that are in a non-terminal state must not exceed this value + inputInfo.limits.memory: Total memory limits of all Pods that are in a non-terminal state must not exceed this value + inputInfo.requests.cpu: Total CPU requests of all Pods that are in a non-terminal state must not exceed this value + inputInfo.requests.memory: Total memory requests of all Pods that are in a non-terminal state must not exceed this value +kind: ConfigMap +metadata: + labels: + app.kubernetes.io/component: istio-operator.kyma-project.io + app.kubernetes.io/name: istios.operator.kyma-project.io + busola.io/extension: resource + busola.io/extension-version: "0.5" + name: istio-ui.operator.kyma-project.io + namespace: kyma-system +--- +apiVersion: v1 +data: + dataSources: |- + relatedGateways: + resource: + kind: Gateway + group: networking.istio.io + version: v1beta1 + namespace: null + filter: >- + $filter($root.spec.gateways, function($g){$contains($g,'/') ? + ($substringBefore($g,'/') = $item.metadata.namespace and $substringAfter($g, + '/') = $item.metadata.name) : ($substringBefore($g, '.') = + $item.metadata.name and $substringBefore($substringAfter($g, '.'), '.') = + $item.metadata.namespace) }) + relatedServices: + resource: + kind: Service + version: v1 + namespace: null + filter: >- + $filter($root.spec.http.route, function($r) { $filter($r.destination.host, + function($h){($substringBefore($h, '.') = $item.metadata.name) and + ($split($substringAfter($h, '.'),'.')[0] = $item.metadata.namespace)} ) }) + details: |- + resourceGraph: + dataSources: + - source: relatedGateways + - source: relatedServices + body: + - widget: Table + source: spec.gateways[] + name: gateways + visibility: $exists($value) + children: + - source: $item + name: t-name + widget: ResourceLink + resource: + kind: '"Gateway"' + name: >- + $contains($item,'/') ? $substringAfter($item, '/') : + $substringBefore($item, '.') + namespace: >- + $contains($item,'/') ? $substringBefore($item, '/'): + $substringBefore($substringAfter($item, '.'), '.') + - name: summary + widget: Panel + source: spec + visibility: $boolean($exists($value.exportTo) or $exists($value.hosts)) + children: + - name: exportTo + source: exportTo + widget: Labels + visibility: $exists($value) + - name: hosts + source: hosts + widget: JoinedArray + visibility: $exists($value) + - widget: Table + source: spec.http + name: http + visibility: $exists($value) + children: + - source: name + name: t-name + - source: timeout + name: timeout + - source: mirrorPercentage.value + name: mirrorPercentage + collapsible: + - source: match + name: matches + widget: Table + visibility: $exists($value) + children: + - source: name + name: t-name + - source: uri + name: uri + widget: Labels + - source: scheme + name: scheme + widget: Labels + - source: method + name: method + widget: Labels + - source: authority + name: authority + widget: Labels + - source: headers + name: headers + - source: port + name: port + - source: sourceLabels + name: sourceLabels + widget: Labels + - source: gateways + name: gateways + widget: JoinedArray + - source: queryParams + name: queryParams + - source: ignoreUriCase + name: ignoreUriCase + - source: withoutHeaders + name: withoutHeaders + - source: sourceNamespace + name: sourceNamespace + - source: statPrefix + name: statPrefix + - source: route + name: routes + widget: Table + visibility: $exists($value) + children: + - source: destination + name: destination + widget: Panel + visibility: $exists($value) + children: + - source: host + name: host + - source: subset + name: subset + - source: port.number + name: port.number + - source: weight + name: weight + - source: headers + name: headers + widget: Panel + visibility: $exists($value) + children: + - source: request + name: request + widget: Panel + visibility: $exists($value) + children: + - source: set + name: set + widget: Labels + - source: add + name: add + widget: Labels + - source: remove + name: remove + widget: JoinedArray + - source: response + name: response + widget: Panel + visibility: $exists($value) + children: + - source: set + name: set + widget: Labels + - source: add + name: add + widget: Labels + - source: remove + name: remove + widget: JoinedArray + - source: redirect + name: redirect + widget: Panel + visibility: $exists($value) + children: + - source: uri + name: uri + - source: authority + name: authority + - source: port + name: port + - source: derivePort + name: derivePort + widget: Labels + - source: scheme + name: scheme + - source: redirectCode + name: redirectCode + - source: directResponse + name: directResponse + widget: Panel + visibility: $exists($value) + children: + - source: status + name: status + - source: body + name: body + widget: Panel + visibility: $exists($value) + children: + - source: string + name: string + - source: bytes + name: bytes + - source: delegate + name: delegate + widget: Panel + visibility: $exists($value) + children: + - source: name + name: t-name + - source: namespace + name: namespace + - source: rewrite + name: rewrite + widget: Panel + visibility: $exists($value) + children: + - source: uri + name: uri + - source: authority + name: authority + - source: retries + name: retries + widget: Panel + visibility: $exists($value) + children: + - source: attempts + name: attempts + - source: perTryTimeout + name: perTryTimeout + - source: retryOn + name: retryOn + - source: retryRemoteLocalities + name: retryRemoteLocalities + - source: fault + name: fault + widget: Panel + visibility: $exists($value) + children: + - source: delay + name: delay + widget: Panel + visibility: $exists($value) + children: + - source: fixedDelay + name: fixedDelay + - source: percentage.value + name: percentage.value + - source: percent + name: percent + - source: abort + name: Abort + widget: Panel + visibility: $exists($value) + children: + - source: httpStatus + name: httpStatus + - source: percentage.value + name: percentage.value + - source: mirror + name: Mirror + widget: Panel + visibility: $exists($value) + children: + - source: host + name: host + - source: subset + name: subset + - source: port.number + name: port.number + - source: corsPolicy + name: corsPolicy + widget: Panel + visibility: $exists($value) + children: + - source: allowOrigins + name: allowOrigins + - source: allowMethods + name: allowMethods + widget: JoinedArray + - source: allowHeaders + name: allowHeaders + widget: JoinedArray + - source: exposeHeaders + name: exposeHeaders + widget: JoinedArray + - source: maxAge + name: maxAge + - source: allowCredentials + name: allowCredentials + - source: headers + name: headers + widget: Panel + visibility: $exists($value) + children: + - source: request + name: request + widget: Panel + visibility: $exists($value) + children: + - source: set + name: set + widget: Labels + - source: add + name: add + widget: Labels + - source: remove + name: remove + widget: JoinedArray + - source: response + name: response + widget: Panel + visibility: $exists($value) + children: + - source: set + name: set + widget: Labels + - source: add + name: add + widget: Labels + - source: remove + name: remove + widget: JoinedArray + - widget: Table + source: spec.tcp + name: tcp + visibility: $exists($value) + children: + - source: match + name: matches + widget: Table + visibility: $exists($value) + children: + - source: destinationSubnets + name: destinationSubnets + widget: JoinedArray + - source: port + name: port + - source: sourceLabels + name: sourceLabels + widget: Labels + - source: gateways + name: gateways + widget: JoinedArray + - source: sourceNamespace + name: sourceNamespace + collapsible: + - source: route + name: routes + widget: Table + visibility: $exists($value) + children: + - source: destination + name: destination + widget: Panel + visibility: $exists($value) + children: + - source: host + name: host + - source: subset + name: subset + - source: port.number + name: port + - source: weight + name: weight + - widget: Table + source: spec.tls + name: tls + visibility: $exists($value) + children: + - source: match + name: matches + widget: Table + visibility: $exists($value) + children: + - source: sniHosts + name: sniHosts + widget: JoinedArray + - source: destinationSubnets + name: destinationSubnets + widget: JoinedArray + - source: port + name: port + - source: sourceLabels + name: sourceLabels + widget: Labels + - source: gateways + name: gateways + widget: JoinedArray + - source: sourceNamespace + name: sourceNamespace + collapsible: + - source: route + name: routes + widget: Table + visibility: $exists($value) + children: + - source: destination + name: destination + widget: Panel + children: + - source: host + name: host + - source: subset + name: subset + - source: port.number + name: port.number + - source: weight + name: weight + form: |- + - path: spec.tls + widget: GenericList + name: tls + children: + - path: '[].match' + widget: GenericList + name: matches + children: + - path: '[].sniHosts' + widget: SimpleList + name: sniHosts + children: + - path: '[]' + - path: '[].sourceNamespace' + name: sourceNamespace + - path: '[].port' + name: port + - path: '[].destinationSubnets' + widget: SimpleList + name: destinationSubnets + children: + - path: '[]' + - path: '[].sourceLabels' + widget: KeyValuePair + name: sourceLabels + - path: '[].gateways' + widget: SimpleList + name: gateways + children: + - path: '[]' + - path: '[].route' + widget: GenericList + name: routes + children: + - path: '[].destination' + widget: FormGroup + name: destination + children: + - path: host + name: host + - path: subset + name: subset + - path: port.number + name: port.number + - path: '[].weight' + name: weight + - path: spec.tcp + name: tcp + widget: GenericList + children: + - path: '[].match' + name: matches + children: + - path: '[].sourceNamespace' + name: sourceNamespace + - path: '[].port' + name: port + - path: '[].sniHosts' + widget: SimpleList + name: sniHosts + children: + - path: '[]' + - path: '[].destinationSubnets' + widget: SimpleList + name: destinationSubnets + children: + - path: '[]' + - path: '[].sourceLabels' + name: sourceLabels + widget: KeyValuePair + - path: '[].gateways' + widget: SimpleList + name: gateways + children: + - path: '[]' + - path: '[].route' + name: routes + children: + - path: '[].destination' + widget: FormGroup + name: destination + children: + - path: host + name: host + - path: subset + name: subset + - path: port.number + name: port.number + - path: '[].weight' + name: weight + - path: spec.http + simple: true + name: http + widget: GenericList + children: + - path: '[].match' + simple: true + name: matches + widget: GenericList + children: + - path: '[].name' + simple: true + name: t-name + - path: '[].uri' + simple: true + name: uri + widget: KeyValuePair + keyEnum: + - prefix + - exact + - regex + - path: '[].scheme' + simple: true + name: scheme + widget: KeyValuePair + keyEnum: + - prefix + - exact + - regex + - path: '[].method' + simple: true + name: method + widget: KeyValuePair + keyEnum: + - prefix + - exact + - regex + - path: '[].authority' + simple: true + name: authority + widget: KeyValuePair + keyEnum: + - prefix + - exact + - regex + - path: '[].headers' + simple: true + name: headers + defaultExpanded: true + widget: KeyValuePair + value: + type: object + keyEnum: + - prefix + - exact + - regex + - path: '[].port' + simple: true + name: port + - path: '[].sourceLabels' + simple: true + name: sourceLabels + widget: KeyValuePair + - path: '[].gateways' + simple: true + name: gateways + widget: SimpleList + children: + - path: '[]' + - path: '[].queryParams' + simple: true + name: queryParams + widget: KeyValuePair + value: + type: object + keyEnum: + - prefix + - exact + - regex + - path: '[].ignoreUriCase' + simple: true + name: ignoreUriCase + - path: '[].withoutHeaders' + simple: true + name: withoutHeaders + widget: KeyValuePair + value: + type: object + keyEnum: + - prefix + - exact + - regex + - path: '[].sourceNamespace' + simple: true + name: sourceNamespace + - path: '[].statPrefix' + simple: true + name: statPrefix + - path: '[].route' + simple: true + name: routes + children: + - path: '[].destination' + simple: true + name: destination + widget: FormGroup + children: + - path: host + name: host + - path: subset + name: subset + - path: port.number + name: port.number + - path: '[].weight' + simple: true + name: weight + - path: '[].headers' + simple: true + name: headers + widget: FormGroup + children: + - path: response + simple: true + name: response + widget: FormGroup + children: + - path: set + simple: true + name: set + widget: KeyValuePair + - path: add + simple: true + name: add + widget: KeyValuePair + - path: remove + simple: true + name: remove + widget: SimpleList + children: + - path: '[]' + simple: true + - path: request + simple: true + name: request + widget: FormGroup + children: + - path: set + simple: true + name: set + widget: KeyValuePair + - path: add + simple: true + name: add + widget: KeyValuePair + - path: remove + simple: true + name: remove + widget: SimpleList + children: + - path: '[]' + simple: true + - path: '[].redirect' + simple: true + name: redirect + widget: FormGroup + children: + - path: uri + simple: true + name: uri + - path: authority + simple: true + name: authority + - path: port + simple: true + name: port + - path: derivePort + simple: true + name: derivePort + - path: scheme + simple: true + name: scheme + - path: redirectCode + simple: true + name: redirectCode + - path: '[].directResponse' + simple: true + name: directResponse + widget: FormGroup + children: + - path: status + simple: true + name: status + - path: body + simple: true + name: body + widget: FormGroup + children: + - path: string + simple: true + name: string + - path: bytes + simple: true + name: bytes + - path: '[].delegate' + simple: true + name: delegate + widget: FormGroup + children: + - path: name + simple: true + name: name + - path: namespace + simple: true + name: namespace + - path: '[].rewrite' + simple: true + name: rewrite + widget: FormGroup + children: + - path: uri + simple: true + name: uri + - path: authority + simple: true + name: authority + - path: '[].timeout' + simple: true + name: timeout + - path: '[].retries' + simple: true + name: retries + widget: FormGroup + children: + - path: attempts + simple: true + name: attempts + - path: perTryTimeout + simple: true + name: perTryTimeout + - path: retryOn + simple: true + name: retryOn + - path: retryRemoteLocalities + simple: true + name: retryRemoteLocalities + - path: '[].fault' + simple: true + name: fault + widget: FormGroup + children: + - path: delay + simple: true + name: delay + widget: FormGroup + children: + - path: fixedDelay + simple: true + name: fixedDelay + - path: percentage.value + simple: true + name: percentage.value + - path: percent + simple: true + name: percent + - path: abort + simple: true + name: abort + widget: FormGroup + children: + - path: httpStatus + simple: true + name: httpStatus + - path: grpcStatus + simple: true + name: grpcStatus + - path: percentage.value + simple: true + name: percentage.value + - path: '[].mirror' + simple: true + name: mirror + widget: FormGroup + children: + - path: host + simple: true + name: host + - path: subset + simple: true + name: subset + - path: port.number + simple: true + name: port.number + - path: '[].mirrorPercentage.value' + simple: true + name: mirrorPercentage + - path: '[].corsPolicy' + simple: true + name: corsPolicy + widget: FormGroup + children: + - path: allowCredentials + simple: true + name: allowCredentials + type: boolean + - path: allowMethods + simple: true + name: allowMethods + widget: SimpleList + placeholder: allowMethods.placeholder + children: + - path: '[]' + simple: true + - path: allowHeaders + simple: true + name: allowHeaders + widget: SimpleList + children: + - path: '[]' + simple: true + - path: exposeHeaders + simple: true + name: exposeHeaders + widget: SimpleList + children: + - path: '[]' + simple: true + - path: maxAge + simple: true + name: maxAge + placeholder: maxAge.placeholder + - path: '[].headers' + simple: true + name: headers + widget: FormGroup + children: + - path: response + simple: true + name: response + widget: FormGroup + children: + - path: set + simple: true + name: set + widget: KeyValuePair + - path: add + simple: true + name: add + widget: KeyValuePair + - path: remove + simple: true + name: remove + widget: SimpleList + children: + - path: '[]' + simple: true + - path: request + simple: true + name: request + widget: FormGroup + children: + - path: set + simple: true + name: set + widget: KeyValuePair + - path: add + simple: true + name: add + widget: KeyValuePair + - path: remove + simple: true + name: remove + widget: SimpleList + children: + - path: '[]' + simple: true + - path: spec.hosts + name: hosts + widget: SimpleList + children: + - path: '[]' + - path: spec.gateways + name: gateways + widget: SimpleList + children: + - path: '[]' + - path: spec.exportTo + name: exportTo + widget: SimpleList + children: + - path: '[]' + general: |- + resource: + kind: VirtualService + group: networking.istio.io + version: v1beta1 + urlPath: virtualservices + category: Istio + name: Virtual Services + scope: namespace + description: >- + {{[VirtualService](https://istio.io/latest/docs/reference/config/networking/virtual-service/)}} + describes a configuration that affects traffic routing. . + list: |- + - name: hosts + source: spec.hosts + widget: JoinedArray + - name: gateways + source: spec.gateways + widget: JoinedArray + translations: |- + en: + t-name: Name + gateways: Gateways + hosts: Hosts + exportTo: Export to + summary: Summary + http: HTTP + tls: TLS + tcp: TCP + mirror: Mirror + mirrorPercentage: Mirror Percentage + timeout: Timeout + matches: Matches + uri: URI + scheme: Scheme + method: Method + authority: Authority + headers: Headers + port: Port + sourceLabels: Source Labels + queryParams: Query Params + ignoreUriCase: Ignore URI Case + withoutHeaders: Without Headers + sourceNamespace: Source Namespace + statPrefix: Stat Prefix + routes: Routes + destination: Destination + host: Host + subset: Subset + port.number: Port Number + weight: Weight + request: Request + response: Response + set: Set + add: Add + remove: Remove + redirect: Redirect + derivePort: Derive Port + redirectCode: Redirect Code + directResponse: Direct Response + status: Status + body: Body + string: String + bytes: Bytes + delegate: Delegate + namespace: Namespace + rewrite: Rewrite + retries: Retries + attempts: Attempts + perTryTimeout: Per Try Timeout + retryOn: Retry On + retryRemoteLocalities: Retry Remote Localities + fault: Fault + delay: Delay + fixedDelay: Fixed Delay + abort: Abort + percentage.value: Percentage Value + percent: Percent + httpStatus: HTTP Status + grpcStatus: GRPC Status + corsPolicy: CORS Policy + allowOrigins: Allow Origins + allowMethods: Allow Methods + allowMethods.placeholder: For example, GET + allowHeaders: Allow Headers + exposeHeaders: Expose Headers + maxAge: Max Age + maxAge.placeholder: For example, 24h + allowCredentials: Allow Credentials + destinationSubnets: Destination Subnets + sniHosts: SNI Hosts +kind: ConfigMap +metadata: + labels: + app.kubernetes.io/component: istio-operator.kyma-project.io + app.kubernetes.io/name: istios.operator.kyma-project.io + busola.io/extension: resource + busola.io/extension-version: "0.5" + name: istio-virtualservices-ui.operator.kyma-project.io + namespace: kyma-system +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/component: istio-operator.kyma-project.io + name: istio-operator-metrics + namespace: kyma-system +spec: + ports: + - name: http-metrics + port: 8080 + targetPort: 8080 + selector: + app.kubernetes.io/component: istio-operator.kyma-project.io + control-plane: controller-manager +--- +apiVersion: scheduling.k8s.io/v1 +description: Used for Istio components that are managed by Kyma Istio Manager and + must run in the cluster. +globalDefault: false +kind: PriorityClass +metadata: + labels: + app.kubernetes.io/component: istio-operator.kyma-project.io + name: istio-kyma-priority +value: 2100000 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app.kubernetes.io/component: istio-operator.kyma-project.io + app.kubernetes.io/created-by: istio + app.kubernetes.io/instance: controller-manager + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: deployment + app.kubernetes.io/part-of: istio + control-plane: controller-manager + name: istio-controller-manager + namespace: kyma-system +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/component: istio-operator.kyma-project.io + control-plane: controller-manager + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + labels: + app.kubernetes.io/component: istio-operator.kyma-project.io + control-plane: controller-manager + sidecar.istio.io/inject: "false" + spec: + containers: + - args: + - --leader-elect + - --health-probe-bind-address=:8081 + - --metrics-bind-address=127.0.0.1:8080 + command: + - /manager + image: europe-docker.pkg.dev/kyma-project/prod/istio-manager:1.3.1 + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + name: manager + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + limits: + cpu: 1000m + memory: 512Mi + requests: + cpu: 10m + memory: 64Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + securityContext: + runAsNonRoot: true + serviceAccountName: istio-controller-manager + terminationGracePeriodSeconds: 10 diff --git a/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/invalid-ca/ca.crt b/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/invalid-ca/ca.crt new file mode 100644 index 00000000..f3c8985f --- /dev/null +++ b/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/invalid-ca/ca.crt @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC+jCCAeICCQCyguPXg3Bj8jANBgkqhkiG9w0BAQsFADA/MQswCQYDVQQGEwJQ +TDEKMAgGA1UECAwBQTEMMAoGA1UECgwDU0FQMRYwFAYDVQQDDA10ZXN0LW90aGVy +LWNhMB4XDTI0MDMxMjE1MjI1NFoXDTI1MDMxMjE1MjI1NFowPzELMAkGA1UEBhMC +UEwxCjAIBgNVBAgMAUExDDAKBgNVBAoMA1NBUDEWMBQGA1UEAwwNdGVzdC1vdGhl +ci1jYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALZbWTNPMhXBgQ8t +v8bhwU8O41CL2/B2esBUpiBnujYNkwN4IeSSsmordcGtIKzod3f8mLBhn4JL4pm5 +xh1ndBdgJo6iYRe6xU5QmSndBnj6Lx2RN0s/8gEAoHJ2wyLvZJCkn3z2kLiqN6OM +ye21YNi2C5pU0xQjxkQQl/f5aF9eFInyx3XoFFMv5VT5v5oZA+wJoSAKlkbDGCxB +CFAvcecxiclca3yC0UwYEu9lhllAR6AQLEgCAG7SNTsFFf+uBlz9TblGFxNUQeke +dg0NtVDCH28PCsEBy86h5byfjsUA6reHy32sy3BAt/B/VaSNYVKCzoBxd1kV46do +eLLnUzMCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEArXTAhj11HzaDVsL7p8/XJ+mD +oEF1ukePwHzM0fVGe9uRaPxAjov8+d2nLDjqFMNTlLUYfsdYOQxPQP8jobMnkJI6 +O4RvmRMH3JHQG9vn41MIRRxCPMvWy58nGVLBq1KlQw3wgaJ4rG+mVYNFDjsR3fki +tohQDciW4qX1ohSEk0EQVl7uh0/SCoK2SniGbwI024fHthUYfi49tbamZ75Tt9Cn +XBsVAknFTgYPTJDjNid3KQwbHmKq9/iQy+kfzF3dZ/vhT2MAeBZYGoj4zj+8irNI +fUyrIoa0XM8PUnaY5ogEIA1fllaNqfscAK+xdO3W4kpApBEtftxwuKCXdINXbA== +-----END CERTIFICATE----- diff --git a/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/invalid-ca/ca.key b/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/invalid-ca/ca.key new file mode 100644 index 00000000..4d7952ed --- /dev/null +++ b/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/invalid-ca/ca.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC2W1kzTzIVwYEP +Lb/G4cFPDuNQi9vwdnrAVKYgZ7o2DZMDeCHkkrJqK3XBrSCs6Hd3/JiwYZ+CS+KZ +ucYdZ3QXYCaOomEXusVOUJkp3QZ4+i8dkTdLP/IBAKBydsMi72SQpJ989pC4qjej +jMnttWDYtguaVNMUI8ZEEJf3+WhfXhSJ8sd16BRTL+VU+b+aGQPsCaEgCpZGwxgs +QQhQL3HnMYnJXGt8gtFMGBLvZYZZQEegECxIAgBu0jU7BRX/rgZc/U25RhcTVEHp +HnYNDbVQwh9vDwrBAcvOoeW8n47FAOq3h8t9rMtwQLfwf1WkjWFSgs6AcXdZFeOn +aHiy51MzAgMBAAECggEARVy5sSqOgnf3/y4HpD56qPegvyJzpiNqzX1lMy5BRg4j +vU4Uljy2YGvnfqO7qglCaAFMksqPQaBbsN1Y3hZbsgiAogBhrgT8x60glSvdKmb/ +RN/XiGfqRTdX0DIcR02Hkv0LLR0cLyGPyEXlCOXU0JluEGXzY9W3tGwbYdccPCmI +VGS2ajOo6k5/BL3X8dGvsFwSns25GtMO4nmIVlfHU+WVjqNSjJ7PxDL8kAsh39TN +lccAf+6KMPGwhH9DRiByYzr628uZzHwdgBTt6FKeP3bmY8cg5yWnC5aA8CfjJMp1 +GcHAh/hX7aV10OgAdemICCzUDEd8FKZ3KRU9GY4PAQKBgQDjC2xSJ83+XbvKFbbZ +/sM4YYlMaTVSk2SFQfPByR4/QIOFyL423XRGlqmKVLBCZ5l5rlTlL2XWWRB85qNv +2DibrFj+afnTjxooTYkQlMTTqmLevOkTHthzH/WYcGmUWoS8DYCBxwuWubN5IVnx +uqV6Oz/fvDA3r/Bd9mjHwAi/zQKBgQDNnPQbw/mvHp67a1RZH1gUeZ7c0XLCdnLZ +WUqJE2O0/vKZ+h5bkGtAaIFB6yR8WuhkWLV2WN3JJT6VRHIAJHZ6wUO2l9um1804 +3cbgE2QXB8HH0OZ+jWQGWUFepYGxVKfzy/iO9CfxnJ34QN+LCqp4fZQkZ0yqLOZN +/m1jkc9e/wKBgEyw2hjyGxG1pa4AIbCG7nhH8fGehAVthgHBIk4t2gqxhvuUsDOm +IBWL5J62NodnqR7B9SkpFnQNx5T47vHjjlN/Jtxg/aMpbkN41TiFl+qLXjQwiWYN +AD366KFiLzeOT9GZmfO8QTzbYzUiP9h4HFcqVkwDrCHTSxTiG8iFJC9tAoGBAMRn +qt5i4zyeWS3aJmZDhJV6X3+7Ko4LK0Xm/0XVGaco6YCf5SO93lVV/jKDdQS8qcRA +4IW9+Y1MYG2hRexQ6EP2HMJsaMsE1On/HxuoKjG9nSNLrwEv+l3+IG1SV4KWxSAi +tLmJDCbFXjhnb6GXfKNAAaSMcDUWUqAp6z/zZkztAoGBALyiUozaSVuVYUF3d7xH +G+HItjZbSHLuxwO42FWHy7B0Ec9iwUAjg6e0mZJXKKD3u8Wpp5cDgSOb6qqmc51b +EpoBDvz6Lk7juY4Dpb5+Sa+awXY1HFUOl7TA5LMKkUG2ITHMOzqpp/LchE8EGQpY +1bvG1Po3isDgeGs9qfVp45On +-----END PRIVATE KEY----- diff --git a/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/invalid-ca/client.crt b/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/invalid-ca/client.crt new file mode 100644 index 00000000..59a89ff8 --- /dev/null +++ b/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/invalid-ca/client.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDHTCCAgWgAwIBAgIJAI/Amrjs0rrcMA0GCSqGSIb3DQEBCwUAMD8xCzAJBgNV +BAYTAlBMMQowCAYDVQQIDAFBMQwwCgYDVQQKDANTQVAxFjAUBgNVBAMMDXRlc3Qt +b3RoZXItY2EwHhcNMjQwMzEyMTUyMjU0WhcNMjUwMzEyMTUyMjU0WjA/MQswCQYD +VQQGEwJQTDEKMAgGA1UECAwBQTEMMAoGA1UECgwDU0FQMRYwFAYDVQQDDA10ZXN0 +LW90aGVyLWNhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt170jy/u +NxrhlmmrDhgQ95Ir6ah07e1oeLptnzLI37KuoLIQInMLZx1QW8k+USyIVBNv4d2W +zL6c3s6tPbQuiwrPI3o9cF5p7xexJ3kFeboffoABOpoQcWIKZ6DIs8nGGD4wdRjF +6Ue0qDp6za/Rot+vARcrqTbvPmY/2PpTUoLiF0xwWULfyrkjOryUVnbYC48DH3/J +FJ5FmSUi4W5UVbBhONCNDBFkwZX+ASa4HnTUijjCbP+OVWzBQdObkhziRv03hEbm +qYhnioFcToJG4WGzEqwZ0/3DbYICuIo/ESKrS47xQ6KN28dxFr3gvLab1H5Gej2A +E3crfSFjXq5ERwIDAQABoxwwGjAYBgNVHREEETAPgg10ZXN0LW90aGVyLWNhMA0G +CSqGSIb3DQEBCwUAA4IBAQB1IdsS7wp6iaAwo4LMAlU36j9ofvmYruCHuj75Lkk6 +0AagjUhUj5VB05U3nMHVDJrj5lELTW/oQC4IfjfScOFYa6E+G61356bJGAwwwPwr +6QvYvkSDm6LDot6awER6En9mlEVeAfE7wuZVCkQ1I2BJ99+AI+KN5Z6ZMbjNJYxG +3JbGCAWqarJJC6GQy0iIutjpYyDt92kEyj1KelGsHz8IPOjHdZEnKljGm+HbigXd +I9yMns+nDcNNhWMm+e6Fpo1ja6fpIqCTZ8jDSTc6bPj+1SmyWjemUofFub4ri2lj +k2tW9JsrFKBMLK/gBEmi6lZXGBYe8QU7iusk6UFzTob4 +-----END CERTIFICATE----- diff --git a/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/invalid-ca/client.csr b/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/invalid-ca/client.csr new file mode 100644 index 00000000..ab1c6f1e --- /dev/null +++ b/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/invalid-ca/client.csr @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICrzCCAZcCAQAwPzELMAkGA1UEBhMCUEwxCjAIBgNVBAgMAUExDDAKBgNVBAoM +A1NBUDEWMBQGA1UEAwwNdGVzdC1vdGhlci1jYTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBALde9I8v7jca4ZZpqw4YEPeSK+modO3taHi6bZ8yyN+yrqCy +ECJzC2cdUFvJPlEsiFQTb+Hdlsy+nN7OrT20LosKzyN6PXBeae8XsSd5BXm6H36A +ATqaEHFiCmegyLPJxhg+MHUYxelHtKg6es2v0aLfrwEXK6k27z5mP9j6U1KC4hdM +cFlC38q5Izq8lFZ22AuPAx9/yRSeRZklIuFuVFWwYTjQjQwRZMGV/gEmuB501Io4 +wmz/jlVswUHTm5Ic4kb9N4RG5qmIZ4qBXE6CRuFhsxKsGdP9w22CAriKPxEiq0uO +8UOijdvHcRa94Ly2m9R+Rno9gBN3K30hY16uREcCAwEAAaArMCkGCSqGSIb3DQEJ +DjEcMBowGAYDVR0RBBEwD4INdGVzdC1vdGhlci1jYTANBgkqhkiG9w0BAQsFAAOC +AQEArM94Qcp0oadPD5Ci65tFN+odazV2gNktBnwIxQRByqzNdS81AtTpcjwtHPiF +t+2sujoIAP+GHisIB2kmObpl04qY+vVLBh8kNRFVrcdyY4qtsP94Lg0I8vZ5nT0O +2EJ+xdz2WimLII/fEcIAsQgycESMGCPRa2nAnxFjRKPt7LqHRoIaDMnKCFWXaG5W +cSktOaBYLoad0SWBnqMvynCncrN8S29hvtaIEhrn8IldFkwhxmfkoienlpORjQVW +vsJQzu+CB+cHngr1G9M543BONu6Rl3qWARpESaZSkNpsxyQG1xs0RpMg46mB6hP/ +teOzJAvLtIC/FAHq7fCCaSNDMA== +-----END CERTIFICATE REQUEST----- diff --git a/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/invalid-ca/client.key b/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/invalid-ca/client.key new file mode 100644 index 00000000..a046dc29 --- /dev/null +++ b/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/invalid-ca/client.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAt170jy/uNxrhlmmrDhgQ95Ir6ah07e1oeLptnzLI37KuoLIQ +InMLZx1QW8k+USyIVBNv4d2WzL6c3s6tPbQuiwrPI3o9cF5p7xexJ3kFeboffoAB +OpoQcWIKZ6DIs8nGGD4wdRjF6Ue0qDp6za/Rot+vARcrqTbvPmY/2PpTUoLiF0xw +WULfyrkjOryUVnbYC48DH3/JFJ5FmSUi4W5UVbBhONCNDBFkwZX+ASa4HnTUijjC +bP+OVWzBQdObkhziRv03hEbmqYhnioFcToJG4WGzEqwZ0/3DbYICuIo/ESKrS47x +Q6KN28dxFr3gvLab1H5Gej2AE3crfSFjXq5ERwIDAQABAoIBAF62IucaQJYhwkbo +STu8Xncg/qFvKwYBS6af1CMYHfy808mYbxD8DvWxhGIELbXLpJaYe39T7qHOCkRi +x8RJHokeiiKu7rDtcxXVTOEwdw2Kft3dy8Sy8q89jlY8C64hF7pJ1MmGhCKbsMn7 +epZmq4bOthuAFkMOZr/6HBw8H3FL2+ok6PBnIi/t8WsZZAyH1SiKpGPsT+Ccq513 +bRZYU0aEGz8oyADnE+rutnTsP2Y5oOQKeb7AmcirhE689mLgB2habfdKM8DhrtKe +sxNwC6f23DAz7XE9SG67QMEIa9+ImnP+VvnPq8+Ky0gEjllZvv+bbLzRcQFPmNgj +XlPevjECgYEA8FJg0deZwn6rycl6CJaQOOTELa0zeUyLJKI6nkLYNRvNL788Y7Rv +r042vhjdSAhrThIlRN41352wnMUXBijwkgpE6LmGaXHKDalF5h8Zd5z4QBRijwc7 +DXz7j7D0RDndOBBFSxxeerUVpO7QpITyKaXWVsnJlpksuzYRXwbdPHMCgYEAw1Vw +VnQbiOhIcynBXW+OCJ6DOOxHxoZ4ltof0PmZ772bADOP9oH5nMAlBVgPM7dnapCd +VMzcSGn60MAQ5myt2LZQxV0DxltSuEqOHCgn4eq+ZKVwW2cUxPGpeAi2HWNUj4bo +0rJuZiLJnS9axY7+51Wlf5tpao2xOw2CGwHCV90CgYEAzwh45m5RxU+xGP6cRgfH +qWvTYfJDVO1PNbkYvLyjXGVeCBM8qDyKtsCvwmbTQzoVj0Vsm/6+9Kz5uKTGKAVe +8sEsCj3CANcJlWlNkWkbXIN7DmFBYyx8gCs64Ng2JwyeeqzxtTp0XkvgoJ0oW4M4 +yA5ZL51ZMIc3FPUIVBAqyecCgYEAqkF3SDOtqFhmhdKYUzufvk2DrQLt0NF2nG4F +G13j2f4W80b+LWu+yO0Fl7HYlkg+4LiqGbbyLkAJuRgf4uhJY0IOBuj7GFKTOETr +twkdMiIsz4cP9utTRBIrl49oRRdGJk/98WBLL8UUnGghI19vOO4C8cXhTVmxZ/pv +M+EFpxUCgYARl0Ha9US3uZILMNtEkZ+O/q+TgDlZoQWAHuxnPG1K/Q0RKHFUxpQh +b3t7+aJLsREdIXNeX5vu8f+reKAx1Fsm0SGV4j6fBrzyA3x2zX9ktGM4ejvq+BxE +scZl6PX8eFgGdDVnZpdLVVFTfiLO6ov6itZb6kw3HPIhA4ew3xa+Ng== +-----END RSA PRIVATE KEY----- diff --git a/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/invalid-ca/server.crt b/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/invalid-ca/server.crt new file mode 100644 index 00000000..ad768280 --- /dev/null +++ b/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/invalid-ca/server.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDHTCCAgWgAwIBAgIJAI/Amrjs0rrbMA0GCSqGSIb3DQEBCwUAMD8xCzAJBgNV +BAYTAlBMMQowCAYDVQQIDAFBMQwwCgYDVQQKDANTQVAxFjAUBgNVBAMMDXRlc3Qt +b3RoZXItY2EwHhcNMjQwMzEyMTUyMjU0WhcNMjUwMzEyMTUyMjU0WjA/MQswCQYD +VQQGEwJQTDEKMAgGA1UECAwBQTEMMAoGA1UECgwDU0FQMRYwFAYDVQQDDA10ZXN0 +LW90aGVyLWNhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu9vEEyH8 +diuqze5Yi0yF1qyEwaTVyUx3KZ7QR+ykM+njyv2zZbtC+/aWLMWAp3cswigb8RG1 +1+CMtsTB0puvTeMFDiJIZGelBO+ks1ijIXnqLut3Y2VLOwydHZLx/6VjQmvAE/Cx +Cm4I/3unNy8VPRoKswFtEyJ1wSaidZfaElA9EMQtlIC4W5A4pdanhsAm0xu4uD3+ +mS5p6THsVPjXDKb6rYvrnvZ8P9tdR5HJKuvzbLBxzZiPkktN5vKcBuzN4XmU55yP +hd7LoMPINRAfKH0yFz7X13ob+GRb3i5ho8llxw8veHP62iygrrXi2k0DpF3rRA8x +OIRooHZVBM9KNwIDAQABoxwwGjAYBgNVHREEETAPgg10ZXN0LW90aGVyLWNhMA0G +CSqGSIb3DQEBCwUAA4IBAQBMxk6YQSi4+SD5X0mE8gLMM0K7S//QJFwV/OjhnmPq +RflyWD9lVQhrf0SC44A2HPGnLqDItc90QX+JDawXwEywicFuEHCobe8rR0udMhbz +cuLeZsOJs97GaAnrIcG3dU+gBLqBCLr2jnHcvh9r25sYQLQuk6oF5Xen6Tet6VBZ +/MpVCQYmY0k+XY+UJHoPJXkNpVz0OrV0QgYJm7+jnX+TEb9G+bAgxgYmTzNPknv7 +yrdOZH2wC9SbchCXvouuT49dhy8xyeP1ae4yZKEfCFKWP8MTzDKrnXpXoX9SpPDn +RR+GfYoqBNxBbwR/LqMj3654sYF96q0z4SOi3hLwLv2E +-----END CERTIFICATE----- diff --git a/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/invalid-ca/server.csr b/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/invalid-ca/server.csr new file mode 100644 index 00000000..39bc46ec --- /dev/null +++ b/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/invalid-ca/server.csr @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICrzCCAZcCAQAwPzELMAkGA1UEBhMCUEwxCjAIBgNVBAgMAUExDDAKBgNVBAoM +A1NBUDEWMBQGA1UEAwwNdGVzdC1vdGhlci1jYTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBALvbxBMh/HYrqs3uWItMhdashMGk1clMdyme0EfspDPp48r9 +s2W7Qvv2lizFgKd3LMIoG/ERtdfgjLbEwdKbr03jBQ4iSGRnpQTvpLNYoyF56i7r +d2NlSzsMnR2S8f+lY0JrwBPwsQpuCP97pzcvFT0aCrMBbRMidcEmonWX2hJQPRDE +LZSAuFuQOKXWp4bAJtMbuLg9/pkuaekx7FT41wym+q2L6572fD/bXUeRySrr82yw +cc2Yj5JLTebynAbszeF5lOecj4Xey6DDyDUQHyh9Mhc+19d6G/hkW94uYaPJZccP +L3hz+tosoK614tpNA6Rd60QPMTiEaKB2VQTPSjcCAwEAAaArMCkGCSqGSIb3DQEJ +DjEcMBowGAYDVR0RBBEwD4INdGVzdC1vdGhlci1jYTANBgkqhkiG9w0BAQsFAAOC +AQEAuOxhbvH0mImfIq64o5naN1iRXMfX8FL9q0Lo1gzpH2F5BA1dmt8/pIoyl3S1 +mR3/mbQJL3wGoJYJH85TEcxjl1WepyRCiEm8+KjPTuEnKkn9kJXbrgz1OOMAUT5H +6LPazPb4wcwY06N55oGbq9llvqSYvXKkrgWawTX8BvzaFKDUaPN8PcnYjqxvgLen +m2mmz5KJXLaNmLCpfzg0e+r5zd0EuYQ/bo7EpMyaJh99wR2V9Eb71WTZSqdB32lf +dNmN5j89bQtmWEMqywqUXeiFovD1NU63lzls0KxvAZPKfwMjtsPLDIn8cFwnE4M7 +ZJ2UZ9rimXqIA3jIJ3h4hiQp5Q== +-----END CERTIFICATE REQUEST----- diff --git a/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/invalid-ca/server.key b/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/invalid-ca/server.key new file mode 100644 index 00000000..64b883f2 --- /dev/null +++ b/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/invalid-ca/server.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAu9vEEyH8diuqze5Yi0yF1qyEwaTVyUx3KZ7QR+ykM+njyv2z +ZbtC+/aWLMWAp3cswigb8RG11+CMtsTB0puvTeMFDiJIZGelBO+ks1ijIXnqLut3 +Y2VLOwydHZLx/6VjQmvAE/CxCm4I/3unNy8VPRoKswFtEyJ1wSaidZfaElA9EMQt +lIC4W5A4pdanhsAm0xu4uD3+mS5p6THsVPjXDKb6rYvrnvZ8P9tdR5HJKuvzbLBx +zZiPkktN5vKcBuzN4XmU55yPhd7LoMPINRAfKH0yFz7X13ob+GRb3i5ho8llxw8v +eHP62iygrrXi2k0DpF3rRA8xOIRooHZVBM9KNwIDAQABAoIBAE/agSRo4/oPYdGb +qUO9SX8RYnU17jJdMKIeggawzrPKjivxX9q0mSqljPyHD8Mf44S8q/PzRUr4hpgC +VymBSClhgPqbFA6qB/lrLKWX3fAS9LrxGJTFsA7vs7GojvnOgbzwNHvFalw2ndiL +5W6NsweAFGA3EPh7Q3bRR2mZHPd/LF8MytZHPOJniGr76LhcW8wf7auYoZfqiP3W +8Bx218ZtnZgN7YVhmtFLk4MdkJCP16xE/u3fWAAfBok4uJz8aW37M75y/2m5ND16 +yHkXEjFxVibIP5p1wFROKrkW+qdYjKSWoTEoxjOYxFCXGnN0X0UrzCPDNc4wukbG +JjyhUEECgYEA9oC0CmBoI0kgS0nyPJB3NZ3PvfaICxYFA8OnJgM/WHB7cUUCjLUF +7uyF9R7uC/Usgzo01/01E3Rw8P0tdo4DRI6LS+WDUahcX7Kw/evYEhukdfqfV8pU +TamlTEzkmWGlv/3bkExX3rgcOhXDpmr8IlkMXdSGj0RCgeJS5JzcBDECgYEAwxik +/Uvb+fBXKci6rpgQjeJRiCZsqdzsVfz2SrVGQawXgOblZd4OeJqtTksSYhYKWT2r +YAEbzuZzET6wAxs+bMJuDteU0qLR5HPhhEgoO7pZFBvnEG39/WmJygwBy1yFM/kj ++7sBmFDnLXtObv14Ji6/dvsN2NbPOx/74NwJIucCgYEA29H8PACq/UR56wn/XekK +laKsnl+aBCDXyfqRNUHSHID6ZBFBa88GgoEkGGpDqCA0WLXwZ+hii33cNdvgzgdo +fJuNNtpuV8SG08RbN6U44zUJXThpHnXM5hDx2m/7r5g3olW0liKufWu7qES+W8qu +G8dDUu1yLZKWqJL2ju4e+hECgYEAjIt4cDDx64BEiolcUuKhKlVbvuVPap8Icnml +Q/SLBExhMi8kGtp5OlDTgI98UsTl95wBlUu3KLnDMy0hx1sdAWSfvHl5cLRy2EzS +rWT99ukrutzO/HsAekpJRRCZSlMAcoyMa5AwefXuVEl8G2Dl6TvMGn6JXyiAaWuF +HFvnCQkCgYAg4SoNszdMsQBf8QsQS2uR13B3EzRqOD0xq4W4MTHEi7lhJb3E6wVZ +6yTJoWPo1oWACIGhns3F6nLpBQIBfXi/DVDS/ea7bybjlzId3MznoAXGrnvrENns +JLq9ApJ8tCQPAptcU7vcwRNiVvzMdbl9hYGv35Mp5oPKRkow+q/qcQ== +-----END RSA PRIVATE KEY----- diff --git a/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/negative/ca.crt b/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/negative/ca.crt new file mode 100644 index 00000000..96708c18 --- /dev/null +++ b/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/negative/ca.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDLjCCAhYCCQD94yttBX1IeDANBgkqhkiG9w0BAQsFADBZMQswCQYDVQQGEwJQ +TDEKMAgGA1UECAwBQTEMMAoGA1UECgwDU0FQMTAwLgYDVQQDDCdtb2NrLWFwcGxp +Y2F0aW9uLnRlc3Quc3ZjLmNsdXN0ZXIubG9jYWwwHhcNMjQwMzEyMTUyMjUzWhcN +MjUwMzEyMTUyMjUzWjBZMQswCQYDVQQGEwJQTDEKMAgGA1UECAwBQTEMMAoGA1UE +CgwDU0FQMTAwLgYDVQQDDCdtb2NrLWFwcGxpY2F0aW9uLnRlc3Quc3ZjLmNsdXN0 +ZXIubG9jYWwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCg+bsH3/CN +kTRQrRfjeRBVadxx45i4C2KpvXHO2GjkZamjat63u9CgJdTHdoG0qXWdlFnoJ1kw +a3V2L31FiF70yl2ZfAyTMtCBEWG9c5PmJrhc5yczWz6N2uEVOK4u9KCuDaCoVpP4 +aimhM9BKSmhHz9iX18IYfXQH1L3ESSwi8Jk/Q7ZSwWxzt1k3iVZJfzwU3vNqckae +fwzxACftchQK9FGTFt4IkIQ95GDKOKpovOvD16naeNmGaOM3Tz6b730gIBfinAum +sAYr43APHWb5y1Sw1zPAujgV8lMM+IvQZvJkBcKerYeJ29XpCsuRAxr90R0C3jcY +K7vY0zDQT0mHAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAD4YK0Zirxv9AkoR+ik0 +CpP/h8eQg2pDhKpw4YTUpjPqmz3it5VqDkcAljFGt6Q1xCSSZ18iUFLd7gqtIO2i +Xsxfefme7xLjmfRWE1/sAtpbdASi6Z6kGbFpHY+rqHqi5ANfVeFWiUM0ibtkJ6yJ +yNCWZdgsQOXl6+PITpQKd3PZ/0j8m8iYHJjO6Ssq8PljUWZc2Af2t8oiK3FBwznU +hDLsxY2eP2A8mkEZ0wldT9ajhatoNMslPtwjVg8BG8oRrOD2QgqK5PfWG9HDk/4j +zrXcArdil7+Ran2TF3e/Ynnjj/v3PakHBNmV28KKZqKlJ9SjT78avdY1dDQn8kbb +Gp8= +-----END CERTIFICATE----- diff --git a/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/negative/ca.key b/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/negative/ca.key new file mode 100644 index 00000000..52c1488f --- /dev/null +++ b/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/negative/ca.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCg+bsH3/CNkTRQ +rRfjeRBVadxx45i4C2KpvXHO2GjkZamjat63u9CgJdTHdoG0qXWdlFnoJ1kwa3V2 +L31FiF70yl2ZfAyTMtCBEWG9c5PmJrhc5yczWz6N2uEVOK4u9KCuDaCoVpP4aimh +M9BKSmhHz9iX18IYfXQH1L3ESSwi8Jk/Q7ZSwWxzt1k3iVZJfzwU3vNqckaefwzx +ACftchQK9FGTFt4IkIQ95GDKOKpovOvD16naeNmGaOM3Tz6b730gIBfinAumsAYr +43APHWb5y1Sw1zPAujgV8lMM+IvQZvJkBcKerYeJ29XpCsuRAxr90R0C3jcYK7vY +0zDQT0mHAgMBAAECggEAd5wX1KoY25fg/3EeJu91q4GVQyqR926+SNFzFvbGOa8w +dTSbeHodcmGp0OvFRLAFrKjmhRF3u/qctMxkkJ3bsJgNJFaAIX/IXZ7EuTh/1KtA +ogc/oXFS8aSJSnNrOYibO7j0fyCVoid/9z/ArPLMuU8+6NRwbDILXSY+OvMD0JE3 +58xQPywykTwcfjcRj1Mxo7i5t0NMjRUjJnw2znitTFawlc6lK/0Gp5masDOfz9ts +zT16CeYK6VAhRnwGIHwI5CW0GovvYA6PWfIo4SX67D3Fq8zduoN8ZDPY7bwWTFdh +npgBCVdmPzFJSUHZ4NzHC1ttNkPRicGyKkgachPRQQKBgQDS4OHRNwVoIyXX+Brh +31hffXY6c9MLJXCkifFvTLABbHVNGpD5auniUFAhKT9xO9iyFKWj4F5Ue+7MrZng +I/l1Ya4fitY4awufyJoS7YjOuKYhzvL6O6sa2cSgpcU6+nT3YpvO//mjiR1aMbwW +wS/C874SxdXMt/iyak0MeeqfDQKBgQDDa1sU3dhwKXZj4T9aLfxLiwgsaNJv//c7 +uBq4vXGOXbUpKauk+FBcLGkg1W1iX21DmzTYTngc6Vy4AF1yK2f/P1tXmaz1nU8c +nhL4LWeHzG8VI9LDV+I0CBGeNSBedbs7sxhyilhLAeyTNmMA1hleeSzxqxcYKtqy +ZSzqHo0F4wKBgQDSYTQoKwIj4FzS11zKVq2tplca/Y5golt8a3oIlbNJ2FA2OfjE +PBtVgtZOHv6CEziegOa3VRIGqxWT8OWAraMjre2u3i3VX0XbhJ/hnkRMJ/7l37ac +WobbZMI7muXnbxLd8uyKWOlOc25rGw8QjG7/yXeo9uHTOP7N0CtJ9R9SyQKBgBAQ +53ATvROblQwpHJhBZ5ieWZGtHH/wv1a9kBTYHlniAl7b+iyZ7aFmVU5JvbB0v/rq +67FM4jseRG0sOoKEZwxpHQ1aqQmYYUStCko1EWnsuMU4KL++ne5BK5GiNIMPktEZ +rEzeatvf0J9ZvVH4SCWoOLW1pzRpcYlxH/wvftCZAoGAK6NpC24Ymz1eSAGyYEkv +dhrpRT7hislwZW2wEXGxI9gk19gjR3/BcdnZwm91ei2X0BH+1l7BQCoZaCpqwg4x +Xqgue0rbkOrdSzxyfWe1TrmMB7BoKsvHcv1m627XCGjgYHaufzCtckLSaJSdhrPn +V+ZeDk2NYB7YY3VJ7iH41JM= +-----END PRIVATE KEY----- diff --git a/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/negative/client.crt b/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/negative/client.crt new file mode 100644 index 00000000..9b80ac0e --- /dev/null +++ b/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/negative/client.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDazCCAlOgAwIBAgIJAI/Amrjs0rraMA0GCSqGSIb3DQEBCwUAMFkxCzAJBgNV +BAYTAlBMMQowCAYDVQQIDAFBMQwwCgYDVQQKDANTQVAxMDAuBgNVBAMMJ21vY2st +YXBwbGljYXRpb24udGVzdC5zdmMuY2x1c3Rlci5sb2NhbDAeFw0yNDAzMTIxNTIy +NTNaFw0yNTAzMTIxNTIyNTNaMFkxCzAJBgNVBAYTAlBMMQowCAYDVQQIDAFBMQww +CgYDVQQKDANTQVAxMDAuBgNVBAMMJ21vY2stYXBwbGljYXRpb24udGVzdC5zdmMu +Y2x1c3Rlci5sb2NhbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMRz +PU23risX4lPK1y+iyqvJvq+FjfeDruGKKAMZr13HqMxX4EXatpMhcMUu+kVUKEeC +gGXY9b5kAqExh8Y4N13NzUtlGh+2dkc8yi59wLuYZGnruJqPN2ABgHh6K+M0A7Ie +Om/67Poi+XMoVsyAvYIs0DPuSbm4DsFrwoV2cm06QHSmqRZU4uk96jwL1dKEyrA2 +Nm4yfPQX4bKwGWvxe+XdGn5HUuMM7guDf5Uf4PMDTUglbkttpZMRWy23HfnnzGDu +iM/v6gjN//9FwLDP/35QMsJSgEyQEeXZohhOAKy8OPvVATPDNkJh0GwOP1wR3Q+V +zbgw5W4A0LsOVPgg4XsCAwEAAaM2MDQwMgYDVR0RBCswKYInbW9jay1hcHBsaWNh +dGlvbi50ZXN0LnN2Yy5jbHVzdGVyLmxvY2FsMA0GCSqGSIb3DQEBCwUAA4IBAQAY +Qg1K33Lr0r3o270uDKhQQz/b3myHM7+YXKjGDMgsmjJKAQ/0zvy/35fpwW/gFxzs +NRDcGNQO3pPimQ+muJfBC30Z40vKSdGCaj7NSChI92xY2Lp9F4Zl3lyJ42SvJXUl +ipMNehpy3YyGimx8hwg+ctnUMrnFcorp2LBYbbSSIt49Zdh5l9RAQLZ5CbA+8FBS +K/p3tEYHesWQ7AZGQsMZJy5OffPOQ73tFSVGdjEdth/mHnpxUHKdD3caLVU/jh1o +017JV1cU7QGyfcXfJ+OG2SHrUObDbIrQzngUgsEm3+5+auBwmeXz2LULfTVWJXJ+ +8QxvIcRz0dvxR3LESlGu +-----END CERTIFICATE----- diff --git a/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/negative/client.csr b/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/negative/client.csr new file mode 100644 index 00000000..dc663a33 --- /dev/null +++ b/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/negative/client.csr @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIC4zCCAcsCAQAwWTELMAkGA1UEBhMCUEwxCjAIBgNVBAgMAUExDDAKBgNVBAoM +A1NBUDEwMC4GA1UEAwwnbW9jay1hcHBsaWNhdGlvbi50ZXN0LnN2Yy5jbHVzdGVy +LmxvY2FsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxHM9TbeuKxfi +U8rXL6LKq8m+r4WN94Ou4YooAxmvXceozFfgRdq2kyFwxS76RVQoR4KAZdj1vmQC +oTGHxjg3Xc3NS2UaH7Z2RzzKLn3Au5hkaeu4mo83YAGAeHor4zQDsh46b/rs+iL5 +cyhWzIC9gizQM+5JubgOwWvChXZybTpAdKapFlTi6T3qPAvV0oTKsDY2bjJ89Bfh +srAZa/F75d0afkdS4wzuC4N/lR/g8wNNSCVuS22lkxFbLbcd+efMYO6Iz+/qCM3/ +/0XAsM//flAywlKATJAR5dmiGE4ArLw4+9UBM8M2QmHQbA4/XBHdD5XNuDDlbgDQ +uw5U+CDhewIDAQABoEUwQwYJKoZIhvcNAQkOMTYwNDAyBgNVHREEKzApgidtb2Nr +LWFwcGxpY2F0aW9uLnRlc3Quc3ZjLmNsdXN0ZXIubG9jYWwwDQYJKoZIhvcNAQEL +BQADggEBAHg9BUcZPXpoqjXfYC8FF6WJS2VdJGJ21J0xFSoH9O9+cj3cn4OrT6Ej +j2GoxiD/DhhyOyiecdhMiKheiHiIIXFLaQCCrvbWrUgPZ/T3GfZdF0aTVcMqcast +2RpDj5jQ90nSZR83/JDeIHlgL6N5OKijnGzrovwAlgIday/NQziP4OeimewQjuZO +O2pgGu3h34fuRu2NEOFaxX/pVKyAhPZA0x3vc6Dzr1ddjxaCf2xa0Kn7IfHB2ZzA +XeUXjf3HoIn1j8QbK/y2fXG1eBIpHD8WiyIvFII0mbvP2WAHj2pFzy5Gp6PRsjET +a/HvfYykrcOZw1sQE8IOVVxPvdx7WYc= +-----END CERTIFICATE REQUEST----- diff --git a/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/negative/client.key b/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/negative/client.key new file mode 100644 index 00000000..0e064134 --- /dev/null +++ b/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/negative/client.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAxHM9TbeuKxfiU8rXL6LKq8m+r4WN94Ou4YooAxmvXceozFfg +Rdq2kyFwxS76RVQoR4KAZdj1vmQCoTGHxjg3Xc3NS2UaH7Z2RzzKLn3Au5hkaeu4 +mo83YAGAeHor4zQDsh46b/rs+iL5cyhWzIC9gizQM+5JubgOwWvChXZybTpAdKap +FlTi6T3qPAvV0oTKsDY2bjJ89BfhsrAZa/F75d0afkdS4wzuC4N/lR/g8wNNSCVu +S22lkxFbLbcd+efMYO6Iz+/qCM3//0XAsM//flAywlKATJAR5dmiGE4ArLw4+9UB +M8M2QmHQbA4/XBHdD5XNuDDlbgDQuw5U+CDhewIDAQABAoIBAAJJgS41dD6mMYle +NDEmyQtE9wZeHLAEBXY0wJCArQz/dRSj9UV67WM7IW/6QwmpmCp093+4Dexgh7NO +u1DweJyL99bn32z9F9VufMAb0LGebZTaHLUX88IXYmKEsZwcj+pz9aQ2HKow3Aye +LJyG5y3rzaS9IniaDvnrgkFBhsWEwdw89QpDnPvMmms+WpDCLxk4qP9ev9EZzYVM +oPRmFz9AGNUlsO3cLMuu9xTXCoYIWYpzunNo2Mw+ro74p8y/1MJ89aCfm/dEwoQk +nwHhC5QIwGJsrtcKFGGd6rXe/qkvO7jqxTSI5nZAHzVkYmk98wiVszKZ5KyxKIgp +QcWNdskCgYEA8wxwJqgJza/K44aqCyxobS8YqsYMn7s98sW1r5u3Hz3bZV8yw4RW +zWZ4IIroI9peIQfMCwAw+qZJnGzdduN8sFUi75j0fbQ6tV23qjHtzqFhsvK1lEsO +lRa0P8SNklXAf560TTkMYUp704Op3AcpKmxN35MaYe/XCVoH6X4DXvUCgYEAzusg +RaC8vYQSqNCinE9LuKESEIL9ihBhnAS/8rIEIHvwO5UBvtQbkcL8WhKQh2OCFix2 +nEkfvlqrSsfTBOWOgO4HSCjPm63qQ0hC4abK4v8LNpvrrgc8kLTgKFqPmjuBoHwq +ZFv6m4tUjgO0pIwmAU4mHa/2NiCcHX/2rEu8GK8CgYAPw9/GciHoqJ11cre279N6 +OZLVCPGqrr+O7sohMO2I5j9D7Q/i5MOooRvrqHb0VGbEp7fRgtqqd3zQ27Ll0k21 +NmCEwBwjxzwDpaeTL5foTkmDDQFANDom64kXlc1FD7Dj1kyFscyexvEPQDwVXJWL +/ehzNxx/+8mr/p4CxDy9vQKBgBqHBmIm00uwrPu7k71aZSjMbZZ0VLDonLr12O3y +aJkJiqj413pxkv9C8jtR+fmBhmH1XVd4AHvU//TcXW9ZRsW8vm4/3S3mRAxQLVLk +oUoszHE52CM9hkQ/DCXgRlzNmDbM9FpdeGmMmVCDpRsbZJvyOuy/bF9QGvOlPiik +pP69AoGBAN2N53pv9xLsZYxTx5emHN93UlJt+x8OhHTjI3FZ+mJMOXNTT2s5Sgms +iizlpCXpBuZC67uuPiTN/FmoCrZ1/GSOCovDMwQXZLf+5xlqCk+Xu/AnVsP2UGkl +N9nues9C9FziXZvS5G2kEEN8XHcJcsKgwgcpwgMsQIuKb81MM584 +-----END RSA PRIVATE KEY----- diff --git a/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/negative/server.crt b/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/negative/server.crt new file mode 100644 index 00000000..44c8e7bf --- /dev/null +++ b/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/negative/server.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDazCCAlOgAwIBAgIJAI/Amrjs0rrZMA0GCSqGSIb3DQEBCwUAMFkxCzAJBgNV +BAYTAlBMMQowCAYDVQQIDAFBMQwwCgYDVQQKDANTQVAxMDAuBgNVBAMMJ21vY2st +YXBwbGljYXRpb24udGVzdC5zdmMuY2x1c3Rlci5sb2NhbDAeFw0yNDAzMTIxNTIy +NTNaFw0yNTAzMTIxNTIyNTNaMFkxCzAJBgNVBAYTAlBMMQowCAYDVQQIDAFBMQww +CgYDVQQKDANTQVAxMDAuBgNVBAMMJ21vY2stYXBwbGljYXRpb24udGVzdC5zdmMu +Y2x1c3Rlci5sb2NhbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMOa +feprYLf12H0x0dn1IcDhGNwDX2H/8P8ubNwYh0aZafY4YhIIa03agCpiUn7Zk/LG +joiEFWWnlFjn9jSyxTiRwU5zQTXvnTPZD59ZtoOylq20Zi/2M2eG9Mw6zvQG8G3e +iHEHvNdaVZfUMXeLFMD0wU3Xpc56NLM+1+Yz0EdNhAjU6Eo5FIb++xCUWBdQCX9A +Bt7/TeUkjUGSsgiNwfklegUn+UkM+Bxu0XNXQ7hZ+jGTHmWLQEgQhPU8WWAKxeMt +VO0nb5iI6pM5YuIIgkHLg+0nFOGkk82fEeUkYYSWgVT9f4oLZquN/ybPEzv/cMaW +bziyH5PUCxRsfp9MCjkCAwEAAaM2MDQwMgYDVR0RBCswKYInbW9jay1hcHBsaWNh +dGlvbi50ZXN0LnN2Yy5jbHVzdGVyLmxvY2FsMA0GCSqGSIb3DQEBCwUAA4IBAQBJ +OZkFgDtwXiNKKMUOQIJCQFp0zTYqvaC+yMBnSO34xL7rdzmj5H2TBUYqSwGEbrGG +j8SEmT7ZdeYFG4QQ/DtFfbzzrzn8GEsQUFJZvcOpQijczCF8opOHAPgS0JJecEfv +jdYlvOysluon6gq1BBhfDaFPnC27JkvdkSI35Dm+L5e11zAC+cwyLmIqS4Fp+hUG +/Tx+Cgz9mBLheThvgksWHXuDyC5KXgMibVH20ONkv8XqrCb8S2Vb6pCYSRnoY6C4 +HdWvRs0EkcKNN5JNmZdLyGNvbE0acIBeZszDoN/2M42pkP0oUvJG2JoX0S3JlfOL +iyoN7vgnaH0Wx9fQ8Byf +-----END CERTIFICATE----- diff --git a/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/negative/server.csr b/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/negative/server.csr new file mode 100644 index 00000000..984afbc6 --- /dev/null +++ b/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/negative/server.csr @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIC4zCCAcsCAQAwWTELMAkGA1UEBhMCUEwxCjAIBgNVBAgMAUExDDAKBgNVBAoM +A1NBUDEwMC4GA1UEAwwnbW9jay1hcHBsaWNhdGlvbi50ZXN0LnN2Yy5jbHVzdGVy +LmxvY2FsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw5p96mtgt/XY +fTHR2fUhwOEY3ANfYf/w/y5s3BiHRplp9jhiEghrTdqAKmJSftmT8saOiIQVZaeU +WOf2NLLFOJHBTnNBNe+dM9kPn1m2g7KWrbRmL/YzZ4b0zDrO9Abwbd6IcQe811pV +l9Qxd4sUwPTBTdelzno0sz7X5jPQR02ECNToSjkUhv77EJRYF1AJf0AG3v9N5SSN +QZKyCI3B+SV6BSf5SQz4HG7Rc1dDuFn6MZMeZYtASBCE9TxZYArF4y1U7SdvmIjq +kzli4giCQcuD7ScU4aSTzZ8R5SRhhJaBVP1/igtmq43/Js8TO/9wxpZvOLIfk9QL +FGx+n0wKOQIDAQABoEUwQwYJKoZIhvcNAQkOMTYwNDAyBgNVHREEKzApgidtb2Nr +LWFwcGxpY2F0aW9uLnRlc3Quc3ZjLmNsdXN0ZXIubG9jYWwwDQYJKoZIhvcNAQEL +BQADggEBAD3ZDHYdTUkj4C0JsKyM8DXrPmJ4zJbJ8etwUNTA34cxevxB50MFaNw/ +E8OIYPrt6WJ7nh8pK0brfi/5TpYHgnqBXN2G1aFVsL9QB3Vkk+9Wp9TPnW9uyBx7 +pdnfIoF4iEdj6nv/8enMUTXsBBDPlhl2Hh3no8M8S5HsbsCJAWkchm1yDWli7KeK +dI9CpYx4HAK78NCApRERbY0TK53Be0SIZFxIJKm/Uxk9V0gqnBCWcUqUCDLfpkum +rn1Lh0KhI8t7JSQ/8L7rtZOe5xmq+T2D9leBYdi8XJMIMYmbZihY8Yti4XiDB1ac +WQPj/1ooX3BfOVxj92vWpmEXQ3Ie93w= +-----END CERTIFICATE REQUEST----- diff --git a/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/negative/server.key b/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/negative/server.key new file mode 100644 index 00000000..2a0a7b18 --- /dev/null +++ b/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/negative/server.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAw5p96mtgt/XYfTHR2fUhwOEY3ANfYf/w/y5s3BiHRplp9jhi +EghrTdqAKmJSftmT8saOiIQVZaeUWOf2NLLFOJHBTnNBNe+dM9kPn1m2g7KWrbRm +L/YzZ4b0zDrO9Abwbd6IcQe811pVl9Qxd4sUwPTBTdelzno0sz7X5jPQR02ECNTo +SjkUhv77EJRYF1AJf0AG3v9N5SSNQZKyCI3B+SV6BSf5SQz4HG7Rc1dDuFn6MZMe +ZYtASBCE9TxZYArF4y1U7SdvmIjqkzli4giCQcuD7ScU4aSTzZ8R5SRhhJaBVP1/ +igtmq43/Js8TO/9wxpZvOLIfk9QLFGx+n0wKOQIDAQABAoIBABThZRelJsn8gIO8 +0b4GoPfKD7FM2t3HSJ61AgHszGQI9HrIQg/SvkGtVYkwvcW0zEpaT9Ta5L3ZScjD +2lB91PfY9128h/WOAqYKQdSs6wLcCaG5ZD8ydQJUMcWrcXQzWW8hFkean9oNVp3C +lRVBz5FZj1kT8Cs+eGm6B6oXVeCGggWtfoME7S6Fi35x9gJqPRSOkHskf6J0yOsw +UOz+oRturIVQk4D3bT5pl3fmmjuQHl496lBT24HIdIuLOJNEUJH8YEeOZkcd+XfX +M4w+LEpe0FxwEhK6E1YnNMg2i0WTJmohNV+ckzHn0dI/sVYvNQPeCAkxhVZS/Ncd +EZUwx9ECgYEA6L/U/7m+u+m6Vkd307e9thnzf0Y7cp/xBwvdMpABOJnQqiw9+PLD +U9uC22+AJwAjffIYyvyIf9+wLsFVmU8zxCNbvN1PFbwAb5qDxIu/nZOIrfvdK9YY +O1j653FTGzZ849XXzpAza2dVXVQKqBIIBr4eEioOf7mrEslO6uDtZRMCgYEA1yS3 +vc5ELqBjdDD6+1ucTB8N/fJ0IUXq8qujMpQ6o6S359ow9eNtRl0RN2Myg9SrN6kl +aUiVa3QWVcN/jjWLx2j8sKeIOEmRriJmdH7pllpsmO8TzuOVfcV9kDNLEqHhjl+x +DZkFsUJavwWAhLW6s0BrDCSvGFHg1yMWO7FnGQMCgYEA5JDWdJeNJm6eTfJ7S1AK +ntUXWaq34JYPFdNh0zC18kajMyqlZV/J0AUmmaYC3Mn3EMz56gVbavBZzWCRVjAA +byImCe/vpTFt4CuGMTLn5rAmrm1DwrPKMiXsp/KTIYs91GSBPNpBlLzyiOdqW6jx +duWnFEF24ZmM1bMZq/FdUD8CgYBqUBGf8IqOw+EBbKbJiPaOQxZF9AZg3s0AB8LI +XqkvblkWcCKbjzvTdm/of9NZg8Dr62C6SwzTIXVcAhRJMLqW2pFrtg3BStF7TDJ2 +xLQnAR4C4LlYFewsT2gB1ub1GRt9oFm8j25ZrqFrHYvpiGfu5hk/0ezYRrWleggl +jT+WAwKBgC1lA/3bSiEv2gJmhQ2am96NpD/KoOzOdTuMTqbCak58dpLPjLI1rHLS +RIUvw1zr5r8I/KkSND9gzn8GaX1XwgoiMr2sTbPxD213gckQLxkbN3x9P6fFrDnO +zrfOp24yAbID5R+0HYva8kjzGec1S3B906Fdprdr1cJy/Ggx6627 +-----END RSA PRIVATE KEY----- diff --git a/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/positive/ca.crt b/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/positive/ca.crt new file mode 100644 index 00000000..7d97b10b --- /dev/null +++ b/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/positive/ca.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDLjCCAhYCCQDvMB3pYXKWyzANBgkqhkiG9w0BAQsFADBZMQswCQYDVQQGEwJQ +TDEKMAgGA1UECAwBQTEMMAoGA1UECgwDU0FQMTAwLgYDVQQDDCdtb2NrLWFwcGxp +Y2F0aW9uLnRlc3Quc3ZjLmNsdXN0ZXIubG9jYWwwHhcNMjQwMzEyMTUyMjUzWhcN +MjUwMzEyMTUyMjUzWjBZMQswCQYDVQQGEwJQTDEKMAgGA1UECAwBQTEMMAoGA1UE +CgwDU0FQMTAwLgYDVQQDDCdtb2NrLWFwcGxpY2F0aW9uLnRlc3Quc3ZjLmNsdXN0 +ZXIubG9jYWwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDmDhoFvRxJ +JCrmIGxfHVs2DuNWX1hA0oC3/0N+jEDOvzIOeQ+zT5DfDMON6poSvKWgQHVAufk/ +u+FL3r3r9aIvq4UBzv8sRCDGWngilXNFY4VVkD9MTHCDSlA+BN8eJ5LRE8Q7/rVT ++uwHsjz8gSrWV6Ep8w5RjF9Uz9C6J63craInrjESf+BhlHrlfTDGP1WQbnRBA7d0 +BL+AyzEWnfOPj8ybzfUERhHrY+FIQV5ejxsvmBu+PyPz/hpCL9Yd1HIcDi6dwwdH +XM//wnQ3OxvmXYO7ODT05qs1Dv1dl7NswwMjz/D74CQyzcTGhk6joVGX6qC5pv4U +q7WVKByP54T9AgMBAAEwDQYJKoZIhvcNAQELBQADggEBALoWg/ijhLlALcXpim8M +5+kBXYu/bw7ZRrujAxqgUP7tOivKb+MYD+uy87o03ltNxqkJAUyWW2foslpYEbnb +1T8aPu9RGGqt/QYFyCbxa4g0+9SN5SG0g7UhuuVTmiAyohiiTta+Ov12CVDpY3yq +xb9eXw+cGWOx8DruBdWmjL1dah+6YVkxbh92jV79RljLpLlihQM4h42SEcTZdTBH +DFJqYIz0ChXYqeNn9/AQmKns4SNv6M318r7WKfOKncAKUJA49fGRmy6wNCW+dOMj +VNGiMy7N3ysrRJheuN92zjEUwvKDOG8gzKsrUBwyrjPt/1n032Y+5Ubl7+HsBaIO +2D8= +-----END CERTIFICATE----- diff --git a/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/positive/ca.key b/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/positive/ca.key new file mode 100644 index 00000000..27d133af --- /dev/null +++ b/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/positive/ca.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDmDhoFvRxJJCrm +IGxfHVs2DuNWX1hA0oC3/0N+jEDOvzIOeQ+zT5DfDMON6poSvKWgQHVAufk/u+FL +3r3r9aIvq4UBzv8sRCDGWngilXNFY4VVkD9MTHCDSlA+BN8eJ5LRE8Q7/rVT+uwH +sjz8gSrWV6Ep8w5RjF9Uz9C6J63craInrjESf+BhlHrlfTDGP1WQbnRBA7d0BL+A +yzEWnfOPj8ybzfUERhHrY+FIQV5ejxsvmBu+PyPz/hpCL9Yd1HIcDi6dwwdHXM// +wnQ3OxvmXYO7ODT05qs1Dv1dl7NswwMjz/D74CQyzcTGhk6joVGX6qC5pv4Uq7WV +KByP54T9AgMBAAECggEBAI9JizW10s9PgpSw7y7SxwPFhB1A52QAeIGhsPU6AAeF +mHThPEEvtojml0pmK66t5u8IFr/I1ZC3wZyS0HIOHQVZ7E7zEYYNrOg+YwMPWKuI +T/y6CmLIXW+4sn/eYuWSOmSUzi2b+G6lI6urfUa8YOT90XiiVXG/X4Ugpt27ZxPB +t2WAYtCtziN5UXJbvirfeZaZVM3feqExSC2p+iKTgpkxiOaNh3xWxgG7x8P59HTz +vy88fSe1eW+RgBHTiUZVNIlBTXMNb4E/L13YNYvgOIH0TJ/pMvlc2RmVsoeFBAFz +pxjz8zLH5HlItAQ8rCUHa+ox6Xv62Zwd8L9rBMnMYzUCgYEA/M5QbjJt5gZi2rkl +Bfr7WOao3xNfpiBxffZKlJ037BcB3zi/JYEVRkwrHBFfiJrZXqZlM1zsryTddfmp +Bl2cDKpUTtyMeorgIzjJSqfHlUMGyzoI9PJFje0p5TiCawtQXiqehuyDVePsNTl8 +FU/+i8BuD9p7agTg/yvi+N3FWkMCgYEA6PYzhSTD7EMgfGVVSQNPjIOfSsoovwuX +KTBUGMRyVTv1HPgMnx8oOerO6+/LoxwE1TiUYnSVn8Tq8lf+rGrkbtx9BDJcC0lR +Ryb4CYsZqgimthHnEJS7wNDq+Fcbsf3E8wabj1vc4Fzo1xXGa10CIFyrz9IEQHYs +DgKxhHlPz78CgYBf06y1OLRjvwP1uLyJ+csQtc5JUMSu6hlbD+LRVo7+FPKGtLHv ++3AfB3xH1WYLF/dRY210/MJS2XyA3bPuT8l5G499nSg6wy2W7E2Q2OxUbeXDXypJ +/xPSapW456S4Ar/iEfGpXzmhcxX0Tuf0BDCOtNCDfePOGi9XSkFs6FOIlQKBgHeb +cHg7mBwFmvhDTrZd7MnIClDr2l+8I7ASEBtnQQxh7EcjU9eet5iE5hhc1cC48gJH +OmgSU3/kKnyikS8U8pO4wLcW5AsnaYOOjmrX8CVMq8tvBaONuZgVq441qxKHqEbe +bZ/9GjpXeXR0yZr19dGHwu7AdU5jXdsTpvDNGB0hAoGAaFMXW9oZiUvIJRkJl+ju +J2jvfBVxHzYnqXma8XrR3FaudSGze8NirngZCMeV7ZUPgO/iIfWjm0GjhCfPSOM0 +uBwiZ5K57LAOEqJn9gDb9cw9M9n8BfnwMcIpdUqhWPd06YkT8c0JLFl7Lgflk3S0 +m7i+/XRohEqwXruPbc0pMSk= +-----END PRIVATE KEY----- diff --git a/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/positive/client.crt b/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/positive/client.crt new file mode 100644 index 00000000..873557d3 --- /dev/null +++ b/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/positive/client.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDazCCAlOgAwIBAgIJAI/Amrjs0rrYMA0GCSqGSIb3DQEBCwUAMFkxCzAJBgNV +BAYTAlBMMQowCAYDVQQIDAFBMQwwCgYDVQQKDANTQVAxMDAuBgNVBAMMJ21vY2st +YXBwbGljYXRpb24udGVzdC5zdmMuY2x1c3Rlci5sb2NhbDAeFw0yNDAzMTIxNTIy +NTNaFw0yNTAzMTIxNTIyNTNaMFkxCzAJBgNVBAYTAlBMMQowCAYDVQQIDAFBMQww +CgYDVQQKDANTQVAxMDAuBgNVBAMMJ21vY2stYXBwbGljYXRpb24udGVzdC5zdmMu +Y2x1c3Rlci5sb2NhbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMoK +G2sHAjqWymzAeHaXXOxjBU998TPrZWdWdk6BjyCPDGhOyTollAYbS74JzivpOcPB +RcUYKJAv6oZN488KeWLgD4iIg6q0Qyxp2I8xBuPXrKCXQR9BVuwr+7elgRgaJMbM +AA9mzvqZBlPD1uUeEu+WRJb2ESnev0rfZLIJVNKIuWGItKGRCikze9GxSF7XhMlQ +IIXyL+bBLPUg3J8YiMICmTPb3/j9JXa/2UHwNIDqcJXqmxItUKMIknQWLiTIZGPd +SE5U2o8KHXN7yN4QtW5/nT8KFa4eBlmKsz5RiIp/dy6pEiv0bvUdH15sfVBPc1ee +Q/RAteeEUvBK7swj/EUCAwEAAaM2MDQwMgYDVR0RBCswKYInbW9jay1hcHBsaWNh +dGlvbi50ZXN0LnN2Yy5jbHVzdGVyLmxvY2FsMA0GCSqGSIb3DQEBCwUAA4IBAQAJ +gVTkc4YGcizgsXprGRZL1G3w4mr/GeJ4piZGupch2hfVupTnxyinOVIJjWK6yLZq +PKp7Z+q9/MNrADuFQsPWzk+4RP1PstGSMAJSUcR4EeIhd55Ai82j+jBE9NzvWmDv +UZs3VxS4DDllmbkyQkzFMapTqrBeyHFv759XcG/TvJaqUBPuubLtIHAXnSXYxc73 +yCTFnfdZrnKJ/Gc2iRaHNduB92URMX6PwTiuHby8nB3Nb1wRx2T5Xz2raYA0pogn +ec7BxrooC78gmEu0VlQv/jXtQOPWZ5l+1Cg2Y9VpyWFFYntwwyy6IptktBAWqF2e +Iwr9ftEj4BzV0IN/mBZ0 +-----END CERTIFICATE----- diff --git a/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/positive/client.csr b/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/positive/client.csr new file mode 100644 index 00000000..f5905106 --- /dev/null +++ b/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/positive/client.csr @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIC4zCCAcsCAQAwWTELMAkGA1UEBhMCUEwxCjAIBgNVBAgMAUExDDAKBgNVBAoM +A1NBUDEwMC4GA1UEAwwnbW9jay1hcHBsaWNhdGlvbi50ZXN0LnN2Yy5jbHVzdGVy +LmxvY2FsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAygobawcCOpbK +bMB4dpdc7GMFT33xM+tlZ1Z2ToGPII8MaE7JOiWUBhtLvgnOK+k5w8FFxRgokC/q +hk3jzwp5YuAPiIiDqrRDLGnYjzEG49esoJdBH0FW7Cv7t6WBGBokxswAD2bO+pkG +U8PW5R4S75ZElvYRKd6/St9ksglU0oi5YYi0oZEKKTN70bFIXteEyVAghfIv5sEs +9SDcnxiIwgKZM9vf+P0ldr/ZQfA0gOpwleqbEi1QowiSdBYuJMhkY91ITlTajwod +c3vI3hC1bn+dPwoVrh4GWYqzPlGIin93LqkSK/Ru9R0fXmx9UE9zV55D9EC154RS +8EruzCP8RQIDAQABoEUwQwYJKoZIhvcNAQkOMTYwNDAyBgNVHREEKzApgidtb2Nr +LWFwcGxpY2F0aW9uLnRlc3Quc3ZjLmNsdXN0ZXIubG9jYWwwDQYJKoZIhvcNAQEL +BQADggEBAB2hcSh1vflduY86Ynd0CAtqQJfxZDCCXU+aaldqpcj2cCFGAt/2nha/ +oIxIjBn/wcaaA4yVnEsrwvsA0mGLBfgMIVygwrasWdqPA3gvJAAo7I5wOHF1y2Bx +hYepliFnzSh2ajEETl3eIbvK4REDy/CJDmKhlAH6jk1oWp0ATWjo53baDzz5AkEY +W7MhO8mxEuCiKRkDPz7C6rSYI/an7HDY0OY0D577yy6Gpd8X3OTCikjYmmOu5/R4 +RJKNTUWK08hzkz869dqKp1lXf54ZJ/eHfSIX0vL/7wL6iOghy/U8/eSWPuL5Y9Bv +NtkSiX29PmGUvvgQ2jzWzOwPJaYT7s4= +-----END CERTIFICATE REQUEST----- diff --git a/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/positive/client.key b/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/positive/client.key new file mode 100644 index 00000000..8b98c9a0 --- /dev/null +++ b/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/positive/client.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAygobawcCOpbKbMB4dpdc7GMFT33xM+tlZ1Z2ToGPII8MaE7J +OiWUBhtLvgnOK+k5w8FFxRgokC/qhk3jzwp5YuAPiIiDqrRDLGnYjzEG49esoJdB +H0FW7Cv7t6WBGBokxswAD2bO+pkGU8PW5R4S75ZElvYRKd6/St9ksglU0oi5YYi0 +oZEKKTN70bFIXteEyVAghfIv5sEs9SDcnxiIwgKZM9vf+P0ldr/ZQfA0gOpwleqb +Ei1QowiSdBYuJMhkY91ITlTajwodc3vI3hC1bn+dPwoVrh4GWYqzPlGIin93LqkS +K/Ru9R0fXmx9UE9zV55D9EC154RS8EruzCP8RQIDAQABAoIBAHfqe8+Qf2Aq88aM +jnNE76BWPWarB6ibRLqK9PkvqLXYcbLPYFwkxbDCLriCtV4WtXRcmH6dEiZSak6A +mH/gZZ+sAUw2Sn/dMimAQUrr/HzrG8jNPZfBfkf66xJbJz4Y9k8P8dEyYhMXFExP +ZpLiwLZ3aAp5zkIdtUhJQ0jwhOnXsc41cojeJwrVDivonFIQaYbS3ix5WcRuYxB+ +KoeZNt4Y3XUKu25SEt5zdGIaKYMqW859/anN3Cog2dEv1TAjfyS1xddYIMz/5BF1 +89p0rhkmPnya9e72x3g6ONLsOqoSXSvHP786zVAXVultCwHR+reB7C5xj7/xulDC +E5Txd4ECgYEA95cssDfysQNGCnYQY9Wn3+cYtM+81yy8eH8gCkpZmdX01C4Jz5cH +KWvpAA8s8ooBg+4tE7OBdtzyKlFqXtL8AlpTd0rAnoStsdmidu6XHtgwnGGL1CWD +qplQTUlRxWARE8OcdIiey/wPEkYNjdJpQBbNJVR/So9SViyZP9/sb9UCgYEA0Obc +iEK0jd8FECihnXrEqTFfuzfnJVP3rV+oev+f8gUT5Kc00fIEFkkBFPJGgTqANZVy +FF6HlnyKjd9YuY/XrmRu9gh5yjPfFCcY0jPdTf8RVr7rh1KGtChcFnJ8Eq3hL0RS +clfNwVVLV8nAMPu9mmYg8/mGwJAI2jj1Fj4fArECgYBplIrW/pS8jWPR1DT7DcJP +xbGQcUHbFFWuoK9eFASPiGCmFpfScVn5fO3YO6B0MQuiYe/RBexAbsnJ5/wPQbN7 +oV8UcMkhD/0t6VvRkb7ZxWE9Xo+NQQ4bstM+kfSP0X6WygSu4Q+ududKaJshDkgZ +r199+sFpXyLCYrRbO7cMnQKBgHLoWiw1jP1wzGcsAmIOUrjCaOchg+qbemSKdrFZ +hNBqjJu8gahuGGNtusOb1L5mwHk5ACxGJwzW6pvJXBOOFNRfeE2rMdrQl4eNTfDq +CHRLtmzhzcp80Y2tmaHbTXY04OXQDg3JUGtlEHF0j1wiTRPt03iAK+gmEkh/Bgk7 +GHGxAoGBAJkkxO2NYaavLeUh7zfYKv2yHw+nwnFCSDNDwNkWoRUSHkSSK45N4noL +zfH3C+rvICBACKZH+N1cyAvX45TXNZCY5ACP/sVX+G+Aqmy4jdj8HNcSOpBBNvt2 +X04O3bJLYjKuD1rrr2Rsdue1l7ZCaZdaoezP8+85bS7Z9f36NPv5 +-----END RSA PRIVATE KEY----- diff --git a/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/positive/server.crt b/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/positive/server.crt new file mode 100644 index 00000000..6de04d78 --- /dev/null +++ b/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/positive/server.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDazCCAlOgAwIBAgIJAI/Amrjs0rrXMA0GCSqGSIb3DQEBCwUAMFkxCzAJBgNV +BAYTAlBMMQowCAYDVQQIDAFBMQwwCgYDVQQKDANTQVAxMDAuBgNVBAMMJ21vY2st +YXBwbGljYXRpb24udGVzdC5zdmMuY2x1c3Rlci5sb2NhbDAeFw0yNDAzMTIxNTIy +NTNaFw0yNTAzMTIxNTIyNTNaMFkxCzAJBgNVBAYTAlBMMQowCAYDVQQIDAFBMQww +CgYDVQQKDANTQVAxMDAuBgNVBAMMJ21vY2stYXBwbGljYXRpb24udGVzdC5zdmMu +Y2x1c3Rlci5sb2NhbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKXT +WilnzzPqhMwYZiohRGB3x6XJ3d43b2cMswZMRYcjR9gA/XYLolci8UYOOMcVqEkh +mKeeKjryYoa4vcLlex1Zxh10haTKO5LqNwx6tlMOPLo/Xrfz3JRicYJl3I8vl+YP +AHfabfZfqqKP87sRD7O+pb/M1NjGi2xSVzG2uwJ3RIQJsfj+l1lMF9JoTZc4h4fW +nUtcgW+ReyX1+UXd35zI2fCx0gedSe86r7t+XGbm3lfKsEcZdt8tJ24bj17PAv7n +Z7H//0zUTqCrA92WE/flM2pGXniMcVS0cylMw5XxlAKpjRUh9dsiKLLfQDROz538 +FSnh/ypVndxBPPxfoesCAwEAAaM2MDQwMgYDVR0RBCswKYInbW9jay1hcHBsaWNh +dGlvbi50ZXN0LnN2Yy5jbHVzdGVyLmxvY2FsMA0GCSqGSIb3DQEBCwUAA4IBAQDD +R9vRbUjIrzKXFRMCuklnDVDGwZGOIMjLGSvnd4X8m6ZolJb3rYjgUJHmj0X2sd1S +A3YcTQ+bvrIRHMaE2Y3gXxwqJkl47YISzAQ2ZE7ahKrFFFzx8BDBNRactijw8jBD +eqxhAxlMybcQAuK1HyGJotmKTkBW9scWVn616XbQb/H/UylYNn5oeIwm2mmZ0iw7 +Mw0Tzzds1d/HCAFwdLy1SJMZGICBq9Qk4vn273Xeail8GmKleZKcMPw30EujpyyA +epRcGg1erNR12gpfYqBkO7l90WFDChq4iabr+sbnbLnpaLRTUxByo2m7LqRz6F6L ++QKjz77UMrTqAf212z/j +-----END CERTIFICATE----- diff --git a/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/positive/server.csr b/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/positive/server.csr new file mode 100644 index 00000000..87e8ffdc --- /dev/null +++ b/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/positive/server.csr @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIC4zCCAcsCAQAwWTELMAkGA1UEBhMCUEwxCjAIBgNVBAgMAUExDDAKBgNVBAoM +A1NBUDEwMC4GA1UEAwwnbW9jay1hcHBsaWNhdGlvbi50ZXN0LnN2Yy5jbHVzdGVy +LmxvY2FsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApdNaKWfPM+qE +zBhmKiFEYHfHpcnd3jdvZwyzBkxFhyNH2AD9dguiVyLxRg44xxWoSSGYp54qOvJi +hri9wuV7HVnGHXSFpMo7kuo3DHq2Uw48uj9et/PclGJxgmXcjy+X5g8Ad9pt9l+q +oo/zuxEPs76lv8zU2MaLbFJXMba7AndEhAmx+P6XWUwX0mhNlziHh9adS1yBb5F7 +JfX5Rd3fnMjZ8LHSB51J7zqvu35cZubeV8qwRxl23y0nbhuPXs8C/udnsf//TNRO +oKsD3ZYT9+UzakZeeIxxVLRzKUzDlfGUAqmNFSH12yIost9ANE7PnfwVKeH/KlWd +3EE8/F+h6wIDAQABoEUwQwYJKoZIhvcNAQkOMTYwNDAyBgNVHREEKzApgidtb2Nr +LWFwcGxpY2F0aW9uLnRlc3Quc3ZjLmNsdXN0ZXIubG9jYWwwDQYJKoZIhvcNAQEL +BQADggEBAJujFRd2YFqoDr8kdTVQpLHz3UCaxjBw5S4Lh2xlwwyww0rGmaCeNxk4 +VT0dpTgVjaMjiFGuMAhq2Bz/O0CJV4gvOaTvrEUhTcA+VGytCcTOJVScLduC6KOw +j2l1w6VCzI4ZLILIfsSmRHa32j08qZJ2NEMoaMdaORZI9qbJ+E3DG6TjhF1jJN6p +h5dqSDXgSagGDmHfppvVFdQrTZDKglPgNeU5npIXMLfA7mKp5KkcVUFBQH8Tj8yh +yaKdBFuFeQ/P0GAuu6EG7XbgxaO34OUdwaN8cKxMfPXsZufbBAV2VbwX+YWSslCe +M4RPu3vJzdJe0+Sc0w1mTWZaHkG+Rng= +-----END CERTIFICATE REQUEST----- diff --git a/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/positive/server.key b/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/positive/server.key new file mode 100644 index 00000000..51687d55 --- /dev/null +++ b/tests/hack/ci/resources/charts/gateway-test/charts/test/certs/positive/server.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEApdNaKWfPM+qEzBhmKiFEYHfHpcnd3jdvZwyzBkxFhyNH2AD9 +dguiVyLxRg44xxWoSSGYp54qOvJihri9wuV7HVnGHXSFpMo7kuo3DHq2Uw48uj9e +t/PclGJxgmXcjy+X5g8Ad9pt9l+qoo/zuxEPs76lv8zU2MaLbFJXMba7AndEhAmx ++P6XWUwX0mhNlziHh9adS1yBb5F7JfX5Rd3fnMjZ8LHSB51J7zqvu35cZubeV8qw +Rxl23y0nbhuPXs8C/udnsf//TNROoKsD3ZYT9+UzakZeeIxxVLRzKUzDlfGUAqmN +FSH12yIost9ANE7PnfwVKeH/KlWd3EE8/F+h6wIDAQABAoIBAQCjznNbQYvCSiFi +h5usdG5aKRiUIiREVlh64GWcjA2GoAhhTSET0gxMrVzPik72AuPZUhG9SpWG41cG +pEn3077ZUIxPHoLCNW/CAhHdBv9Cbmb4yI6lgoTcI57jZAILg8U2Yo1g1+oWHYyu +xyrKGOF8+pA7NnjvprmliHVPy6VSma0soTt6C72groaz85WsdAf+UMAhVP/3YwnI +orgfOt3rZSeVCdcdrte43tQp9fBh2CGfgoQjpglNth/sozTL593ZXqMnP9K2QWsA +UM0J5zWdg6TIjImLuDGdH3mQ03wTmJh6n12vvCq0p2f/235AFySAEB2MAdINb+iC +P7kVA48ZAoGBANbwnlg465cIj8MObGaG7vrVeCM3Yh4ObZG2zEh4EVoRv6Ea6mpm +CXdzqgsAl+neYviQdddOS0Ol7/vGbJ7I05WYAFtGimEFo36Dqd6HyA5F7JZwzMCu +gZOQZJbib0QZGyJ0qYKnN/lOoXibz+qz98IQge7iPp+0Pqrzjl3mK1zXAoGBAMWA +2ya15Re3KhEElr+EIHxoTyn1dkC0zmH8zikWU1QVzCa6NihH03Ekzv7Qkc6ungGY +SkFYljf8URL8HavkAKnzK0A632CTArtkw8DZQgZTNVS1TAnMC1v35V4LkdgNfBk1 +vWqziOHLY1seQT3E3wRBU3QzGhHwfAmucEJfkw0NAoGAYHBTy6e2ZOzNfCpTjukJ +/vea0Mo/ttaoaNHI9NcSigQepA1sklK3+qWl7QvWHXPPmlFO3kzdzjt84s3T3Kak +8KDjwBB1dDTQd6phpFvt8iGDlriD1gw2TVxjFaQBYl+VYi9QAzQ+FBkor/HRJzCa +gLNhaSqQCJ4Z5CAlh5IHcL8CgYAiu19OtmwcOIzAQ2NTOKQR3LIXOeBazrEAkFmc +5h0vS0oEgXimqsLnQcbZDsqlYxXMSAC+7xozrD6BrS52nPj06htwBypjLFctpzG5 +hztSK23UgLFng6d3u+dtG3HBYdWyBT5TNlFbC85kJrTobOefMvG/HIF4KCdX+IIr +We1dPQKBgQCs6xvw8NB1q7UDGcCIpkNLZv6PRyl6H3Yz9gzrTo/YDuCEjwZGGTzo +pUW9cgo0zEwpSGL9r2uIGr/++XFIgTEGEGfnyetisOw4o9ePqop4woSFXOxBcJJV +gEoZYAq8dqCmuuoVexjv+Tapn2DMx/E85JwmddS/mNY6TJCMXqd73g== +-----END RSA PRIVATE KEY----- diff --git a/tests/internal/testkit/httpd/http.go b/tests/internal/testkit/httpd/http.go new file mode 100644 index 00000000..694b03e2 --- /dev/null +++ b/tests/internal/testkit/httpd/http.go @@ -0,0 +1,44 @@ +package httpd + +import ( + "io" + "net/http" + "testing" +) + +type LogHttp struct { + t *testing.T + httpCli *http.Client +} + +func NewCli(t *testing.T) LogHttp { + return LogHttp{t: t, httpCli: &http.Client{}} +} + +func (c LogHttp) Get(url string) (resp *http.Response, body []byte, err error) { + c.t.Helper() + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return + } + + return c.Do(req) + +} + +func (c LogHttp) Do(req *http.Request) (res *http.Response, body []byte, err error) { + c.t.Helper() + c.t.Logf("%s %s", req.Method, req.URL) + + res, err = c.httpCli.Do(req) + if err != nil { + return + } + + body, err = io.ReadAll(res.Body) + if err == nil && len(body) > 0 { + c.t.Logf("Body: %s", body) + } + + return +} diff --git a/tests/internal/testkit/test-api/apis.go b/tests/internal/testkit/test-api/apis.go new file mode 100644 index 00000000..98c04105 --- /dev/null +++ b/tests/internal/testkit/test-api/apis.go @@ -0,0 +1,122 @@ +package test_api + +import ( + "fmt" + "io" + "net/http" + + "github.com/go-http-utils/logger" + "github.com/gorilla/mux" + log "github.com/sirupsen/logrus" +) + +func SetupRoutes(logOut io.Writer, basicAuthCredentials BasicAuthCredentials, oAuthCredentials OAuthCredentials, expectedRequestParameters ExpectedRequestParameters, oauthTokens map[string]OAuthToken, csrfTokens CSRFTokens) http.Handler { + router := mux.NewRouter() + + router.HandleFunc("/v1/health", alwaysOk).Methods("GET") + api := router.PathPrefix("/v1/api").Subrouter() + api.Use(Logger(logOut, logger.DevLoggerType)) + + oauth := NewOAuth(oAuthCredentials.ClientID, oAuthCredentials.ClientSecret, oauthTokens) + csrf := NewCSRF(csrfTokens) + + { + api.HandleFunc("/oauth/token", oauth.Token).Methods(http.MethodPost) + api.HandleFunc("/oauth/bad-token", oauth.BadToken).Methods(http.MethodPost) + api.HandleFunc("/csrf/token", csrf.Token).Methods(http.MethodGet) + api.HandleFunc("/csrf/bad-token", csrf.BadToken).Methods(http.MethodGet) + } + + { + r := api.PathPrefix("/unsecure").Subrouter() + r.HandleFunc("/ok", alwaysOk).Methods(http.MethodGet) + r.HandleFunc("/echo", echo).Methods(http.MethodPut, http.MethodPost, http.MethodDelete) + r.HandleFunc("/code/{code:[0-9]+}", resCode).Methods(http.MethodGet) + r.HandleFunc("/timeout", timeout).Methods(http.MethodGet) + } + { + r := api.PathPrefix("/basic").Subrouter() + r.Use(BasicAuth(basicAuthCredentials)) + r.HandleFunc("/ok", alwaysOk).Methods(http.MethodGet) + } + { + r := api.PathPrefix("/oauth").Subrouter() + r.Use(oauth.Middleware()) + r.HandleFunc("/ok", alwaysOk).Methods(http.MethodGet) + } + { + r := api.PathPrefix("/csrf-basic").Subrouter() + r.Use(csrf.Middleware()) + r.Use(BasicAuth(basicAuthCredentials)) + r.HandleFunc("/ok", alwaysOk).Methods(http.MethodGet) + } + { + r := api.PathPrefix("/csrf-oauth").Subrouter() + r.Use(csrf.Middleware()) + r.Use(oauth.Middleware()) + r.HandleFunc("/ok", alwaysOk).Methods(http.MethodGet) + } + { + r := api.PathPrefix("/request-parameters-basic").Subrouter() + r.Use(RequestParameters(expectedRequestParameters)) + r.Use(BasicAuth(basicAuthCredentials)) + r.HandleFunc("/ok", alwaysOk).Methods(http.MethodGet) + } + { + r := api.PathPrefix("/redirect").Subrouter() + + r.HandleFunc("/ok/target", alwaysOk).Methods(http.MethodGet) + + r.Handle("/ok", http.RedirectHandler("/v1/api/redirect/ok/target", http.StatusTemporaryRedirect)) + + ba := BasicAuth(basicAuthCredentials) + ok := http.HandlerFunc(alwaysOk) + r.Handle("/basic/target", ba(ok)).Methods(http.MethodGet) + r.Handle("/basic", http.RedirectHandler("/v1/api/redirect/basic/target", http.StatusTemporaryRedirect)) + + r.Handle("/external", http.RedirectHandler("http://central-application-gateway.kyma-system:8081/v1/health", http.StatusTemporaryRedirect)) + } + + return router +} + +func SetupMTLSRoutes(logOut io.Writer, oAuthCredentials OAuthCredentials, oauthTokens map[string]OAuthToken, csrfTokens CSRFTokens) http.Handler { + router := mux.NewRouter() + + router.HandleFunc("/v1/health", alwaysOk).Methods("GET") + api := router.PathPrefix("/v1/api").Subrouter() + api.Use(Logger(logOut, logger.DevLoggerType)) + + oauth := NewOAuth(oAuthCredentials.ClientID, oAuthCredentials.ClientSecret, oauthTokens) + csrf := NewCSRF(csrfTokens) + + { + r := api.PathPrefix("/mtls").Subrouter() + r.Use(oauth.Middleware()) + api.HandleFunc("/mtls-oauth/token", oauth.MTLSToken).Methods(http.MethodPost) + } + + { + r := api.PathPrefix("/mtls").Subrouter() + r.HandleFunc("/ok", alwaysOk).Methods(http.MethodGet) + } + { + r := api.PathPrefix("/csrf-mtls").Subrouter() + r.Use(csrf.Middleware()) + r.HandleFunc("/ok", alwaysOk).Methods(http.MethodGet) + } + + return router +} + +func Logger(out io.Writer, t logger.Type) mux.MiddlewareFunc { + return func(next http.Handler) http.Handler { + return logger.Handler(next, out, t) + } +} + +func handleError(w http.ResponseWriter, code int, format string, a ...interface{}) { + err := fmt.Errorf(format, a...) + log.Error(err) + w.WriteHeader(code) +} diff --git a/tests/internal/testkit/test-api/basicauth.go b/tests/internal/testkit/test-api/basicauth.go new file mode 100644 index 00000000..0307ae94 --- /dev/null +++ b/tests/internal/testkit/test-api/basicauth.go @@ -0,0 +1,30 @@ +package test_api + +import ( + "github.com/gorilla/mux" + "net/http" +) + +type BasicAuthCredentials struct { + User string + Password string +} + +func BasicAuth(credentials BasicAuthCredentials) mux.MiddlewareFunc { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + u, p, ok := r.BasicAuth() + if !ok { + handleError(w, http.StatusForbidden, "Basic auth header not found") + return + } + + if credentials.User != u || credentials.Password != p { + handleError(w, http.StatusForbidden, "Incorrect username or Password") + return + } + + next.ServeHTTP(w, r) + }) + } +} diff --git a/tests/internal/testkit/test-api/csrf.go b/tests/internal/testkit/test-api/csrf.go new file mode 100644 index 00000000..06f7993b --- /dev/null +++ b/tests/internal/testkit/test-api/csrf.go @@ -0,0 +1,95 @@ +package test_api + +import ( + "github.com/google/uuid" + "github.com/gorilla/mux" + "net/http" + "sync" +) + +const ( + csrfTokenHeader = "X-csrf-token" + csrfTokenCookie = "csrftokencookie" +) + +type CSRFTokens map[string]interface{} + +type CSRFHandler struct { + mutex sync.RWMutex + tokens map[string]interface{} +} + +func NewCSRF(tokens CSRFTokens) CSRFHandler { + return CSRFHandler{ + mutex: sync.RWMutex{}, + tokens: tokens, + } +} + +func (ch *CSRFHandler) Token(w http.ResponseWriter, _ *http.Request) { + token := uuid.New().String() + + ch.mutex.Lock() + ch.tokens[token] = nil + ch.mutex.Unlock() + + w.Header().Set(csrfTokenHeader, token) + http.SetCookie(w, &http.Cookie{ + Name: csrfTokenCookie, + Value: token, + }) + + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") +} + +func (ch *CSRFHandler) BadToken(w http.ResponseWriter, _ *http.Request) { + token := uuid.New().String() + + w.Header().Set(csrfTokenHeader, token) + http.SetCookie(w, &http.Cookie{ + Name: csrfTokenCookie, + Value: token, + }) + + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") +} + +func (ch *CSRFHandler) Middleware() mux.MiddlewareFunc { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + headerToken := r.Header.Get(csrfTokenHeader) + if headerToken == "" { + handleError(w, http.StatusForbidden, "CSRF token header missing") + return + } + + ch.mutex.RLock() + _, found := ch.tokens[headerToken] + ch.mutex.RUnlock() + + if !found { + handleError(w, http.StatusForbidden, "Invalid CSRF token from the header") + return + } + + cookieToken, err := r.Cookie(csrfTokenCookie) + if err != nil { + handleError(w, http.StatusForbidden, "CSRF token cookie missing") + return + } + + ch.mutex.RLock() + _, found = ch.tokens[cookieToken.Value] + ch.mutex.RUnlock() + + if !found { + handleError(w, http.StatusForbidden, "Invalid CSRF token from the cookie") + return + } + + next.ServeHTTP(w, r) + }) + } +} diff --git a/tests/internal/testkit/test-api/handlers.go b/tests/internal/testkit/test-api/handlers.go new file mode 100644 index 00000000..26114863 --- /dev/null +++ b/tests/internal/testkit/test-api/handlers.go @@ -0,0 +1,69 @@ +package test_api + +import ( + "encoding/json" + "io/ioutil" + "log" + "net/http" + "strconv" + "time" + + "github.com/gorilla/mux" +) + +func alwaysOk(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) +} + +type EchoResponse struct { + Body []byte `json:"body"` + Headers map[string][]string `json:"headers"` + Method string `json:"method"` + Query string `json:"query"` +} + +func echo(w http.ResponseWriter, r *http.Request) { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + log.Println("Couldn't read request body:", r.URL) + body = []byte("") + } + + res := EchoResponse{ + Method: r.Method, + Body: body, + Headers: r.Header, + Query: r.URL.RawQuery, + } + + w.Header().Set("Content-Type", "application/json") + err = json.NewEncoder(w).Encode(res) + + if err != nil { + log.Println("Couldn't encode the response body to JSON:", r.URL) + } +} + +// resCode should only be used in paths with `code` +// parameter, that is a valid int +func resCode(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + codeStr := vars["code"] // must exist, because path has a pattern + code, _ := strconv.Atoi(codeStr) // can't error, because path has a pattern + w.WriteHeader(code) + w.Write([]byte(codeStr)) +} + +func timeout(w http.ResponseWriter, r *http.Request) { + c := r.Context().Done() + if c == nil { + log.Println("Context has no timeout, sleeping for 2 minutes") + time.Sleep(2 * time.Minute) + return + } + log.Println("Context timeout, waiting until done") + + _ = <-c + + alwaysOk(w, r) +} diff --git a/tests/internal/testkit/test-api/oauth.go b/tests/internal/testkit/test-api/oauth.go new file mode 100644 index 00000000..2fe60ea3 --- /dev/null +++ b/tests/internal/testkit/test-api/oauth.go @@ -0,0 +1,190 @@ +package test_api + +import ( + "encoding/json" + "fmt" + "github.com/gorilla/mux" + "net/http" + "strings" + "sync" + "time" + + "github.com/google/uuid" + log "github.com/sirupsen/logrus" +) + +type OAuthCredentials struct { + ClientID string + ClientSecret string +} + +const ( + clientIDKey = "client_id" + clientSecretKey = "client_secret" + grantTypeKey = "grant_type" + tokenLifetime = "token_lifetime" + defaultTokenExpiresIn = 5 * time.Minute +) + +type OauthResponse struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + ExpiresIn int64 `json:"expires_in,omitempty"` +} + +type OAuthToken struct { + exp time.Time +} + +func (token OAuthToken) Valid() bool { + return token.exp.After(time.Now()) +} + +type OAuthHandler struct { + clientID string + clientSecret string + mutex sync.RWMutex + tokens map[string]OAuthToken +} + +func NewOAuth(clientID, clientSecret string, tokens map[string]OAuthToken) OAuthHandler { + return OAuthHandler{ + clientID: clientID, + clientSecret: clientSecret, + mutex: sync.RWMutex{}, + tokens: tokens, + } +} + +func (oh *OAuthHandler) Token(w http.ResponseWriter, r *http.Request) { + if ok, status, message := oh.isRequestValid(r); !ok { + handleError(w, status, message) + return + } + + token := uuid.New().String() + exp := defaultTokenExpiresIn + + if ttlStr := r.URL.Query().Get(tokenLifetime); ttlStr != "" { + parsedEXP, err := time.ParseDuration(ttlStr) + if err == nil { + log.Info("Received valid OAuth expiresIn:", parsedEXP) + exp = parsedEXP + } else { + log.Error("Received invalid OAuth expiresIn:", err) + } + } + + oh.storeTokenInCache(token, exp) + + response := OauthResponse{AccessToken: token, TokenType: "bearer", ExpiresIn: int64(exp.Seconds())} + oh.respondWithToken(w, response) +} + +func (oh *OAuthHandler) BadToken(w http.ResponseWriter, r *http.Request) { + if ok, status, message := oh.isRequestValid(r); !ok { + handleError(w, status, message) + return + } + + token := uuid.New().String() + response := OauthResponse{AccessToken: token, TokenType: "bearer"} + oh.respondWithToken(w, response) +} + +func (oh *OAuthHandler) MTLSToken(w http.ResponseWriter, r *http.Request) { + if ok, status, message := oh.isMTLSRequestValid(r); !ok { + handleError(w, status, message) + return + } + + token := uuid.New().String() + exp := defaultTokenExpiresIn + + oh.storeTokenInCache(token, exp) + response := OauthResponse{AccessToken: token, TokenType: "bearer", ExpiresIn: 3600} + oh.respondWithToken(w, response) +} + +func (oh *OAuthHandler) Middleware() mux.MiddlewareFunc { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + authHeader := r.Header.Get("Authorization") + if authHeader == "" { + handleError(w, http.StatusUnauthorized, "Authorization header missing") + return + } + + splitToken := strings.Split(authHeader, "Bearer") + if len(splitToken) != 2 { + handleError(w, http.StatusUnauthorized, "Bearer token missing") + return + } + + token := strings.TrimSpace(splitToken[1]) + + oh.mutex.RLock() + data, found := oh.tokens[token] + oh.mutex.RUnlock() + + if !found || !data.Valid() { + handleError(w, http.StatusUnauthorized, "Invalid token") + return + } + + next.ServeHTTP(w, r) + }) + } +} + +func (oh *OAuthHandler) isRequestValid(r *http.Request) (bool, int, string) { + err := r.ParseForm() + if err != nil { + return false, http.StatusInternalServerError, fmt.Sprintf("Failed to parse form: %v", err) + } + + clientID := r.FormValue(clientIDKey) + clientSecret := r.FormValue(clientSecretKey) + grantType := r.FormValue(grantTypeKey) + + if !oh.verifyClient(clientID, clientSecret) || grantType != "client_credentials" { + return false, http.StatusForbidden, "Client verification failed" + } + + return true, 0, "" +} + +func (oh *OAuthHandler) verifyClient(id, secret string) bool { + return id == oh.clientID && secret == oh.clientSecret +} + +func (oh *OAuthHandler) respondWithToken(w http.ResponseWriter, response OauthResponse) { + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(response); err != nil { + handleError(w, http.StatusInternalServerError, "Failed to encode token response") + return + } + w.WriteHeader(http.StatusOK) +} + +func (oh *OAuthHandler) storeTokenInCache(token string, expIn time.Duration) { + oh.mutex.Lock() + oh.tokens[token] = OAuthToken{exp: time.Now().Add(expIn)} + oh.mutex.Unlock() +} + +func (oh *OAuthHandler) isMTLSRequestValid(r *http.Request) (bool, int, string) { + err := r.ParseForm() + if err != nil { + return false, http.StatusInternalServerError, fmt.Sprintf("Failed to parse form: %v", err) + } + + clientID := r.FormValue(clientIDKey) + grantType := r.FormValue(grantTypeKey) + + if r.TLS == nil || clientID != oh.clientID || grantType != "client_credentials" { + return false, http.StatusForbidden, "Client verification failed" + } + + return true, 0, "" +} diff --git a/tests/internal/testkit/test-api/requestparams.go b/tests/internal/testkit/test-api/requestparams.go new file mode 100644 index 00000000..774ff9fa --- /dev/null +++ b/tests/internal/testkit/test-api/requestparams.go @@ -0,0 +1,53 @@ +package test_api + +import ( + "github.com/gorilla/mux" + "net/http" +) + +type ExpectedRequestParameters struct { + Headers map[string][]string + QueryParameters map[string][]string +} + +func RequestParameters(expectedRequestParams ExpectedRequestParameters) mux.MiddlewareFunc { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + for key, expectedVals := range expectedRequestParams.Headers { + actualVals := r.Header.Values(key) + if !containsSubset(actualVals, expectedVals) { + handleError(w, http.StatusBadRequest, "Incorrect additional headers. Expected %s header to contain %v, but found %v", key, expectedVals, actualVals) + return + } + } + + queryParameters := r.URL.Query() + for key, expectedVals := range expectedRequestParams.QueryParameters { + actualVals := queryParameters[key] + if !containsSubset(actualVals, expectedVals) { + handleError(w, http.StatusBadRequest, "Incorrect additional query parameters. Expected %s query parameter to contain %v, but found %v", key, expectedVals, actualVals) + return + } + } + + next.ServeHTTP(w, r) + }) + } +} + +func containsSubset(set, subset []string) bool { + for _, bVal := range subset { + found := false + for _, aVal := range set { + if aVal == bVal { + found = true + break + } + } + + if !found { + return false + } + } + return true +} diff --git a/tests/resources/charts/application-connectivity-validator-test/Chart.yaml b/tests/resources/charts/application-connectivity-validator-test/Chart.yaml new file mode 100644 index 00000000..0424adb3 --- /dev/null +++ b/tests/resources/charts/application-connectivity-validator-test/Chart.yaml @@ -0,0 +1,23 @@ +apiVersion: v2 +name: application-connectivity-validator-test +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +appVersion: 1.16.0 diff --git a/tests/resources/charts/application-connectivity-validator-test/charts/echoserver/Chart.yaml b/tests/resources/charts/application-connectivity-validator-test/charts/echoserver/Chart.yaml new file mode 100644 index 00000000..807b2174 --- /dev/null +++ b/tests/resources/charts/application-connectivity-validator-test/charts/echoserver/Chart.yaml @@ -0,0 +1,23 @@ +apiVersion: v2 +name: echoserver +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +appVersion: 1.16.0 diff --git a/tests/resources/charts/application-connectivity-validator-test/charts/echoserver/templates/echoserver.yml b/tests/resources/charts/application-connectivity-validator-test/charts/echoserver/templates/echoserver.yml new file mode 100644 index 00000000..d4f83b72 --- /dev/null +++ b/tests/resources/charts/application-connectivity-validator-test/charts/echoserver/templates/echoserver.yml @@ -0,0 +1,41 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: echosever + name: echoserver + namespace: {{ .Values.global.namespace }} +spec: + replicas: 1 + selector: + matchLabels: + app: echoserver + template: + metadata: + labels: + app: echoserver + spec: + containers: + - image: ealen/echo-server:0.7.0 + name: echoserver + ports: + - containerPort: 80 + livenessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 3 + periodSeconds: 3 +--- +apiVersion: v1 +kind: Service +metadata: + name: echoserver + namespace: {{ .Values.global.namespace }} +spec: + selector: + app: echoserver + ports: + - name: "http" + protocol: TCP + port: 80 diff --git a/tests/resources/charts/application-connectivity-validator-test/charts/test/Chart.yaml b/tests/resources/charts/application-connectivity-validator-test/charts/test/Chart.yaml new file mode 100644 index 00000000..6e99aa1f --- /dev/null +++ b/tests/resources/charts/application-connectivity-validator-test/charts/test/Chart.yaml @@ -0,0 +1,23 @@ +apiVersion: v2 +name: test +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +appVersion: 1.16.0 diff --git a/tests/resources/charts/application-connectivity-validator-test/charts/test/templates/_helpers.tpl b/tests/resources/charts/application-connectivity-validator-test/charts/test/templates/_helpers.tpl new file mode 100644 index 00000000..5acdb5e3 --- /dev/null +++ b/tests/resources/charts/application-connectivity-validator-test/charts/test/templates/_helpers.tpl @@ -0,0 +1,11 @@ +{{/* +Create a URL for container images +*/}} +{{- define "imageurl" -}} +{{- $registry := default $.reg.path $.img.containerRegistryPath -}} +{{- if hasKey $.img "directory" -}} +{{- printf "%s/%s/%s:%s" $registry $.img.directory $.img.name $.img.version -}} +{{- else -}} +{{- printf "%s/%s:%s" $registry $.img.name $.img.version -}} +{{- end -}} +{{- end -}} \ No newline at end of file diff --git a/tests/resources/charts/application-connectivity-validator-test/charts/test/templates/applications/event-test-compass.yml b/tests/resources/charts/application-connectivity-validator-test/charts/test/templates/applications/event-test-compass.yml new file mode 100644 index 00000000..8e1a498e --- /dev/null +++ b/tests/resources/charts/application-connectivity-validator-test/charts/test/templates/applications/event-test-compass.yml @@ -0,0 +1,12 @@ +apiVersion: applicationconnector.kyma-project.io/v1alpha1 +kind: Application +metadata: + name: event-test-compass +spec: + compassMetadata: + applicationId: applicationId + authentication: + clientIds: ["clientId1", "clientId2"] + description: Test app-con-validator + skipVerify: true + diff --git a/tests/resources/charts/application-connectivity-validator-test/charts/test/templates/applications/event-test-standalone.yml b/tests/resources/charts/application-connectivity-validator-test/charts/test/templates/applications/event-test-standalone.yml new file mode 100644 index 00000000..424b7bd6 --- /dev/null +++ b/tests/resources/charts/application-connectivity-validator-test/charts/test/templates/applications/event-test-standalone.yml @@ -0,0 +1,7 @@ +apiVersion: applicationconnector.kyma-project.io/v1alpha1 +kind: Application +metadata: + name: event-test-standalone +spec: + description: Test app-con-validator + skipVerify: true diff --git a/tests/resources/charts/application-connectivity-validator-test/charts/test/templates/test.yml b/tests/resources/charts/application-connectivity-validator-test/charts/test/templates/test.yml new file mode 100644 index 00000000..12eb1f67 --- /dev/null +++ b/tests/resources/charts/application-connectivity-validator-test/charts/test/templates/test.yml @@ -0,0 +1,17 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: application-connectivity-validator-test + namespace: {{ .Values.global.namespace }} +spec: + backoffLimit: 0 + template: + metadata: + annotations: + traffic.sidecar.istio.io/excludeOutboundPorts: "8080" + spec: + containers: + - name: application-connectivity-validator-test + image: {{ include "imageurl" (dict "reg" .Values.global.containerRegistry "img" .Values.global.images.validatorTest) }} + imagePullPolicy: Always + restartPolicy: Never \ No newline at end of file diff --git a/tests/resources/charts/application-connectivity-validator-test/values.yaml b/tests/resources/charts/application-connectivity-validator-test/values.yaml new file mode 100644 index 00000000..5b84bcba --- /dev/null +++ b/tests/resources/charts/application-connectivity-validator-test/values.yaml @@ -0,0 +1,15 @@ +# Default values for application-connectivity-validator-test. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +global: + containerRegistry: + path: "europe-docker.pkg.dev/kyma-project" + + images: + validatorTest: + name: "connectivity-validator-test" + version: "v20230925-75c3a9a8" + directory: "prod" + + namespace: "test" diff --git a/tests/resources/charts/compass-runtime-agent-test/Chart.yaml b/tests/resources/charts/compass-runtime-agent-test/Chart.yaml new file mode 100644 index 00000000..9b3c49e9 --- /dev/null +++ b/tests/resources/charts/compass-runtime-agent-test/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: compass-runtime-agent-test +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "0.1.0" diff --git a/tests/resources/charts/compass-runtime-agent-test/templates/_helpers.tpl b/tests/resources/charts/compass-runtime-agent-test/templates/_helpers.tpl new file mode 100644 index 00000000..9cba1391 --- /dev/null +++ b/tests/resources/charts/compass-runtime-agent-test/templates/_helpers.tpl @@ -0,0 +1,12 @@ + +{{/* +Create a URL for container images +*/}} +{{- define "imageurl" -}} +{{- $registry := default $.reg.path $.img.containerRegistryPath -}} +{{- if hasKey $.img "directory" -}} +{{- printf "%s/%s/%s:%s" $registry $.img.directory $.img.name $.img.version -}} +{{- else -}} +{{- printf "%s/%s:%s" $registry $.img.name $.img.version -}} +{{- end -}} +{{- end -}} diff --git a/tests/resources/charts/compass-runtime-agent-test/templates/applications/test-create-app.yaml b/tests/resources/charts/compass-runtime-agent-test/templates/applications/test-create-app.yaml new file mode 100644 index 00000000..e95e05cd --- /dev/null +++ b/tests/resources/charts/compass-runtime-agent-test/templates/applications/test-create-app.yaml @@ -0,0 +1,59 @@ +apiVersion: applicationconnector.kyma-project.io/v1alpha1 +kind: Application +metadata: + labels: + applicationconnector.kyma-project.io/managed-by: compass-runtime-agent + name: app1 +spec: + description: Test Application for testing Compass Runtime Agent + displayName: "" + longDescription: "" + providerDisplayName: "" + skipVerify: false + services: + - description: Foo bar + displayName: bndl-app-1 + entries: + - centralGatewayUrl: http://central-application-gateway.kyma-system.svc.cluster.local:8082/mp-app1gkhavxduzb/bndl-app-1/comments-v1 + credentials: + secretName: "" + type: "" + gatewayUrl: "" + id: 30747de1-4a87-4b67-a75d-9fe84af6e6f9 + name: comments-v1 + targetUrl: http://mywordpress.com/comments + type: API + id: e4148ee9-79c0-4d81-863c-311f32aeed9b + identifier: "" + name: bndl-app-1-0d79e + providerDisplayName: "" +--- +apiVersion: applicationconnector.kyma-project.io/v1alpha1 +kind: Application +metadata: + labels: + applicationconnector.kyma-project.io/managed-by: compass-runtime-agent + name: app1-updated +spec: + description: "The app was updated" + displayName: "" + longDescription: "" + providerDisplayName: "" + services: + - description: Foo bar + displayName: bndl-app-1 + entries: + - centralGatewayUrl: http://central-application-gateway.kyma-system.svc.cluster.local:8082/mp-app1gkhavxduzb/bndl-app-1/comments-v1 + credentials: + secretName: "" + type: "" + gatewayUrl: "" + id: 30747de1-4a87-4b67-a75d-9fe84af6e6f9 + name: comments-v1 + targetUrl: http://mywordpress.com/comments + type: API + id: e4148ee9-79c0-4d81-863c-311f32aeed9b + identifier: "" + name: bndl-app-1-0d79e + providerDisplayName: "" + skipVerify: false diff --git a/tests/resources/charts/compass-runtime-agent-test/templates/secret-compass.yaml b/tests/resources/charts/compass-runtime-agent-test/templates/secret-compass.yaml new file mode 100644 index 00000000..8a76d2cf --- /dev/null +++ b/tests/resources/charts/compass-runtime-agent-test/templates/secret-compass.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Values.oauthCredentialsSecretName }} + namespace: {{ .Values.oauthCredentialsNamespace }} +data: + client_id: {{ .Values.compassCredentials.clientID | b64enc | quote }} + client_secret: {{ .Values.compassCredentials.clientSecret | b64enc | quote }} + tokens_endpoint: {{ .Values.compassCredentials.tokensEndpoint | b64enc | quote }} +type: Opaque \ No newline at end of file diff --git a/tests/resources/charts/compass-runtime-agent-test/templates/service-account.yaml b/tests/resources/charts/compass-runtime-agent-test/templates/service-account.yaml new file mode 100644 index 00000000..92c53e1d --- /dev/null +++ b/tests/resources/charts/compass-runtime-agent-test/templates/service-account.yaml @@ -0,0 +1,59 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ .Values.serviceAccountName }} + namespace: {{ .Values.namespace }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ .Values.serviceAccountName }} + namespace: {{ .Values.namespace }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ .Values.serviceAccountName }} +subjects: + - kind: ServiceAccount + name: {{ .Values.serviceAccountName }} + namespace: {{ .Values.namespace }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ .Values.serviceAccountName }} +rules: + - apiGroups: + - "" + resources: + - secrets + verbs: + - create + - get + - list + - delete + - apiGroups: + - "apps" + resources: + - deployments + verbs: + - get + - list + - update + - apiGroups: + - "applicationconnector.kyma-project.io" + resources: + - "applications" + verbs: + - get + - list + - apiGroups: + - "compass.kyma-project.io" + resources: + - "compassconnections" + verbs: + - create + - get + - delete + - update + - list \ No newline at end of file diff --git a/tests/resources/charts/compass-runtime-agent-test/templates/test.yaml b/tests/resources/charts/compass-runtime-agent-test/templates/test.yaml new file mode 100644 index 00000000..639a8b8d --- /dev/null +++ b/tests/resources/charts/compass-runtime-agent-test/templates/test.yaml @@ -0,0 +1,26 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: compass-runtime-agent-test + namespace: {{ .Values.namespace }} +spec: + template: + spec: + restartPolicy: Never + serviceAccountName: {{ .Values.serviceAccountName }} + containers: + - name: compass-runtime-agent-test + image: {{ include "imageurl" (dict "reg" .Values.containerRegistry "img" .Values.images.compassTest) }} + imagePullPolicy: Always + env: + - name: APP_DIRECTOR_URL + value: {{ .Values.directorUrl }} + - name: APP_TESTING_TENANT + value: {{ .Values.testTenant }} + - name: APP_SKIP_DIRECTOR_CERT_VERIFICATION + value: {{ .Values.skipDirectorCertVerification | quote }} + - name: APP_OAUTH_CREDENTIALS_SECRET_NAME + value: {{.Values.oauthCredentialsSecretName}} + - name: APP_OAUTH_CREDENTIALS_NAMESPACE + value: {{ .Values.oauthCredentialsNamespace }} + backoffLimit: 0 diff --git a/tests/resources/charts/compass-runtime-agent-test/values.yaml b/tests/resources/charts/compass-runtime-agent-test/values.yaml new file mode 100644 index 00000000..e788e7d3 --- /dev/null +++ b/tests/resources/charts/compass-runtime-agent-test/values.yaml @@ -0,0 +1,20 @@ +namespace: "test" +testTenant: "3e64ebae-38b5-46a0-b1ed-9ccee153a0ae" +oauthCredentialsSecretName: "oauth-compass-credentials" +oauthCredentialsNamespace: "test" +skipDirectorCertVerification: true +serviceAccountName: "test-compass-runtime-agent" + +containerRegistry: + path: "europe-docker.pkg.dev/kyma-project" + +images: + compassTest: + name: "compass-runtime-agent-test" + version: "v20230925-75c3a9a8" + directory: "prod" + +compassCredentials: + clientID: "" + clientSecret: "" + tokensEndpoint: "" diff --git a/tests/resources/charts/gateway-test/Chart.yaml b/tests/resources/charts/gateway-test/Chart.yaml new file mode 100644 index 00000000..ea515d61 --- /dev/null +++ b/tests/resources/charts/gateway-test/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: application-gateway-test +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "0.1.0" diff --git a/tests/resources/charts/gateway-test/charts/mock-app/Chart.yaml b/tests/resources/charts/gateway-test/charts/mock-app/Chart.yaml new file mode 100644 index 00000000..5671f8ac --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/mock-app/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: mock-app +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "0.1.0" diff --git a/tests/resources/charts/gateway-test/charts/mock-app/certs/invalid-ca/ca.crt b/tests/resources/charts/gateway-test/charts/mock-app/certs/invalid-ca/ca.crt new file mode 100644 index 00000000..324a4960 --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/mock-app/certs/invalid-ca/ca.crt @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC+jCCAeICCQCIIuahgVg3RTANBgkqhkiG9w0BAQsFADA/MQswCQYDVQQGEwJQ +TDEKMAgGA1UECAwBQTEMMAoGA1UECgwDU0FQMRYwFAYDVQQDDA10ZXN0LW90aGVy +LWNhMB4XDTI0MDMxMjEyMTExOVoXDTI1MDMxMjEyMTExOVowPzELMAkGA1UEBhMC +UEwxCjAIBgNVBAgMAUExDDAKBgNVBAoMA1NBUDEWMBQGA1UEAwwNdGVzdC1vdGhl +ci1jYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALX1TzEMihcmLDFv +1Hr8CPi9PQ6IOiKZzHe+cPhb5o/qr52jbqoqcPcLPhWMt/COsdu9YJTS4N4v6AnF +573CwjaIuXiHuR8Nwej+L0yHqxQJSS4OOWa4fzQulUI341ObKQe5LWGYXJ9tDHHQ +DUbZfS4mCSHSCBavKJxorUfaihO9piKHlbcADZUl8ruAsuQwWGY3/PufcUj28k7o +BkImnS+kDNiTrWENSJlktOtwJeLWWSm3STkWZumvoK0//vig+/EMofSrdnwNGB8m +eN44GC18t/6JkBt+Ccy0EB51Jcmxz7kWPs5efAXzTYk93vt3KzsH3oqmleSVBX83 +58BvsRcCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAWX17M2c3pzF6eroZtxljYiou +hh0uEMFMzQubj57iyeYJzOkjWWrWkHImvOJhF/2eLIqjJt7DIlUjSGxEOXg+zYZP +zp1et/dGhmSsIuJ3m4IxLzkjajeRBnqhPe4lE/bgilqDgpHTElCLuIY3eP4fkwYp +G0QDO8mEz2tghfsZFM1iK1vs1DXTCCr6XHivA/fQZyN45Be/0BDqYnTtTIZr9/uL +aa6EXHCVrE3sSfQlwA0qTvqpgVQR7RO/QmzZeVavRQbeDC+Gt9wl//nWlwqLKx+I +3Y9X3+v/iO7OEglxp1QZAHHO63gtWm54EXtOEeWUnUUWA/cdA9gNQQocZNawMA== +-----END CERTIFICATE----- diff --git a/tests/resources/charts/gateway-test/charts/mock-app/certs/invalid-ca/ca.key b/tests/resources/charts/gateway-test/charts/mock-app/certs/invalid-ca/ca.key new file mode 100644 index 00000000..4b728467 --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/mock-app/certs/invalid-ca/ca.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC19U8xDIoXJiwx +b9R6/Aj4vT0OiDoimcx3vnD4W+aP6q+do26qKnD3Cz4VjLfwjrHbvWCU0uDeL+gJ +xee9wsI2iLl4h7kfDcHo/i9Mh6sUCUkuDjlmuH80LpVCN+NTmykHuS1hmFyfbQxx +0A1G2X0uJgkh0ggWryicaK1H2ooTvaYih5W3AA2VJfK7gLLkMFhmN/z7n3FI9vJO +6AZCJp0vpAzYk61hDUiZZLTrcCXi1lkpt0k5Fmbpr6CtP/74oPvxDKH0q3Z8DRgf +JnjeOBgtfLf+iZAbfgnMtBAedSXJsc+5Fj7OXnwF802JPd77dys7B96KppXklQV/ +N+fAb7EXAgMBAAECggEAKbL1Gg2Am/uAhzfUnvaha7eahXkMsZ9Db3GyXAhbl0G2 +S08H7nFZgBQQf0nHYZaiBfSpbJHDPMgHyi2ThTZb4bmFn6yi7Q3vEWEnH8e7mhTi +s25JE1RWunOuewVp0GAvj/iNAN+04khQYMjIMiNnf6rxztFeTyyHBwkqJNxdZlZe +BrT41N3LqFsvjhz5kWuEzBYWs5SFV7Co62+fD2gJW4atWt4w9QywELVsYM8k72nS +cuZ/cxgiCpDLej6M9MgZZvYPNs9x0sWjDUOH/1zW1vr98cVbfwf4DcZdKNHux6Dt +iwl5awok5EAxlJT3KY2e5Xy9yulBioImrEj7t1h2GQKBgQDdefY3Gm8b1EsSLnGu +tYYFiDCC2TnUMRCdXs9+ZSmIgylv/mi1kGu5TIh1YPNEG0ZBqkmPS6p9d0sgDwDH +cagAIpfDK7c1H4w61lQYUo2O0RvExULDsAAFQxcz7GTADLeDaiBuNy2VpzNtIEw2 +xaD9dYwCqReiY2cpzW0QZ5EUtQKBgQDSUl71Rpjg4EntZdztIW/WnYy0fDzcNSqE +zH8ZepXx7SFd4N/Ru27dofVE1oi3MQIBojfxbuLQ6DYAsbZBnLz7WGCeIp32W/vz +LAotojyTSL7jBJXH5XV6CJvFNzXIufc6Hzd29XUvAm6TEC5TKd/rwQYxJX/OtVNE +30qgzBu6GwKBgDXf4xrIXVrBq3lCvvimw3E5DcPmn4CUZtxBIew3I4FHlp7dng78 +kJfEnDUhXkuk7tQuXjJzT4exqx6jR6c8aIeP4qbhTXGouO3fERnRiwnAqCaXbYQ4 +neipx00kJeXpsgJPoI/u8DHFOGdFQgTY0i6Vl3dWNp+T2pZ6mBszdkE5AoGBAK95 +6B4uS6j7mNKH9V6nUh82jcmcCk8T0KjB0Z1ZaLdTSE6CK1taTXJ/CRro/2IQcoMY +bCJ0iKsRwtSrcMunUQlHwDzP1wlPz5MggFF4lZ+wxwqzrZ/9MxmhCw3tNWOGvN1y +ZB1NR/rzxXvPuUbLnjadcmQYzFyTbqj8v9AO22dXAoGBAKCNHJjXj1Gurs5lQ1tG +F/YwoJAOuXBy3mmDGYOOJ688Q4d56M9D+SWK6USVWvggx95ZmQ4SToS3ZUDnNPyV +l2TNvzx1Dq2a/vbT0dy1UNkk7epTWH4K0laY//P8P+ytB++XcWGu3orJdx1z8UMT +u12YURbB7Y87lg6kiYWHl6eN +-----END PRIVATE KEY----- diff --git a/tests/resources/charts/gateway-test/charts/mock-app/certs/invalid-ca/client.crt b/tests/resources/charts/gateway-test/charts/mock-app/certs/invalid-ca/client.crt new file mode 100644 index 00000000..fc38caed --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/mock-app/certs/invalid-ca/client.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDHTCCAgWgAwIBAgIJAOkxC9iq+eDQMA0GCSqGSIb3DQEBCwUAMD8xCzAJBgNV +BAYTAlBMMQowCAYDVQQIDAFBMQwwCgYDVQQKDANTQVAxFjAUBgNVBAMMDXRlc3Qt +b3RoZXItY2EwHhcNMjQwMzEyMTIxMTIwWhcNMjUwMzEyMTIxMTIwWjA/MQswCQYD +VQQGEwJQTDEKMAgGA1UECAwBQTEMMAoGA1UECgwDU0FQMRYwFAYDVQQDDA10ZXN0 +LW90aGVyLWNhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwQui3NW+ +KkSknJxpgE8xxjihwZ10xfQKTie8zj8hrwEhkxqc1xHid68XSQ51oE/k+AdtbUoE +pDLWySSh4fLIb46Ox+t1RFMlsGitp0WRRSYs0Ri8Hldqpqb22r7xZvIscCEGtTjR +bwVuWcli8AUNdFY6Ak/sZjmv8wnj1OQWfm+v4YxOtgvsTq9s13/xY6Whg7DTWh+w +kWFYOKpJV1x1aT7QnN7q6/DXYYYHE3nW3sJMCGq71JGbsdK8dOyAM42sYz+8n5fh +4Dv0G46c+mFIPqnrMQrp1PVAL5nm3piUMkKH9kfOIWvGWQO9FZNOL4tG4qN661It +Zknw3zcha77f4wIDAQABoxwwGjAYBgNVHREEETAPgg10ZXN0LW90aGVyLWNhMA0G +CSqGSIb3DQEBCwUAA4IBAQAfm2inwlQB/X/VIun4bcKcVLlCZtvQOjXhsS+Z//Ju +1N8j1kg46asnF3JG+sH0Cndfr95br9+HNsn64B9B/n1MKkc9iMQUaEC52gcGMMxM +Tf3Gz7Kut5p3voOThnILPUXeY9lCisTZ0UEgHdTIgy/RnJ34sRB+hABNTjzGNBNt +tKI4qbTERIAadNIfY2z9HvuechG981UbZEnMZmt8/B8loNp/rOPfVBLAh0toK7om +7VMJ72p8L0QSoYKJuVZ3yVanHB5MvbGUvX0TSb5tSGbKrJDZoiwpIbrfn9judhrW +kuWwF5NB2wkMdmY//HkaiFrtQUdeorGrq0duX6Fu/aFe +-----END CERTIFICATE----- diff --git a/tests/resources/charts/gateway-test/charts/mock-app/certs/invalid-ca/client.csr b/tests/resources/charts/gateway-test/charts/mock-app/certs/invalid-ca/client.csr new file mode 100644 index 00000000..44e9c58d --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/mock-app/certs/invalid-ca/client.csr @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICrzCCAZcCAQAwPzELMAkGA1UEBhMCUEwxCjAIBgNVBAgMAUExDDAKBgNVBAoM +A1NBUDEWMBQGA1UEAwwNdGVzdC1vdGhlci1jYTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAMELotzVvipEpJycaYBPMcY4ocGddMX0Ck4nvM4/Ia8BIZMa +nNcR4nevF0kOdaBP5PgHbW1KBKQy1skkoeHyyG+OjsfrdURTJbBoradFkUUmLNEY +vB5Xaqam9tq+8WbyLHAhBrU40W8FblnJYvAFDXRWOgJP7GY5r/MJ49TkFn5vr+GM +TrYL7E6vbNd/8WOloYOw01ofsJFhWDiqSVdcdWk+0Jze6uvw12GGBxN51t7CTAhq +u9SRm7HSvHTsgDONrGM/vJ+X4eA79BuOnPphSD6p6zEK6dT1QC+Z5t6YlDJCh/ZH +ziFrxlkDvRWTTi+LRuKjeutSLWZJ8N83IWu+3+MCAwEAAaArMCkGCSqGSIb3DQEJ +DjEcMBowGAYDVR0RBBEwD4INdGVzdC1vdGhlci1jYTANBgkqhkiG9w0BAQsFAAOC +AQEAUsdzm7CNn8kKo3ZexfR4utj0mVljCHgLWLWVDgOYDkJ/eQ6PT2bZ/VjSBABI +XMPcgeigN+6I5CcsTEQ8RIBCw9q0YsOS0oI7GtLUA6bQbv6OUGdyP/4tVenJryHt +c4DVNRybl+YzkaIEmIvuikiNUfzYrRRmDRlQJOGmk4rFlTlgyKpRQoRwWMtwXSyD +KvM/F5sTV8YgOzcuXDr7bLbf8MRwrnBb+UvKlRJ91x0Z1EzruS40bIu8yVfVG84p +3oJoBdD1ROgoLEcCnSsq3IlN6uTlNm4CUAmbxudNa0jrNI7PcmiMcfbl+krV8ZBH +SWkSacW6AMKvsGDJjL4MdU4YQA== +-----END CERTIFICATE REQUEST----- diff --git a/tests/resources/charts/gateway-test/charts/mock-app/certs/invalid-ca/client.key b/tests/resources/charts/gateway-test/charts/mock-app/certs/invalid-ca/client.key new file mode 100644 index 00000000..947000ef --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/mock-app/certs/invalid-ca/client.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAwQui3NW+KkSknJxpgE8xxjihwZ10xfQKTie8zj8hrwEhkxqc +1xHid68XSQ51oE/k+AdtbUoEpDLWySSh4fLIb46Ox+t1RFMlsGitp0WRRSYs0Ri8 +Hldqpqb22r7xZvIscCEGtTjRbwVuWcli8AUNdFY6Ak/sZjmv8wnj1OQWfm+v4YxO +tgvsTq9s13/xY6Whg7DTWh+wkWFYOKpJV1x1aT7QnN7q6/DXYYYHE3nW3sJMCGq7 +1JGbsdK8dOyAM42sYz+8n5fh4Dv0G46c+mFIPqnrMQrp1PVAL5nm3piUMkKH9kfO +IWvGWQO9FZNOL4tG4qN661ItZknw3zcha77f4wIDAQABAoIBAFQEZ3ZrhF9LDsWm +gXg5f3VA8o2cpNT+uHl5a//rlBJhkKZAX+BuxTzHtH+0TldeTk3wlZyKKWj5Q2e5 +jMcU7k03I0c5YAlDktSrSmDRsz8ANWMvu7gM3br4Udm0XsYqQlLu3MeEmgoSuAtV +zbyexlNKr+aPuFhpZP2G4WSnfG68FXipGY/L2YQ39RhgxYESajlX6laxpLUYDUg9 +D1P3PuIk98pmco7dewW+YNx3Ngkzk1ivNDC99P0zpy/JfkMxe6imoSGObD5lzgfD +WKnCGxcIPC9GkDo8zgvN9z48QH0Mm0V6JaIlCSWhR5wbn9eYHU+w1y4jYemg3RYL +Ej6rGBECgYEA6WtS0nW390OVlafRvMiqZdvDXh6yfulC25u8VovL+DpC4PJ60cjt +bfEDSQBH0QbbYCpCsbZasH1upB3kab6eyHbWhAhH/eVb96xDEVN/FC83nfm08cCU +YLMwIGca4veB6Kf/sgsnmJ6rnubIS0v6go8iBHWk0tbkHlTWq+j+fPkCgYEA07hy +kNLKb6mADnPdywpUHvJJNJZRlSY5Ff8tlilpvo40GD09xj90lx8Om1VVReC4HFcw +8a3TWaSqRxsZFhm/ThlbNnQLIhum6KqOF8fsZotvpujr8LsOinMIsu0I760kAsuX +ADdg/IdeweVrJcFze0vwMOt/q2HAdzWoiU/5xrsCgYEAjjkykcHggezQLAvBJAIw +sTeiZqrVn7aJYj4WF7W+ZlU5gs68Py7qXF7J3aUqHRbMfF/Dm3y87WTAEYeVMUlQ +flzKgFB7bRxfWR3BD8GMYMQUY1FPCy6IOhN0c4nfPAQLR7N1fQqG6dtkPsHnsNlu +njaQR59W+pCtFj4jP0QMLCECgYBpZYva3qSaG8Y8659BAX5I/ZJF1IL+fc2zTpny +A+G5U+9JFcuX0mUHChXqa/uMUsc0jI838LGjEZ8W0L2XS+/5QBQxMmmMbDmV37nm +ysa7cbR+Ybt61pPxhjyRXgCx1/5yScl8+RSWAgnA+qVxYTFM8su6frHKrlnyvkqN +OLv+GwKBgGGNIp/ie4vMgWNa1JuaKD1bpzRUq9aI3bIsCRJw/rRkpephAFAZTh8e +F4Knbqj7okpCRu6lVyMmf1IKeRxXz+pSr2tHlp9JAsdt271pP5DWa8iwjEfmIIHU +Nf2Op/ydxNO9sZbW9PXWM449RWGt1yweZD4lGIMq9oXJbJL8VhAc +-----END RSA PRIVATE KEY----- diff --git a/tests/resources/charts/gateway-test/charts/mock-app/certs/invalid-ca/server.crt b/tests/resources/charts/gateway-test/charts/mock-app/certs/invalid-ca/server.crt new file mode 100644 index 00000000..c682341d --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/mock-app/certs/invalid-ca/server.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDHTCCAgWgAwIBAgIJAOkxC9iq+eDPMA0GCSqGSIb3DQEBCwUAMD8xCzAJBgNV +BAYTAlBMMQowCAYDVQQIDAFBMQwwCgYDVQQKDANTQVAxFjAUBgNVBAMMDXRlc3Qt +b3RoZXItY2EwHhcNMjQwMzEyMTIxMTIwWhcNMjUwMzEyMTIxMTIwWjA/MQswCQYD +VQQGEwJQTDEKMAgGA1UECAwBQTEMMAoGA1UECgwDU0FQMRYwFAYDVQQDDA10ZXN0 +LW90aGVyLWNhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0oKsACRp +RQuOEKmEV3V+nsHaxrAbRdAaLJHltoHkI5He1xdOIvhBWps/Ms81XydybOOrbbrz +qZdhYxG1WwrP1NVf3lfG06I//TeBS9vTjLecXc1c/1kJIWeHbgq11dg/E2vxDUVJ +2jIV4xNf/lcmV9nb3C5x5PUGNfju7im/DV/+9x+dN/kFBry5AbXwwZzm76AZ19sn +EN6sx02sFXIKTXNi1xNo8AQcjZfPPrHIQZH1wUxC2zVaRE0SNzMlII/djJgBOAKz +RoMPK7DxPg4VQphDFErtuUhxmcFadyFEBRCn2lGDShRXv/7AWLZr//2cVjAV69Xt +wjVfD94U+tFhFwIDAQABoxwwGjAYBgNVHREEETAPgg10ZXN0LW90aGVyLWNhMA0G +CSqGSIb3DQEBCwUAA4IBAQAW5ZtDgr5vgzt8OkbZUgPisB19GypSLv6tg+wETh78 +Cz/Yqa2K67/z6doYUFzR+uoqJthS+8uuuMR9XNBbVjpFfZuLQDFO88tNaCqPArVw +UEZb5iL9rMBIKm+tkbl/gLp/nECMIiIn4a1t9BA3BRR3nuJwgkFINjf4XNM/wZK5 +4W3iptOYeSVnAZ4c3mMj8Gl26vwXy1gynglDbvL2PYxl3cV6bSk3DqE2bAt5SE7G +Gx40iUUjtGx7wD0QqtFjCVNpQeTD19ELrl4+fX/K4kkaiFjjAWqE/+KmwXfXqN7q +jPEwf/npYKRMt5a+JDotAsLdNwoAzQfglVVRvVs0Xkui +-----END CERTIFICATE----- diff --git a/tests/resources/charts/gateway-test/charts/mock-app/certs/invalid-ca/server.csr b/tests/resources/charts/gateway-test/charts/mock-app/certs/invalid-ca/server.csr new file mode 100644 index 00000000..b4f9fa1e --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/mock-app/certs/invalid-ca/server.csr @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICrzCCAZcCAQAwPzELMAkGA1UEBhMCUEwxCjAIBgNVBAgMAUExDDAKBgNVBAoM +A1NBUDEWMBQGA1UEAwwNdGVzdC1vdGhlci1jYTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBANKCrAAkaUULjhCphFd1fp7B2sawG0XQGiyR5baB5COR3tcX +TiL4QVqbPzLPNV8ncmzjq22686mXYWMRtVsKz9TVX95XxtOiP/03gUvb04y3nF3N +XP9ZCSFnh24KtdXYPxNr8Q1FSdoyFeMTX/5XJlfZ29wuceT1BjX47u4pvw1f/vcf +nTf5BQa8uQG18MGc5u+gGdfbJxDerMdNrBVyCk1zYtcTaPAEHI2Xzz6xyEGR9cFM +Qts1WkRNEjczJSCP3YyYATgCs0aDDyuw8T4OFUKYQxRK7blIcZnBWnchRAUQp9pR +g0oUV7/+wFi2a//9nFYwFevV7cI1Xw/eFPrRYRcCAwEAAaArMCkGCSqGSIb3DQEJ +DjEcMBowGAYDVR0RBBEwD4INdGVzdC1vdGhlci1jYTANBgkqhkiG9w0BAQsFAAOC +AQEAk6YOJmRp+2mhymrdtBJdAbNEYsHgKw90oLFTvlxG9l1rGsrrTF1mYRfFG/cX +19S/yxIshcJmBBB1o/U9fQFMbDq4uuATjGX5sSh7IALkDOq0yVwObYuj1McguQo9 +B7IRUV5LO/lkOB5dY/LWPXOt9MKTv1Q3vTuSkMdRRe6nhiQ3+n2GcV8TVg2rCSdX +gvVs3e3HaZ7Yiz4nZ7sWVmytSK8Rnrz81Ss9iFHPcEVeiZlN3CUQUejhjf1/4DJ8 +jaZz8B0Aw2F7SH/NxNSu0KWEMaBYuZ5+DIWGYgQKCUktF2i2a7I1LQwzyViAY+P6 +ZXFmhzf0N49oueKgX5XqH6AKlA== +-----END CERTIFICATE REQUEST----- diff --git a/tests/resources/charts/gateway-test/charts/mock-app/certs/invalid-ca/server.key b/tests/resources/charts/gateway-test/charts/mock-app/certs/invalid-ca/server.key new file mode 100644 index 00000000..5b8c6d82 --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/mock-app/certs/invalid-ca/server.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA0oKsACRpRQuOEKmEV3V+nsHaxrAbRdAaLJHltoHkI5He1xdO +IvhBWps/Ms81XydybOOrbbrzqZdhYxG1WwrP1NVf3lfG06I//TeBS9vTjLecXc1c +/1kJIWeHbgq11dg/E2vxDUVJ2jIV4xNf/lcmV9nb3C5x5PUGNfju7im/DV/+9x+d +N/kFBry5AbXwwZzm76AZ19snEN6sx02sFXIKTXNi1xNo8AQcjZfPPrHIQZH1wUxC +2zVaRE0SNzMlII/djJgBOAKzRoMPK7DxPg4VQphDFErtuUhxmcFadyFEBRCn2lGD +ShRXv/7AWLZr//2cVjAV69XtwjVfD94U+tFhFwIDAQABAoIBAQCbkihs3nvRo+10 +kOKWA+X0i40UAvfUyytcvuHF1A523wmRac679z3NKSg2c32c+bkNkd+R83S5Y398 +SIz/YGkhgCMeXT46DxE9IDT0i9u2hccQZ4GP0Av4XNtwTof9JpfO0ZnOVeNzVkpo +i1wIyf0zNXTPLp/LNe1GG9bvuXhQ98ZfuvBYZsOpNdQJUMN5gSqzRMMplfhaKkg2 +KwF31ydbGhTBZhqhSW2oaWJVCMz09+BkKMmnrSBNdYR1NV1KBkx3Scj6znBflmxW +K2zNHF78xuioXtFPt2B7Lqu8uZvZWT30SescYUlMct9dgn09CesXVwXTrZDkvinr +MUb6OMYhAoGBAO8Iug2TFrPX9mlUQLXOcagX3pNnsQQe4Mn1x2rw4xvrw/s6VJyu +qXm3ffCvvMLyTZ3i2SY2I+VLHj6FeAv7BeeZZEj00CtyXvvALIPZHxmlPa6BFP9u +xln+l3VbS1cyFUry39uz3WELMSrWaV2wcssiq0L7uaIo38IaOb07EOixAoGBAOFz +quHNbgGJ5DShUAPd3qD9Yc9uYJmmVbiSuXVgXhMpwAYJXxnaFSlo7lPYR2s33kaA +BpvnrdwHqUkNlKBqPRwsOOoGFnZ7JGYQNXsg/E1cnZ8MuwfQCTVTCkV1c7mnasRa +IJ+b8TdG9gUupqo2qW+wrreYyhsNzFYAyHgkZFhHAoGAdQvF5v2+YSP/8gWihiPn +vZKql21v3X+tPNeP5Yq8+qAQ4ETox6wzKnmyPpgfCyqQ3R4GjNJ380A8OAstBFjP +xF91HtBZ2txvLEEmyw0XUHx8XqWwfX9luw2SZpHkq3bHvGJ/QVqqrWlIkxxYjdrn +6xY33F3cwU3Ye3hSC5oPppECgYEAl/04mJ27qcHiXTDbFqA+9F2d0Q/ig/NFGvef +m+fpxBWDZQ5wVKdXWOFquo+2Jiw152VsDzLzXMC1eZB0QGke5Z1SiUKtZhbChSQs +SeQE88qaYJ1egXfYnWBsLkNuTxz0t4bjM3cX+WIXfYrjxSCwvaFpSFDy/6YfuWMx +wv0VwQUCgYAZ4kpCK4bdzRMf2jBZCFkcZUtLKSYWj5kgCTFgtNzpKesjKRP19+FW +PX5Hpi2oxgS2ivXaZ7hMU5AhMyPQ/bNwy2A1Xhvex+hktATw3Gq+/NAeHEeGy5iX +bENXVVE53x4rnbSpXxDoKF4yla+EBRHOrcRmHTdgaO1pKZ1YZKCnUQ== +-----END RSA PRIVATE KEY----- diff --git a/tests/resources/charts/gateway-test/charts/mock-app/certs/negative/ca.crt b/tests/resources/charts/gateway-test/charts/mock-app/certs/negative/ca.crt new file mode 100644 index 00000000..4ef28553 --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/mock-app/certs/negative/ca.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDLjCCAhYCCQDlLPrl+EG3ZzANBgkqhkiG9w0BAQsFADBZMQswCQYDVQQGEwJQ +TDEKMAgGA1UECAwBQTEMMAoGA1UECgwDU0FQMTAwLgYDVQQDDCdtb2NrLWFwcGxp +Y2F0aW9uLnRlc3Quc3ZjLmNsdXN0ZXIubG9jYWwwHhcNMjQwMzEyMTIxMTE5WhcN +MjUwMzEyMTIxMTE5WjBZMQswCQYDVQQGEwJQTDEKMAgGA1UECAwBQTEMMAoGA1UE +CgwDU0FQMTAwLgYDVQQDDCdtb2NrLWFwcGxpY2F0aW9uLnRlc3Quc3ZjLmNsdXN0 +ZXIubG9jYWwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9adsQ3NJj +UsSh4dmV4U0A5RSKrexJS/Y/MrLCGz9c7r1JvRuLpI+AlGMWYnPLW87Gz0XFpHVJ +wX4YnpKxUcQrbNcMhCo2sOvZr/9ignWFtJK3HGpv5Bz7K/ZBPtTYpEYT6LuiKO1K +Haw61AcnsfLc4Dr1WsuTuEYJlR1zYfzp/CkiuwZJslb9De5Vkav6TVkn65qcPNt8 +GHuARxr9jrBn1vEKBKvCYWBTG5Ia8HA18YCJvAJcMYpBE0Vr0QXOuhBMNrcGwYm2 +ZvMrb734ZlYXFSW/tbaps9Zoc5/DoRUxesZbItRMEAKJ49PxLO32tliEAyWf4fEa +1yeUnLSvriDDAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAILey15A7O60YSDPKgLR +UTvB9WU7W1ulPeMgzqFjub6HOenTsVxqAKZGYniJUPrZw6QKi30XkcJQIFqcrWQi +N3tt//EAKotZ14rxZ0WT7n3je98gMVzPrOEr8xLYi39xGBXHfWfrBhAlOXdtOlgq +/kO0T8eleL9+LUUq784JUdjl+cigDhrAaQ/5UuZ/E9WSW+7QnGMC1B1STjel6Zuc +MPSoSPK7F3zP4a5AtFk333UIihmOwkgeMTdtrYY8phr4VvYJ4PmPwhR4cmHnTNmf +rwna3C0yyR9jv6RtB6V7tq3EMJBfViUIvSVyHeMN2+JKyrBWXF4FT8kC8zp3DwUm +jN0= +-----END CERTIFICATE----- diff --git a/tests/resources/charts/gateway-test/charts/mock-app/certs/negative/ca.key b/tests/resources/charts/gateway-test/charts/mock-app/certs/negative/ca.key new file mode 100644 index 00000000..b6a4f0f4 --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/mock-app/certs/negative/ca.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC9adsQ3NJjUsSh +4dmV4U0A5RSKrexJS/Y/MrLCGz9c7r1JvRuLpI+AlGMWYnPLW87Gz0XFpHVJwX4Y +npKxUcQrbNcMhCo2sOvZr/9ignWFtJK3HGpv5Bz7K/ZBPtTYpEYT6LuiKO1KHaw6 +1AcnsfLc4Dr1WsuTuEYJlR1zYfzp/CkiuwZJslb9De5Vkav6TVkn65qcPNt8GHuA +Rxr9jrBn1vEKBKvCYWBTG5Ia8HA18YCJvAJcMYpBE0Vr0QXOuhBMNrcGwYm2ZvMr +b734ZlYXFSW/tbaps9Zoc5/DoRUxesZbItRMEAKJ49PxLO32tliEAyWf4fEa1yeU +nLSvriDDAgMBAAECggEAR5KEUK7gYN+ZpYHt8hCcREZLqMtniZrGhcLmgSpCmx8r +L33htraL8w4fEwpIrwMV81HHD5PBLgmLWEozLAW1lqMd74DRYrEfrbYvTk31knxV +JBP8tCMCQHawKp9PVj1crZE3tWK5p1PnDKOpwHohRw0DukqAumTbMivCYSMZqmAT +riaJYBh30mabiHEBLhcvkLh8lg8i2LsD9Jjx3SZcVpwrGaADiR2YtFLTOhVS08Uv +Lvp1SrvGhnyu9lTBI+XTq/vqrPCKsfM3JKSbzKf6HamUYweAGiIbjnro0uhpLLD6 +BXe9N1PMJjRO/w2jvO16zJItGePjyvrkx4W2nf71gQKBgQDlOz3xp67UK2Dmre80 +riplj7Vd9AL6n+5AdFR0yktoc7+t1SJG9j9XzSzKTH+Ix8g/oasxF3FBoLnZ0z9M +tV0xnbEXTXfu1vnsQxsB6CTwBpgZvTC2JLpyPZP8YFMoOvLTET1xI7oKvvlTyZHo +XSqk25cTjwCd8nODJglOlyRoUwKBgQDTiEb1OIKKRhfmiwO/Id9oyE1BbW3DgK3C +TsK9dXZf7TiygNAQQ/7zCuED9jbhTJWkxBLsbGUib8VUaJsS3SrXIyJbCDErsn/p +rJ+oKTEpI24ZJyg4iEJmBayT53KFFF0f6/ig/GlIsc/Gn07oi5kaJOr/tKiduWCL +JFN9i2SX0QKBgQCqU3KbdLT7AaBmxybORftKq5Vf0kfEYcFuMwHuJcISQq9SQuPN +RnuaieGWD3FT+N5aKY5CU+Dbmsl9iPGn1bsBeuJzJiTPWv0pCFOw/wUzNDMgLOtc +67191TN4ezpO0j5LhqvYvWsnQO+RylyYA2IETQXcio0yz0v1TvXrZ3Kt8QKBgQCe +y5jpEYj9oGzkxssDOrxp/qPwT+Osdfb6/QE4FOvOS1jat9R5wXGspigRP04nh8R2 +sjK6hQzO8zUhjn2LhbhZVKi/ycCP2yonE02vgWzEQzKtczXAapnd2LibN45C1Oyr +wAsfXxzyU3l007b634EJnVlEqCxEaxtMmPKMNo5HYQKBgGQQpp3F0SVAMO5IOZ7B +2aDT7lYKCyDg5OoXAJ1/MQNWG6GaXGWAZwE4NHBtocA9Hp+/7ouLwV24ntVQ9Gv4 +leGyO5GxRdbGZn9YUE9huAfxP7Y4ejij82alJctcs+77XGdURKWbp+0NECQdpa2O +VOUeSCHmcdgSXQHmOdcGDB3e +-----END PRIVATE KEY----- diff --git a/tests/resources/charts/gateway-test/charts/mock-app/certs/negative/client.crt b/tests/resources/charts/gateway-test/charts/mock-app/certs/negative/client.crt new file mode 100644 index 00000000..1d42e07f --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/mock-app/certs/negative/client.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDazCCAlOgAwIBAgIJAOkxC9iq+eDOMA0GCSqGSIb3DQEBCwUAMFkxCzAJBgNV +BAYTAlBMMQowCAYDVQQIDAFBMQwwCgYDVQQKDANTQVAxMDAuBgNVBAMMJ21vY2st +YXBwbGljYXRpb24udGVzdC5zdmMuY2x1c3Rlci5sb2NhbDAeFw0yNDAzMTIxMjEx +MTlaFw0yNTAzMTIxMjExMTlaMFkxCzAJBgNVBAYTAlBMMQowCAYDVQQIDAFBMQww +CgYDVQQKDANTQVAxMDAuBgNVBAMMJ21vY2stYXBwbGljYXRpb24udGVzdC5zdmMu +Y2x1c3Rlci5sb2NhbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM/h +JSE+dkK0OsEb9ahdwkSn4kmtDtl6MeiRKCXjJgSH0qjPeWWQEqSPhTlCJbHosXHr +DCHoXG/UsbmThIpZwOttZ9OdzwrNXLZT5T7QtKdqLqh/wfYyzx5kFjO+sTcFhuCn +fmiaXoqtZpQXjqYfAJ0lY8VnZJQpEMoftLPDlTju5HoMkBMdANeKtqfvNzPAXnKr +jlaRa7GS/X4RguRSEhkVTryYs3hRqS3ynTdj54d3uWxdIb03QqH5p9VfjHRfLdUk +itIxMYXgHYclN9K5bcHDH+56WvOOeRtJCZGExHO+vK7fh84TN83dHCWZK8iEn/dq +1lv9GsIrj+pHtj5SB2sCAwEAAaM2MDQwMgYDVR0RBCswKYInbW9jay1hcHBsaWNh +dGlvbi50ZXN0LnN2Yy5jbHVzdGVyLmxvY2FsMA0GCSqGSIb3DQEBCwUAA4IBAQAL +jU+uhzGjpO54mrJSOTCBNb4LQbLrGDF5MjjzVoXbXm4j3c7a0wzIhTQ9tnV0PvW3 +ozrF0qmE4ihfoLF1WRzB1e+kAT26VpDNEsTs05J8mt58jKuLifrv2OY3Wv65zj0l +cDETnR8xaLL/jfLPVGqDdt1CUbhxb999Nv0rW0Jg+rsqzXzpIs+4Aiwf6FsaAhZh +7YxtbcA89R6Go6liN9wwx6jq2n1GcXlf/itx9Tru5ouv3vZUwS51EHMMHm2Sdueo +mwAxyAf/ALdEAv339qz4s5d4814RuQD1oAoYu7uEiTChOuOnqGLvq5XPSwK8qsq7 +Qeyr7rSduo0I2586qnRy +-----END CERTIFICATE----- diff --git a/tests/resources/charts/gateway-test/charts/mock-app/certs/negative/client.csr b/tests/resources/charts/gateway-test/charts/mock-app/certs/negative/client.csr new file mode 100644 index 00000000..7f7d9ade --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/mock-app/certs/negative/client.csr @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIC4zCCAcsCAQAwWTELMAkGA1UEBhMCUEwxCjAIBgNVBAgMAUExDDAKBgNVBAoM +A1NBUDEwMC4GA1UEAwwnbW9jay1hcHBsaWNhdGlvbi50ZXN0LnN2Yy5jbHVzdGVy +LmxvY2FsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz+ElIT52QrQ6 +wRv1qF3CRKfiSa0O2Xox6JEoJeMmBIfSqM95ZZASpI+FOUIlseixcesMIehcb9Sx +uZOEilnA621n053PCs1ctlPlPtC0p2ouqH/B9jLPHmQWM76xNwWG4Kd+aJpeiq1m +lBeOph8AnSVjxWdklCkQyh+0s8OVOO7kegyQEx0A14q2p+83M8BecquOVpFrsZL9 +fhGC5FISGRVOvJizeFGpLfKdN2Pnh3e5bF0hvTdCofmn1V+MdF8t1SSK0jExheAd +hyU30rltwcMf7npa8455G0kJkYTEc768rt+HzhM3zd0cJZkryISf92rWW/0awiuP +6ke2PlIHawIDAQABoEUwQwYJKoZIhvcNAQkOMTYwNDAyBgNVHREEKzApgidtb2Nr +LWFwcGxpY2F0aW9uLnRlc3Quc3ZjLmNsdXN0ZXIubG9jYWwwDQYJKoZIhvcNAQEL +BQADggEBAGy7cJLILEJ+ZnYliPgdGBbsuXKnhhQxqW/0nlq7Vn7OeO0ExxfODw9w +4sXWuCTOa4aUlBqpn8d3YSJr4d5RADt2NR/KjxtBhHqwSo0g6yCqG5Vhr2KEQIe/ +I3z+HVpmgEEr66gko6/H3zfQAU7J7wwujfpyWm95d9HvOmqRhth2xnNNnThsjTiV +8yPxehA6bU4gzwYUm+qJ2F2Uu86ybAEGFtRV8RfupQbb2nzyfJv/RAEpbpCAR9MD +Yk2HkkGbvlnSJosQ21PlYvw0A5ZuBiq9MiNBsnAa8iF9jGPXEwXP86yLX2h4G7xB +gg/eQ5n8VMpuXxiDyRrONYngXTsiYxQ= +-----END CERTIFICATE REQUEST----- diff --git a/tests/resources/charts/gateway-test/charts/mock-app/certs/negative/client.key b/tests/resources/charts/gateway-test/charts/mock-app/certs/negative/client.key new file mode 100644 index 00000000..678c296d --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/mock-app/certs/negative/client.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAz+ElIT52QrQ6wRv1qF3CRKfiSa0O2Xox6JEoJeMmBIfSqM95 +ZZASpI+FOUIlseixcesMIehcb9SxuZOEilnA621n053PCs1ctlPlPtC0p2ouqH/B +9jLPHmQWM76xNwWG4Kd+aJpeiq1mlBeOph8AnSVjxWdklCkQyh+0s8OVOO7kegyQ +Ex0A14q2p+83M8BecquOVpFrsZL9fhGC5FISGRVOvJizeFGpLfKdN2Pnh3e5bF0h +vTdCofmn1V+MdF8t1SSK0jExheAdhyU30rltwcMf7npa8455G0kJkYTEc768rt+H +zhM3zd0cJZkryISf92rWW/0awiuP6ke2PlIHawIDAQABAoIBAQC2yHfWYE6p3kFf +NQ9u6Gn95kRhlepdrUUfAit0DOOLzkWbqzpJ5EGQMqXor9HnOfx0d0Emu2Iz7qgK +zbwXzk2EdKF7f+Hh1Kq1otUKw4ZlQkceX5+TtB9L0KN5Ai5ee9yZwoyyuzFv7IIq +qwAB73ahtpOgqoXUhLs/jltcSRf3gvcPv7daADmwnRndr1leoaF/hbvjS5OMv2IT +aQ3F6d6VUzfVpJKZg1PSn4FYxIY1qc7O5hqm1lym9KZGP/D4tELmEXc/oo/cWsuS +i8HSE6qj+fFwzDv+m6FdLhZRtP1/0h4pIwW08ITHmmKoXmlEyjZ+OI4VQNARAqu0 +DM3vEWo5AoGBAP4CzBwAmlIhK+WMs4c13eo/Ab9ssty07sclUgIQCgfxNUIy3Afy +6/X8al5mcWRHdggKhnsHFC5EHN/qrcmmzWcXRt0W5Nbx+SErF3BjpzjFOXMC0yST +puPkOPCT7EhNaplyxvyOhAFSfzxB63XNgG2CQ1iQhdwOfZfVoeu0q0RPAoGBANGB +3tC6B9/7COsr7Dgww8TVMlV2wv9zwkn2F6+2FA0XZhZ+jIZ/LHKhcA95vKJLIpHi +08vy62LQzWQaTu1j+oqg5ivb0X1IdgRxheYLJduECnQKKU/6QKxX+tw0XtjKVI4p +GU8J/ZV9L/ksPgg0x8FigR4oOv59k0i9fxw5ClglAoGBAK2qLfCLPPcf9Mopq2ir +HIEV+NTutU8OaR5A1tPQMXuCn34WFbddj5QLspG+CpKcBQe0YoNksJh9Oxygb5cp +8s8j6/AmwehvYXwa4RiXGXJH7WJDsSYVyQmQNJnPGMHKJDKrdX6g1YGt7I2/KAPP +r5mvcOnxTYPJaHbRubXUPTAjAoGAGR+cy6TzWs2sxR7QRfC7GTiDv7HtMlr8Wogz +UPPhtawvptToHxzTBLANUx3DHCcsbxgnU9a+mWv2pWFuQ5NwsP0YfPvwRDjTRjci +2nJNyOQtqLqrN5cH+GLYh12UXiTtPNr62PqWuT146kV+7tb9eVhJqYcjg+8lIVzw +CD9i2S0CgYBKeCAjx6wYdvN/Zqn53I8YxgNgxWBJh+8RHBz6zOhhGQQWEfADkDmp +PjcKcyCnO52G20QA0y0/VNMKw35oZYSfC6oLT58ZBkeqEy8dQ994dYgACAq6BOyx +B3ZCtkOakOzq9xAFxJ35XaXjKSl9e0xPNzdbfIDOp69m3kV/U0UEWw== +-----END RSA PRIVATE KEY----- diff --git a/tests/resources/charts/gateway-test/charts/mock-app/certs/negative/server.crt b/tests/resources/charts/gateway-test/charts/mock-app/certs/negative/server.crt new file mode 100644 index 00000000..17160d8e --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/mock-app/certs/negative/server.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDazCCAlOgAwIBAgIJAOkxC9iq+eDNMA0GCSqGSIb3DQEBCwUAMFkxCzAJBgNV +BAYTAlBMMQowCAYDVQQIDAFBMQwwCgYDVQQKDANTQVAxMDAuBgNVBAMMJ21vY2st +YXBwbGljYXRpb24udGVzdC5zdmMuY2x1c3Rlci5sb2NhbDAeFw0yNDAzMTIxMjEx +MTlaFw0yNTAzMTIxMjExMTlaMFkxCzAJBgNVBAYTAlBMMQowCAYDVQQIDAFBMQww +CgYDVQQKDANTQVAxMDAuBgNVBAMMJ21vY2stYXBwbGljYXRpb24udGVzdC5zdmMu +Y2x1c3Rlci5sb2NhbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKwy +l8RsLkqz6/I36/ytAfU75D+K8oUqzKoaBMWwS7xL+52pMersTsAP9be9B4qBu5+j +DGAWF1fAicseOYWZ0FFIFX65BQFmp+OCC5n3kyziMmbP0VJr7i/ILf75w5VTlsz/ +uZPnDJySW+H9J+BeW+NyMeuvX2ALwAy2ZLdT5sPDvt7m2avah4fFJIxV9AbPUdzB +hBcWo4M4PY0ePiUaDm3nv9c6sXHqtQu493mEWKO0o/tL4aATEm6QdFJjhCrZEL0W +T/Ws/Pm0CSy+wZh7eXJ2sonW7iGu0MDRKJQWz7yRJaugSY3YnJe9brYTPzLkd6sj +vLxvw5HdWDth5tkCKG0CAwEAAaM2MDQwMgYDVR0RBCswKYInbW9jay1hcHBsaWNh +dGlvbi50ZXN0LnN2Yy5jbHVzdGVyLmxvY2FsMA0GCSqGSIb3DQEBCwUAA4IBAQCK +POco+zTxJ0XsgCbtV8p0CqcqTzazYAwkj7M0V+ncVYuXfknz/DxV71dB0iTYkY5I +Fquf3zt00eK4y7fzgjLQEyZqh8uaIUlXvpMvpd867QlM+p7+NT5uuuW/TiwxLP54 +y2H9heMq+NCmwHRq1YI9htnFil7DaFGyNXaqrJj21RaiZT3S6B+oNPgYBA2PdJVq +6aUKZ1n2/9N+frJ/rFvTBqqIAqbIYSFXrvb2d64WCIcY/RKBR2Ilt1X8mn3ZKk4k +0dAHUREc8lCDOIdj6cUNy7vxuTd/FjqTXxcIIJxsMaRAD3iCLElMRMYSnY7uFQKL +jX6ZeR0ei6/D/j2PPdCH +-----END CERTIFICATE----- diff --git a/tests/resources/charts/gateway-test/charts/mock-app/certs/negative/server.csr b/tests/resources/charts/gateway-test/charts/mock-app/certs/negative/server.csr new file mode 100644 index 00000000..ad8a006b --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/mock-app/certs/negative/server.csr @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIC4zCCAcsCAQAwWTELMAkGA1UEBhMCUEwxCjAIBgNVBAgMAUExDDAKBgNVBAoM +A1NBUDEwMC4GA1UEAwwnbW9jay1hcHBsaWNhdGlvbi50ZXN0LnN2Yy5jbHVzdGVy +LmxvY2FsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArDKXxGwuSrPr +8jfr/K0B9TvkP4ryhSrMqhoExbBLvEv7nakx6uxOwA/1t70HioG7n6MMYBYXV8CJ +yx45hZnQUUgVfrkFAWan44ILmfeTLOIyZs/RUmvuL8gt/vnDlVOWzP+5k+cMnJJb +4f0n4F5b43Ix669fYAvADLZkt1Pmw8O+3ubZq9qHh8UkjFX0Bs9R3MGEFxajgzg9 +jR4+JRoObee/1zqxceq1C7j3eYRYo7Sj+0vhoBMSbpB0UmOEKtkQvRZP9az8+bQJ +LL7BmHt5cnayidbuIa7QwNEolBbPvJElq6BJjdicl71uthM/MuR3qyO8vG/Dkd1Y +O2Hm2QIobQIDAQABoEUwQwYJKoZIhvcNAQkOMTYwNDAyBgNVHREEKzApgidtb2Nr +LWFwcGxpY2F0aW9uLnRlc3Quc3ZjLmNsdXN0ZXIubG9jYWwwDQYJKoZIhvcNAQEL +BQADggEBAEC85gqso/tBKu9xBcZKDBrQQl4wWyDjUw35X99AKQtZrqT/CcaKITV8 +1jBnXS5rk01QEj1M6w50cuwKnew1ABALav0dVewBtRD9Aa78+JHWpcp7LyPSDct7 +6XjXp9i7XoPzmpLqithoetQJxyxDx2uL8JOJIadoJre7M+/BnzhADeGHCS0cpVBR +8W5xkCtP6y5fn5h0mC/jLRGhq7tA20H6k20q4Y8tdGswo5F4pMuohw15KmYbAUQw +0tiqVdMDLq9IoSgvYPbgxoOR1lsXgZwq1B/MOOFEtMffiMcLrh6ndiONf1OgPJ8f +i9hM65I803Y9ZrMJXmJiKfUcp3ceUkQ= +-----END CERTIFICATE REQUEST----- diff --git a/tests/resources/charts/gateway-test/charts/mock-app/certs/negative/server.key b/tests/resources/charts/gateway-test/charts/mock-app/certs/negative/server.key new file mode 100644 index 00000000..159ed44e --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/mock-app/certs/negative/server.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpgIBAAKCAQEArDKXxGwuSrPr8jfr/K0B9TvkP4ryhSrMqhoExbBLvEv7nakx +6uxOwA/1t70HioG7n6MMYBYXV8CJyx45hZnQUUgVfrkFAWan44ILmfeTLOIyZs/R +UmvuL8gt/vnDlVOWzP+5k+cMnJJb4f0n4F5b43Ix669fYAvADLZkt1Pmw8O+3ubZ +q9qHh8UkjFX0Bs9R3MGEFxajgzg9jR4+JRoObee/1zqxceq1C7j3eYRYo7Sj+0vh +oBMSbpB0UmOEKtkQvRZP9az8+bQJLL7BmHt5cnayidbuIa7QwNEolBbPvJElq6BJ +jdicl71uthM/MuR3qyO8vG/Dkd1YO2Hm2QIobQIDAQABAoIBAQCRiKzmMLwrHMdU +TtkfE6Vs+zJcVfXEgLi7JwRThD1uJhXBWUc8En44KwT0RknCUQUe1XHXH7SY0Lxk +s+XPuYDrwW2RTZQia/2G9dkSRsDXlVEdvZRfAaMsNRZSwgsAAMaZ+aOBkiwBhF0t +sYTrRzSIFXKFjBGiniuxUtHqc3m8hyejQYoEfwfRioDf6xrPN4RaGqrpE+JaBbQG +U4Hay14GbvA3ngMgmXlJYcWEV26UVq4tFv4xpCwxGB1/X6+ovGtn/64glWsRTvMb +zak8BEnP1cSaXGrMAjIzzXGw3WzKib/nDhHzCQcbgmIdhZl1mF0Bz4DtdVKEe07d +uf7suvRNAoGBANbGJ3Zj6/1fzlfA5FWIZnMKMN47HQJdirsG9ZEjm5KZE0EGhjnB +fNbCwPQZZlFwxWuIqp6/Zfw/UoOiEeXiglAhmnA9cPUZkh4UrcK7nMJGor7jficG +DwmNQoXjhnH/KMor31ntgBbhVRLj+e/RtoaxANioxL0Vv4bDhaW2jjd3AoGBAM1A +Q39o0OqvgM1UMgYMs4PverGqKDYQVqHyqxqPmL3G5TR6uAg8Isg67pSX+i+dEJ/5 +SqMY3fDBaU8Xr4/eovwPnLwWq5z8szpCIFg1mRQr76mR8PJwwj4J39DuEQIE8TaS +QUyAh275ookkn5kjj8rfq0+TppalY/U6M2kPN6A7AoGBAMKH+HZjSvzUKjGRpT9T +rHfGYzzmjf/2ehGs2//6II9H1wiuwCTP/CMJg3uVBff+DNK5ltDyy40OTc6snUl7 +QE0UIq5G+GkIIDDeygP3qqTNFduQclMmSbh9GiPrUXsvgeKcmlD5rWsL7eKOW3O8 +n3agHAQh2RDrAe8uaX8POwFBAoGBAI9GcNebn1pzsIGkaFb4vsc2gHtMwE0dEpxx +/SbJXmH7WTxM/fIhqFYFbU2k2Swrg9Nn/cXkMelB2fUwH4labINvkoVpfdpUO/hK ++LEamQUPtni0O3HBbJZJ5ka+KHk0Yf0qExMIFYJOGDuLqS0JOfLwN3GRLBS01xXz +zrdju/zJAoGBALGCYXxGpK8Rs1EVb7+WF5y/AwMngkvtZjYo4vNgn92PPgO+5Y8G +60YpIMqytDh7vecjx2g7UVp9xRB1PUT9sGQajRrAPOF6zY2RkGXHvsVCF0/4fcEb +7JeNT+1lJGPW/Bduzzd74qSDJ/QyB+Km7PRQx19lLHhrz+uPXrW6J+f4 +-----END RSA PRIVATE KEY----- diff --git a/tests/resources/charts/gateway-test/charts/mock-app/certs/positive/ca.crt b/tests/resources/charts/gateway-test/charts/mock-app/certs/positive/ca.crt new file mode 100644 index 00000000..18f0c610 --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/mock-app/certs/positive/ca.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDLjCCAhYCCQC6YSuCf2n/jzANBgkqhkiG9w0BAQsFADBZMQswCQYDVQQGEwJQ +TDEKMAgGA1UECAwBQTEMMAoGA1UECgwDU0FQMTAwLgYDVQQDDCdtb2NrLWFwcGxp +Y2F0aW9uLnRlc3Quc3ZjLmNsdXN0ZXIubG9jYWwwHhcNMjQwMzEyMTIxMTE5WhcN +MjUwMzEyMTIxMTE5WjBZMQswCQYDVQQGEwJQTDEKMAgGA1UECAwBQTEMMAoGA1UE +CgwDU0FQMTAwLgYDVQQDDCdtb2NrLWFwcGxpY2F0aW9uLnRlc3Quc3ZjLmNsdXN0 +ZXIubG9jYWwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC/FAKyCbQq +jBAvUibELXjBK0Adp4f2oCdb/JuEiy0F5vYi8dBval1kXC8Hh3pKPBHfuPAJXzFg +DsoHynDJUdDCtsV7F7LFJ5o/ufQpLz7UkhViYCEF1ANeeD4vLNc6DDNy77brPXtj +7o31j8rTk3ZgKQCo8vpO2sJNjk+KGWjmZS+P+8QtmFYx+LAGr2pHACnZpsuRQVLL +ubZeJnkg676FZj/KWgdk+IVES8Wod+tzMeeGyE9KK2baqAdx0/Y0ifdXP5x1X2Db +XafPt2PQq+Y1UnM5zsjPxXxsPj+VHcsPbXrifVStz7/TgfjU3fwwS/yZ4X/8qfIQ +pJl+kYHVj1DbAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAHLvfIqClXqYPKbmKWiD +7xyz/ULQkcpVXBMMtHytrZ0SUBjqK3TUdgmWJwdNuGQpx9GhvJXc3P9qhJeOhkoZ +zjbWQUL1k15cdFtT46fGEizuPkGqGz0IOIfqkSBifFpKnrzcxt9+Z5BryIwB3nm8 +j5o08MeS20gTM2ngbVdEdXQv6wGHPTtlS0s0HY82+odcm+oGGOVIyylC0EZQClTS +Bcmy26Oid/9VVymF/q7tc3mPPNNltGVp9b1CUBXDD8G1OaN8pZ54cgMLSzgahNfk +H0U4scgVfaN/Lc71E1uunnwBaV22NZcWYvVFV3WY3FMB9atClJEqD9OOqGXerzfH ++jE= +-----END CERTIFICATE----- diff --git a/tests/resources/charts/gateway-test/charts/mock-app/certs/positive/ca.key b/tests/resources/charts/gateway-test/charts/mock-app/certs/positive/ca.key new file mode 100644 index 00000000..1aa41ce3 --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/mock-app/certs/positive/ca.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC/FAKyCbQqjBAv +UibELXjBK0Adp4f2oCdb/JuEiy0F5vYi8dBval1kXC8Hh3pKPBHfuPAJXzFgDsoH +ynDJUdDCtsV7F7LFJ5o/ufQpLz7UkhViYCEF1ANeeD4vLNc6DDNy77brPXtj7o31 +j8rTk3ZgKQCo8vpO2sJNjk+KGWjmZS+P+8QtmFYx+LAGr2pHACnZpsuRQVLLubZe +Jnkg676FZj/KWgdk+IVES8Wod+tzMeeGyE9KK2baqAdx0/Y0ifdXP5x1X2DbXafP +t2PQq+Y1UnM5zsjPxXxsPj+VHcsPbXrifVStz7/TgfjU3fwwS/yZ4X/8qfIQpJl+ +kYHVj1DbAgMBAAECggEAD00K6jbctouAwElT0WHSyaUs/TLtMFKi1DrmOTbr5A0a +qLG0fzeFQwQev/uZT1iAFeo5TobQ7WBBzV3oqjZjATShm7nKFv+U2oWJh8LAxUTt +cXNBMbZIjsgSMrTkh0Fy3UFU5IGH3/i6ZW+eTlMAp7Kg2uaaJLZf2NYMiIKAY/KS +2Nx/+NWcyoMeZm2acQzy6b9cNH90o0SS76JdBB+89XPktuNmhht2oFsE215UQAF9 +PUoVC76DGtdiUjWNY7qrYxvTsn2TeJ+ufQhB0mTeLtA5+sj67jM360wg9C2EHP0m +D70Dp+B6PIvr7RbqaYWKj7yCIa+/pdndzvI2+nKYAQKBgQDtugeYJykQpkx1op+b +INebcUdPWmHq3Q0mi7Ufqj3c5mndSRj749A7yG3NT/jRx1i/xhfgvvXyT9SAVI0d +GAnYnzkS4q7MhRKcW1qblJeObX5k2AfstTu1Zs6R5PhdYDTmKU3/F+9XjQtoPKj6 +3rlwrjcp9hK5ACpdJT/lZtNRGQKBgQDNxAjQcqZpcSP9DA/ESbNKLbm7PuOkrXG4 ++4zUdtZp9AR5ZMK7blQnWKo0IbT4+NYq7ylLIzr5jKf1ecVOgyG5XSMVXMYkbMYD +z1m3Uyry6wcjss2a+RFIp5SLVOddj/CglSHmhIEVsE8gi5Sjo4aj27XzxbfwNLJA +mhkgf3AsEwKBgQDHDxfe2yOysl2hvwvAnQ6NNZyNoNQPEww485E1s5rbhwCsb9IA +0fECrkDrQ4TJPBBffONvqNdPEHOTBbmn3AIaprDm1HOkA+XikUhcsF77v0mv7Ykt +N1CJBE4CsmUZ4z5IX9vUt9kNSah8nxasAqXq6aZ9d3ST/sR6fH91etWFuQKBgQCL +SgTddn8IKbq+9YdGzM09ja6I/o2DUJYHLuGqgberia/trTPVRV5aND8jgx3K3Ee+ +UJ+XaYXmoDyig4f5GfOeU1oIgADxb2Cr+5Uz8GzGfCsdE1Dzc18r26VGnHbycxnk +2o9USKZJVEx8L4CzNWNTUMve9R0K0eFIsggIY7w/WQKBgHyevSW+TYHA35AfvQrW +Js//+9Q82IHK7nS2HeSKqneavJgyN76zWhBQCpB9GcKlnTYnjuj48ehzidWNIhOA +frBONzQZkGVKfpybcDiJ0vVn8hMvRHqggC2Z7MNy9crUdgO6npIOxQoLeo0sUCA3 +gYHtNHv0//OcyA6JXVKQAOcg +-----END PRIVATE KEY----- diff --git a/tests/resources/charts/gateway-test/charts/mock-app/certs/positive/client.crt b/tests/resources/charts/gateway-test/charts/mock-app/certs/positive/client.crt new file mode 100644 index 00000000..cc9cdea0 --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/mock-app/certs/positive/client.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDazCCAlOgAwIBAgIJAOkxC9iq+eDMMA0GCSqGSIb3DQEBCwUAMFkxCzAJBgNV +BAYTAlBMMQowCAYDVQQIDAFBMQwwCgYDVQQKDANTQVAxMDAuBgNVBAMMJ21vY2st +YXBwbGljYXRpb24udGVzdC5zdmMuY2x1c3Rlci5sb2NhbDAeFw0yNDAzMTIxMjEx +MTlaFw0yNTAzMTIxMjExMTlaMFkxCzAJBgNVBAYTAlBMMQowCAYDVQQIDAFBMQww +CgYDVQQKDANTQVAxMDAuBgNVBAMMJ21vY2stYXBwbGljYXRpb24udGVzdC5zdmMu +Y2x1c3Rlci5sb2NhbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALla +/a1nh8anG0mpqOO/UtBDR/fGGgMRKL055DU/oAlKeoc9e3HBcAPp5cBbp+wyzcD3 +qoAzFK+Mp01J59t5uDFS6Sg1nqoPv+ujaj491q2WxN25WziahjBK29Z9MyoR9gCb +QT8QgxRT/hGALUQoKbbApVG++U/z0f6qhaOoseBaO7R8FXux8Q1dTG4vtIJv/mK+ +uKXFakDJdQh8h/kEMaO7pYBqhOpi8obAQL3dJQxQr7Jje0UX9pzeDDiZcy8b72TC +s/VgliiZTx/1c4cPris21kPcTMoOZ1DRbmrsEDKJfdAAfr11vKRJ7NPRSwFD7oC2 +ni9S9XKztcKwScP3YEkCAwEAAaM2MDQwMgYDVR0RBCswKYInbW9jay1hcHBsaWNh +dGlvbi50ZXN0LnN2Yy5jbHVzdGVyLmxvY2FsMA0GCSqGSIb3DQEBCwUAA4IBAQBu +dQqU671woPZW+gdWLexLOaRe/cS+cuvq/E7AV2Q5LzzradcTFf3mNi0THfORCl9s +3olkJaTcuHW4zP72f1/2B7xFv9Jmice40hdd9+GLucpZqH9hUG0RxzOEu8f9uupM +PyBML0tipwBq+pUOcbh6OHkOPv9FUMHW3GIzlSAEh+KmEmMK8Cxo95lS0fc4d7OI +VcFqKuIKez3+GmOX7leX5n1wWagNNalyzl/C9giSuWeFpboYmbFZSqn/MKl+otPz +rl440x+XbYZN+nK5Fg9+jbLi93aJj/LwkDoWUNGmofmKYdRhwBSg3hkOETHd77WU +Ad11TpbvdE3ONm9HOq0e +-----END CERTIFICATE----- diff --git a/tests/resources/charts/gateway-test/charts/mock-app/certs/positive/client.csr b/tests/resources/charts/gateway-test/charts/mock-app/certs/positive/client.csr new file mode 100644 index 00000000..5f49e4b9 --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/mock-app/certs/positive/client.csr @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIC4zCCAcsCAQAwWTELMAkGA1UEBhMCUEwxCjAIBgNVBAgMAUExDDAKBgNVBAoM +A1NBUDEwMC4GA1UEAwwnbW9jay1hcHBsaWNhdGlvbi50ZXN0LnN2Yy5jbHVzdGVy +LmxvY2FsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuVr9rWeHxqcb +Samo479S0ENH98YaAxEovTnkNT+gCUp6hz17ccFwA+nlwFun7DLNwPeqgDMUr4yn +TUnn23m4MVLpKDWeqg+/66NqPj3WrZbE3blbOJqGMErb1n0zKhH2AJtBPxCDFFP+ +EYAtRCgptsClUb75T/PR/qqFo6ix4Fo7tHwVe7HxDV1Mbi+0gm/+Yr64pcVqQMl1 +CHyH+QQxo7ulgGqE6mLyhsBAvd0lDFCvsmN7RRf2nN4MOJlzLxvvZMKz9WCWKJlP +H/Vzhw+uKzbWQ9xMyg5nUNFuauwQMol90AB+vXW8pEns09FLAUPugLaeL1L1crO1 +wrBJw/dgSQIDAQABoEUwQwYJKoZIhvcNAQkOMTYwNDAyBgNVHREEKzApgidtb2Nr +LWFwcGxpY2F0aW9uLnRlc3Quc3ZjLmNsdXN0ZXIubG9jYWwwDQYJKoZIhvcNAQEL +BQADggEBAA1Wz1ttX4V/q5CwwcOG646m1g5bj9N/ejaDQpFrdCJQU8thCPIpjDHw +ubDRW/9p7ReXkHWhnT3rk5S2QOa5NNStdZ9fcDHWNku0nwj+AFx17OmuITG7Q77y +RWHGW21ewoJe4wwsDbrmVIvk3ZeD6ziP2xP2C3U8IVZ+kAdc56HB8cczjXhyDyYD +M1Q8e8ZHilWot+l9hYD8QQOFvaretzQbrfCG824GWpt61tk+omeYbUBzIlY5Y5HA +jhc7hKOwWXCWtp0u774sudnpsh0Fkgwi+eEhbgtdivz/4hzMvCp0CSZbgpqkXGan +VNzKT6E+TAe28EwP9KqL/ZYda9s6NZs= +-----END CERTIFICATE REQUEST----- diff --git a/tests/resources/charts/gateway-test/charts/mock-app/certs/positive/client.key b/tests/resources/charts/gateway-test/charts/mock-app/certs/positive/client.key new file mode 100644 index 00000000..c097dba5 --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/mock-app/certs/positive/client.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAuVr9rWeHxqcbSamo479S0ENH98YaAxEovTnkNT+gCUp6hz17 +ccFwA+nlwFun7DLNwPeqgDMUr4ynTUnn23m4MVLpKDWeqg+/66NqPj3WrZbE3blb +OJqGMErb1n0zKhH2AJtBPxCDFFP+EYAtRCgptsClUb75T/PR/qqFo6ix4Fo7tHwV +e7HxDV1Mbi+0gm/+Yr64pcVqQMl1CHyH+QQxo7ulgGqE6mLyhsBAvd0lDFCvsmN7 +RRf2nN4MOJlzLxvvZMKz9WCWKJlPH/Vzhw+uKzbWQ9xMyg5nUNFuauwQMol90AB+ +vXW8pEns09FLAUPugLaeL1L1crO1wrBJw/dgSQIDAQABAoIBAQCy8ZtSW08Dg7Se +awK3zK+AjFPgawoVx+0Ssd8VYTV5gsPD6KFSczNXM+owyMvXBj0JfJDIb4ga6qlh +vmXuxxYB2E9sGEfzWn0oWn1pVX353EJ25Emi3duKp9qQuhI5HVnnv/s/jQtfBq+T +6bDJyhRrcJSp1LsQaw1i1PFrzKLdOdgNO0gA81tNAspmAM1+Kk8xVGAbmQkkRKUx +o4JTW0GounZoIJfg7fyfEi3dKyUakphnNu10WVKR2tqoAQhj/mTVl9FhIZtN73Gr +cCzdWj4I3+pmPBTBW3BWA5CoilZ27GqfFTStaeccrUOyXTfPgNEn4u03sm2jfpxB +zh8aFx5hAoGBAPYJGXX3Pel8Se8qKS447qqdgZl/0PEvQOIY9+SOr8NUGCuXgoGY +8DPMgopDDoeLyeOdNMIrhtO6SPKQBYyz2T5frEhj4UUI1HyfmT0mF6GdEBATPmiQ +AKJogIACQBDzDh2n52zfbUKxyZXfGpOOAcUMuIytn4sS/ex6Rs1IcI5tAoGBAMDc +wlNmdzur6t1fK5jxriMMVNWbA3ss4NSvNr3yTnRlhQlV22WkPbngPUGAAaDFweZB +B4FKSZzfLYp8a1cQx2Cr5D51QE73HkB9CO7s+CoxX5faT3cKI3Me7GPJjIORdiRI +oTMRSZOpiiNP9XavQqURefcVLzr+Zi7rpgFCmr/NAoGBAILoOInRsTloDhaYwix7 +0lEpWOmJXmzVjZo/WrZbTR2Kwwl+pcu6yiNlbxeNsk9gi1z2Kjod2rEQ7vtQsgM5 +Nh+/2/TwX83Rcu2UJX6po+0zmnZTJuOPqya+n5B8ogXirOIOkk4VWxcfbXi2qndU +GZD0wcToJHlk84I9VSqonmrJAoGAb9RR9awXfQk9oWkazY9tyrLOyiEdTqICKDEE +y/UhWsq27mfTVMd8ZzhILJ+90ex5dzrD0Es0Dfs22/MzBoQbJ8nkCfdQ97jA2OHn +eSr85vJEHLgglcTSM2F97oqiqHODDpzyo7rlb/LBv6IQkeYj/bT5hLTK8ykqNRC8 +7EQjmQ0CgYEAy4M6fhO8cp7x3Prevv0eM7L/uLPfsrCMxjRDNLZnUVO6gWIwTUGQ +uMzJGsISv8mg30w8QTSWN9nIjQhB1khOwrmTaASRMCOAFNDyR3U7meN0tzW7oXrY +D6kFQD/KXU9UEUyy9ZvMibEqZbFzkNG+LdiCC4UpgKPntkH6OYsRzi4= +-----END RSA PRIVATE KEY----- diff --git a/tests/resources/charts/gateway-test/charts/mock-app/certs/positive/server.crt b/tests/resources/charts/gateway-test/charts/mock-app/certs/positive/server.crt new file mode 100644 index 00000000..f06f315d --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/mock-app/certs/positive/server.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDazCCAlOgAwIBAgIJAOkxC9iq+eDLMA0GCSqGSIb3DQEBCwUAMFkxCzAJBgNV +BAYTAlBMMQowCAYDVQQIDAFBMQwwCgYDVQQKDANTQVAxMDAuBgNVBAMMJ21vY2st +YXBwbGljYXRpb24udGVzdC5zdmMuY2x1c3Rlci5sb2NhbDAeFw0yNDAzMTIxMjEx +MTlaFw0yNTAzMTIxMjExMTlaMFkxCzAJBgNVBAYTAlBMMQowCAYDVQQIDAFBMQww +CgYDVQQKDANTQVAxMDAuBgNVBAMMJ21vY2stYXBwbGljYXRpb24udGVzdC5zdmMu +Y2x1c3Rlci5sb2NhbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL5S +9RpdEnd0AwmWfRE7yJGNX7x2YGwLF8jl9m4nzP2NoO+sYSICyulxRkCp3jywzLk6 +FeLMCQqhaQgmW8woGpwMDah1Blx4SNkYQrEOL39U8R7ertNbhgJt8CADi0YN2c3k +9AoYq/gjWclRkmepzTlwb+x3x9m7o5FKof8u8bC8/LHsekWJHDU4l1wbdX40APCK +jAMYxjTMIGK3qLHnKWdJdn3/642G37laKjAhGyWZMRAVcycDKp++s6t1DANu1C2N +A+63UNpAQCQmDei+2hu4o1HW995h/Dl3lg39DeMiRrWTlBSRad9ReyDJ2HuB0Zeh +46zU/oKc9XzntJd/Cd0CAwEAAaM2MDQwMgYDVR0RBCswKYInbW9jay1hcHBsaWNh +dGlvbi50ZXN0LnN2Yy5jbHVzdGVyLmxvY2FsMA0GCSqGSIb3DQEBCwUAA4IBAQC5 +XmOua74PU8PD7Ifkt5uJLGsb0rBRlLuS3aALXARYAu7oDj+TZR8b7f7Th8sGNm4C +dkjWW4Ke4Q9ts2pCsmM5SZyu6Xd20dZRE8Vc+OQFRHfbW8fHsAGob1gyypQbhG/y +xZNcCx/FLUKiVMJ5X8s72COIqHGbGfqSvhBxkClPgS1h9aeyapwSeYbDz2bSeSI4 +SRZZdvYOQYltERLjluXxsQsHE1HmFJ5jUknulES4brDEuXSRvAzXEkiSnDkdypV7 +WYibqLEX88XEn9lgl0LYgJLoYMr9sziJFtCLRkI09DV6MJs1V6VO7r4Nu12QGD/U +TmdknuKuy9cffLnjfUiz +-----END CERTIFICATE----- diff --git a/tests/resources/charts/gateway-test/charts/mock-app/certs/positive/server.csr b/tests/resources/charts/gateway-test/charts/mock-app/certs/positive/server.csr new file mode 100644 index 00000000..469db94b --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/mock-app/certs/positive/server.csr @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIC4zCCAcsCAQAwWTELMAkGA1UEBhMCUEwxCjAIBgNVBAgMAUExDDAKBgNVBAoM +A1NBUDEwMC4GA1UEAwwnbW9jay1hcHBsaWNhdGlvbi50ZXN0LnN2Yy5jbHVzdGVy +LmxvY2FsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvlL1Gl0Sd3QD +CZZ9ETvIkY1fvHZgbAsXyOX2bifM/Y2g76xhIgLK6XFGQKnePLDMuToV4swJCqFp +CCZbzCganAwNqHUGXHhI2RhCsQ4vf1TxHt6u01uGAm3wIAOLRg3ZzeT0Chir+CNZ +yVGSZ6nNOXBv7HfH2bujkUqh/y7xsLz8sex6RYkcNTiXXBt1fjQA8IqMAxjGNMwg +YreosecpZ0l2ff/rjYbfuVoqMCEbJZkxEBVzJwMqn76zq3UMA27ULY0D7rdQ2kBA +JCYN6L7aG7ijUdb33mH8OXeWDf0N4yJGtZOUFJFp31F7IMnYe4HRl6HjrNT+gpz1 +fOe0l38J3QIDAQABoEUwQwYJKoZIhvcNAQkOMTYwNDAyBgNVHREEKzApgidtb2Nr +LWFwcGxpY2F0aW9uLnRlc3Quc3ZjLmNsdXN0ZXIubG9jYWwwDQYJKoZIhvcNAQEL +BQADggEBABKS9ForQ7abll9++o3CuQZ2+ZM1QYH2TrnHbjVsG0tl8PpSmPgkIgmr +lvqk9UnUcr8kV1iq+RqKkGctPgm3DUAX86h/TjbKfIO09IdK/7zMYZ4pgI+tYD4V +UHOTjlxCUn4HWkyHDGOjOfZNcRNBFJPIFrvlPjTftBJ9LMswyiXZLvrMvXU31Qj3 +vmtS8mBZ8zv6n/by5dNCUpneF88/2RkTj3ICdcJjgN3hqFqD5UlT0lN31BVKOq3f +Cnkff2S1byLNH7Jkv1w5taoTQzpeUStHnf7Er30It8r/hpDoAoGd7FSm3j4WJFEY +K/B5XB+3DhahLJrs3qhwRk3iZ/c77ZM= +-----END CERTIFICATE REQUEST----- diff --git a/tests/resources/charts/gateway-test/charts/mock-app/certs/positive/server.key b/tests/resources/charts/gateway-test/charts/mock-app/certs/positive/server.key new file mode 100644 index 00000000..b402f780 --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/mock-app/certs/positive/server.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAvlL1Gl0Sd3QDCZZ9ETvIkY1fvHZgbAsXyOX2bifM/Y2g76xh +IgLK6XFGQKnePLDMuToV4swJCqFpCCZbzCganAwNqHUGXHhI2RhCsQ4vf1TxHt6u +01uGAm3wIAOLRg3ZzeT0Chir+CNZyVGSZ6nNOXBv7HfH2bujkUqh/y7xsLz8sex6 +RYkcNTiXXBt1fjQA8IqMAxjGNMwgYreosecpZ0l2ff/rjYbfuVoqMCEbJZkxEBVz +JwMqn76zq3UMA27ULY0D7rdQ2kBAJCYN6L7aG7ijUdb33mH8OXeWDf0N4yJGtZOU +FJFp31F7IMnYe4HRl6HjrNT+gpz1fOe0l38J3QIDAQABAoIBACtgyv5kQiY5qcuQ +ohbAcnlCKJTSwi095gDi8OSwa5dKpWia+FSBIHBOYf2w+bcJcM+yvnQ/nrvuh/rU +i02fwljYonBHo9iFjcz1K5YhLpAt8vrfNCd2D7gUCIuzYxXnaEH2MezvLJrUq80n +q1+3ItA5oTjbIBCvJJuj0AJSV8G5G/UXlMl1VVPRPfAeM+ib7J9nCFIFFW+OlKoi +n+VJJkXGKDyIs5caYEBu0Jlmvdpqq5UtxmCMTVuVKjGV4JkM9pSFv2KXB9aEadQt +X3msclWDmonWzEdDhgsSVWrRqCADduZZDJhQGdAzW2jVGGQVVLWqEW5wwhXb0jnD +bdFEu70CgYEA92FKsvJ/0LeMav3bAowaQsA4v6pYvnQCh5tlh5EutA62zoytFHtp +0QiIkmRdCvV32pyQj/vzUwLgQywhxoC1SChDfzJDkKPSpxSvt7DtmcHY792Dtlpi +eOLI9FrHyYlq1vaVMaXw8DzuMXAqaAgkdNgh1JoYpqcl22iMAXRaofcCgYEAxPS1 +SNLEwteFTpriySxouyHpbbkqTJrtjMTu/7njS6G/Xt5nUZTHdf0VpmT2YMELsQx5 +tBUyzoOdXinNXTbx+UVueJQ83ZQwebzrR5130l9/EogzRe+zZSFOUjm6YkNXK5Jd +ktNSVn6zxxKy2oBlFAdiL0A368U4i1shLa8NfcsCgYEA0y5YQZlo6bm3gqLBs1P9 +GxzTlTOL3NJWUoOjUe7rqsSg5IUNQF32wH8Db82D7FYPAi4D7xbL6wKahl2HW9kG +aNoOfOhg63oe24l6VFsTCt6EHojA5wwT4lTf7lINGgxYi7gnNyINJFkvkj7JxNOm +o6TahI8kGii41axTUO6ObJMCgYEAkZiQdtAQUjS+QBhxc+PXXBa6l7kdEtoopzph +rzt8Ukm0zW29lOpV9NvtaD8UfvvWJ8CgK0bMcyuKZrSiMrlOcUYpXwu+XtKQbz3/ +88XtcN/VcR6sQJPs2uKfIlu4c7FyPCyL7eE36ebqAUzKWIo3rnGy3FktvaXioenx +AfN5FrcCgYEA9qt7FEkmrvyxjjpfNc4niNQUz1Hho0Y13hpIxsJoHIia3XIr7P9r +GVjsbtbraGKHjy2vT+Fw/8u3FJGQBnIYgFL3owPP3kWqOowQb5e18kDcW8/KFyE+ +2BuRv0safg1R/reEGJEuRAm2Pw3VU3xqS09Q2em93YdWNjmAdloBz7E= +-----END RSA PRIVATE KEY----- diff --git a/tests/resources/charts/gateway-test/charts/mock-app/templates/_helpers.tpl b/tests/resources/charts/gateway-test/charts/mock-app/templates/_helpers.tpl new file mode 100644 index 00000000..9cba1391 --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/mock-app/templates/_helpers.tpl @@ -0,0 +1,12 @@ + +{{/* +Create a URL for container images +*/}} +{{- define "imageurl" -}} +{{- $registry := default $.reg.path $.img.containerRegistryPath -}} +{{- if hasKey $.img "directory" -}} +{{- printf "%s/%s/%s:%s" $registry $.img.directory $.img.name $.img.version -}} +{{- else -}} +{{- printf "%s/%s:%s" $registry $.img.name $.img.version -}} +{{- end -}} +{{- end -}} diff --git a/tests/resources/charts/gateway-test/charts/mock-app/templates/credentials/expired-mtls-cert-secret.yaml b/tests/resources/charts/gateway-test/charts/mock-app/templates/credentials/expired-mtls-cert-secret.yaml new file mode 100644 index 00000000..0a968871 --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/mock-app/templates/credentials/expired-mtls-cert-secret.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Secret +metadata: + name: expired-mtls-cert-secret + namespace: test +type: Opaque +data: + # Server certificate expired on 02.08.2022 + server.crt: bm90QmVmb3JlPUF1ZyAgMSAwMDowMDowMCAyMDIyIEdNVApub3RBZnRlcj1BdWcgIDIgMDA6MDA6MDAgMjAyMiBHTVQKLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURkakNDQWw2Z0F3SUJBZ0lVWnVOZ3FCQTdSUEs4bStSSnhuaXRTS21xZFdZd0RRWUpLb1pJaHZjTkFRRUwKQlFBd1dURUxNQWtHQTFVRUJoTUNVRXd4Q2pBSUJnTlZCQWdNQVVFeEREQUtCZ05WQkFvTUExTkJVREV3TUM0RwpBMVVFQXd3bmJXOWpheTFoY0hCc2FXTmhkR2x2Ymk1MFpYTjBMbk4yWXk1amJIVnpkR1Z5TG14dlkyRnNNQjRYCkRUSXlNRGd3TVRBd01EQXdNRm9YRFRJeU1EZ3dNakF3TURBd01Gb3dXVEVMTUFrR0ExVUVCaE1DVUV3eENqQUkKQmdOVkJBZ01BVUV4RERBS0JnTlZCQW9NQTFOQlVERXdNQzRHQTFVRUF3d25iVzlqYXkxaGNIQnNhV05oZEdsdgpiaTUwWlhOMExuTjJZeTVqYkhWemRHVnlMbXh2WTJGc01JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBCk1JSUJDZ0tDQVFFQTQ4cTdiU21rSVZwQng5SWo3Si9qWGFJNU85dGs5dEJlTlhxcGNQM2lsTUtFMVpqTmhyb0wKRjNNV2xnWU10cCtBRlEvKzhYdjlESFVCMWtaNnU4QjZiQzFjV1NSZWMrUWY1ZThYNXZlZkFodng2ZHhCem0wbgo5MDhodTVhRFFQUXdCOHdsYSsyRHAzTjZVdTJQcW01cXk1U0xPcFZxN3hnaHBiUFBQSndoZGNIZFoxN0hvckl2CmcvdUt2M3JLdWJtY2U1MDdDaGFVWGhsWTUyeUtHelRvdXp5R3RqRjhOelRWSmQ1QmtFWGhmVW5HdWttbHBOSG8KRHNUWEJYcE9jNkdxb1VmL1FueUdQTTNtVEVkVUJEQzVEOGhtaUhSU3UyM3VHa2VLdGpWV2o1Ukdydkx2aUdRcgpoeHRhcStzNzBPTUtVbURaRDBMU3MyNXNwVzFnUWtkUzRRSURBUUFCb3pZd05EQXlCZ05WSFJFRUt6QXBnaWR0CmIyTnJMV0Z3Y0d4cFkyRjBhVzl1TG5SbGMzUXVjM1pqTG1Oc2RYTjBaWEl1Ykc5allXd3dEUVlKS29aSWh2Y04KQVFFTEJRQURnZ0VCQUMrNnM3TmJlYSs2RnRibSt1V2FUQjZSSTkzMEpIRUhEd3Y5YWQvR2VXZllZd2tBVXpQMApja1VKSzZDWHVCa2FxYzVOSkpuRmVGZGpIOXZ4MHVBSkxvNXdiZUpGTjB6SXRVdTNBMGtEMnd2U3VXTGJCbEhBCnFiTWxQMm9FMWxsU3hZZDhRMFpPaDllUld1MkMzMitjdnorRWw2RXFzbE9OVmlrZWEyNWJvYnFnelJhY2FCM0UKMG5KQUgwOUFyMys3MllzSHFNd2EyUm51bWVORDJWdGNGT3JraWt3ZkxJbml5NFJpQkdSY3RhVTF6bk9oZVpMNQoxZFJDOURhMk5nZzNrb2tDb3ZibmtHcXZsWk80ZnJJeEw1U244WjFXcEl4dXNoTzBaWjdkeWdaT0FtSEluQzd0CmJYSHJsb3VDclh4TGlHKy9qRnVvMXRZYlFHN2VUWE8rWDNBPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== + server.key: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcGdJQkFBS0NBUUVBNDhxN2JTbWtJVnBCeDlJajdKL2pYYUk1Tzl0azl0QmVOWHFwY1AzaWxNS0UxWmpOCmhyb0xGM01XbGdZTXRwK0FGUS8rOFh2OURIVUIxa1o2dThCNmJDMWNXU1JlYytRZjVlOFg1dmVmQWh2eDZkeEIKem0wbjkwOGh1NWFEUVBRd0I4d2xhKzJEcDNONlV1MlBxbTVxeTVTTE9wVnE3eGdocGJQUFBKd2hkY0hkWjE3SApvckl2Zy91S3Yzckt1Ym1jZTUwN0NoYVVYaGxZNTJ5S0d6VG91enlHdGpGOE56VFZKZDVCa0VYaGZVbkd1a21sCnBOSG9Ec1RYQlhwT2M2R3FvVWYvUW55R1BNM21URWRVQkRDNUQ4aG1pSFJTdTIzdUdrZUt0alZXajVSR3J2THYKaUdRcmh4dGFxK3M3ME9NS1VtRFpEMExTczI1c3BXMWdRa2RTNFFJREFRQUJBb0lCQVFEZlB1QWpZeTBsTnRURApKakxwQStZTDdTSVVoTGRWb083RGtOeWhEV0ZUazdRbHRpU3ZSb1A2VG1PelVtaUJUcDV6aGdMQTNsZ3BMajlICnBqbEE2cW5RZlVCRmFQeGNyaFdJL3FNNVRETjlHTEFsRnlVelR3MWROaU9FT2tXV2tmckVtWkdQVGU2NlhOVmsKa3NnN0t3M2xTVWFPZXNPYllkWVFGTUlrejR1SFlFZlEwbWprTUVzdWJLWDRVVmJpbzg3Q0ljbHVYTFRodnBLNwpMWGZVdjdIdXJFY0hxdkVPbDhZYXdJckdLL1VMUWJLV3ZPN0xxS2hzTzJiSmhpbWoyNjVBZGwrNjdpbXJabHRHCnExVWM3V2lVOXlkeVNycTgrbVd0elpwbVpWR1N4RFkzcXlDeEJIbmJKNDNJMFZ3SktLcURMUzVMQTkvc0ZyZUwKVUpFdWlxYmxBb0dCQVBhYU4rdmJuWGlrTEFnckhONDRjQ252QkxhUkV4YmFyVVdMT1ZHVStnWUVtenRveUdBdgpkSWdXeE1maExHL2J5Z29SWmpxenc1TTBHMGVBTTJEWUpZV1hJM1NYR3RXM3B1Vzl4b0pMS09WN2c3anorNjZJCkpSaTFqeUY0QVdOZXpZUWdUZG5ncndULys5bHhOSThMbUMxN2wrSWprNXR2Um5WVCtUQTRRSGw3QW9HQkFPeDUKQVFUZ1QxWEtiNDFGLzRFL3R2cGJBdEdneDE5VzYyb0IwcG83M2JKQjN2bUIzTzZpYzBna2VQS1dxUEFFakxINAo0MVQ0U2MxbU5qT0p2TC82Z2ZXZDRqUEllMTRFWDM5UW8xeEtPSkh0dDU4Qm5yaVZwZVBWUFprRklOYWwrRTJECk1rbzI1dzkzd3FsSDBuVEF4eHlFQ3RTNjdpS1BldVFPRXhLNlJOQlRBb0dCQU14em1jOTdHZmlPckU3dFo1YTUKMWd4K05Uc2oxbDdKV0lUaTQ5ZkdtdS9vVzhjS25hNVpTZFVXZzNsd0w4Wmh4QVZLM2FYbnFrdGVGUXZYdDBFZwpreU5KNWtSZ2p3Z0hwbUN0VVdwdTQrNDIxRVBBVExjcit3MmNZWm1QQkIrZDF1Z25YRVE2YXdETE5zUFZmb3ptClFQbmNrVlVVeCtsRGZYZ0M4Z05QYiswSEFvR0JBTnRmamlCMTcyT0pQMTl4OW94ekRVN0lLNTlKWm12OStMc0oKSWRWUGdHV2tValJwMHduV3p0ZTRiak91ck42dGVkQ0pNbXhiUWl3NGpFUFhuYkVEdHBpamRYdlFteEluUUdpZAo2RTd2MC9jYzd1R2w0UmNnVFJ0RmNiV0pXbU9HNlFrUGt4SGlTUXpDYjJZWGFSaEMxdlNQVW5UelRZUG1VMzFKCnlVdndYWEpkQW9HQkFMMC82Vlc1SGRsSkZCeHlTVGVBTVdVZ0lsVzNpSFhkeVFNRGdic3RPZHl2MittdkRJdEsKS2wzd0FPNitDZ25XNDAxek9yOFRkS2xsNC9paHdoakJaV3k4TWtXUnYrQUEyY21ZMjRRR2liY2cySjIrMkFneApRYllsbEkxWjR3dGVNVjVLTXdFS1FVaXdyTzhBVFZHZCt0QUx6MmRuZXEwNDZuRXFaa2NkZU93aQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo= + # CA root certificate valid till 17.12.2049 + ca.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURrekNDQW51Z0F3SUJBZ0lVR1lXL0t2Mmo0ZTY4UkZ3S2hJTjEzYVkyRStNd0RRWUpLb1pJaHZjTkFRRUwKQlFBd1dURUxNQWtHQTFVRUJoTUNVRXd4Q2pBSUJnTlZCQWdNQVVFeEREQUtCZ05WQkFvTUExTkJVREV3TUM0RwpBMVVFQXd3bmJXOWpheTFoY0hCc2FXTmhkR2x2Ymk1MFpYTjBMbk4yWXk1amJIVnpkR1Z5TG14dlkyRnNNQjRYCkRUSXlNRGd3TVRBd01EQXdNRm9YRFRRNU1USXhOekF3TURBd01Gb3dXVEVMTUFrR0ExVUVCaE1DVUV3eENqQUkKQmdOVkJBZ01BVUV4RERBS0JnTlZCQW9NQTFOQlVERXdNQzRHQTFVRUF3d25iVzlqYXkxaGNIQnNhV05oZEdsdgpiaTUwWlhOMExuTjJZeTVqYkhWemRHVnlMbXh2WTJGc01JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBCk1JSUJDZ0tDQVFFQXY5Vk4vdldzalJTZ2dsd2NBRXR2TmN5NVNiRXZkMk9aSXFMa2lVMmZQWjgvUG5QZHorNnQKZ3d6UEtzczh5bkowUEVYeEt2TDMzSWtrbFNueDFCL0lSaHNlU3VFWUxjVUZXdys2MFFpb2Q5ZlJESEFaeFhCVQpaTGlUaUJFMkFEY2lvWnQ3cjBlZGV6cHM5dEUyU3RwZ0N3dmpGeE5JZGxBUkJTN0o3eTRiOTZ3Y3lzdHFOV1luCnI2eFRCYUovTE8xM29jN1V3ZUdRa0xaSy95OG5hOWR5cTlGVDdFbFZVbFEyT0ptYXErdENiK2NLK2hOcWhMaEoKcmpXemJjVHdTRmtNQWt3eDVHUGp2RHRJak5seEpFY1BsWlA2M0tSZEhyWnZWeXEyQllxUkhCb0VHT3F0ZmpYQgpTZkV4bFNLMXVZQ1NkU2oyMFQ4YUJ5Q1dqTEhiL3hrZFRRSURBUUFCbzFNd1VUQWRCZ05WSFE0RUZnUVVqTjRiCnVaRWg3SkFBaG9pdDE4ZXJXM2RlVFBnd0h3WURWUjBqQkJnd0ZvQVVqTjRidVpFaDdKQUFob2l0MThlclczZGUKVFBnd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBTkJna3Foa2lHOXcwQkFRc0ZBQU9DQVFFQWtSRGhlbHYvT3IvbwpQRzB1S3hReVVvMXJvRlhFNmRnN2tSd1FsYjhNbnlVQmZZWkZrWHFSU04yRGZKaFZ1OTVoWnR4RXVobTBKbjRSCmNKUzV5NUZHNmF5WmZJN3krN0Yvdk9pb0RZYUNWWXluWkRZRlluOENsZld1aWplbW1JSStUTEZIUnFiOEJQUDIKSVRtUGl1VkhEeWMrbUdmMU95WmVONnc5bTZRL0FmdEJ1d3R6Qmt3MGlQUFlHVHZFSFdiNkRWWGIyUjBFYnBJUApPYWsveUtUZ2VOR1htemY2ZUhqZnkxaVVla0VXV1N3YWhkSlo1WFYzbGRUY3Q4bmRwM0NRdHZ1Z3YzQzl6T3A5CkxnMUlGR1JhVElIU2NvOWhheWQ2eEtlL0kvTXY1OStHeTlITDhqeGJxMWpMbzFneEN0Mi9KUWZZRmxpb3RkVGMKQ0VJVjVNNGt2dz09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K diff --git a/tests/resources/charts/gateway-test/charts/mock-app/templates/credentials/mtls-cert-secret.yml b/tests/resources/charts/gateway-test/charts/mock-app/templates/credentials/mtls-cert-secret.yml new file mode 100644 index 00000000..aa8e6027 --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/mock-app/templates/credentials/mtls-cert-secret.yml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Secret +metadata: + name: mtls-cert-secret + namespace: test +type: Opaque +data: + {{- $files := .Files }} + {{- range tuple "ca.crt" "server.crt" "server.key" }} + {{- $path := printf "certs/positive/%s" . }} + {{ . }}: >- + {{ $files.Get $path | b64enc }} + {{- end }} diff --git a/tests/resources/charts/gateway-test/charts/mock-app/templates/mock-app.yml b/tests/resources/charts/gateway-test/charts/mock-app/templates/mock-app.yml new file mode 100644 index 00000000..a57dfb87 --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/mock-app/templates/mock-app.yml @@ -0,0 +1,65 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: {{ .Values.global.mockServiceName}} + name: {{ .Values.global.mockServiceName}} + namespace: {{.Values.global.namespace}} +spec: + replicas: 1 + selector: + matchLabels: + app: {{ .Values.global.mockServiceName}} + template: + metadata: + annotations: + traffic.sidecar.istio.io/includeInboundPorts: "*" + traffic.sidecar.istio.io/excludeInboundPorts: "8090,8091" + labels: + app: {{ .Values.global.mockServiceName}} + spec: + containers: + - image: {{ include "imageurl" (dict "reg" .Values.global.containerRegistry "img" .Values.global.images.mockApplication) }} + name: {{ .Values.global.mockServiceName}} + ports: + - containerPort: 8080 + - containerPort: 8090 + - containerPort: 8091 + imagePullPolicy: Always + volumeMounts: + - name: certs-secret-volume + mountPath: /etc/secret-volume + - name: expired-certs-secret-volume + mountPath: /etc/expired-server-cert-volume + livenessProbe: + httpGet: + path: /v1/health + port: 8080 + initialDelaySeconds: 3 + periodSeconds: 3 + volumes: + - name: certs-secret-volume + secret: + secretName: mtls-cert-secret + - name: expired-certs-secret-volume + secret: + secretName: expired-mtls-cert-secret +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ .Values.global.mockServiceName}} + namespace: {{ .Values.global.namespace }} +spec: + selector: + app: {{ .Values.global.mockServiceName}} + ports: + - name: "http" + protocol: TCP + port: 8080 + - name: "https" + protocol: TCP + port: 8090 + - name: "httpsexp" + protocol: TCP + port: 8091 \ No newline at end of file diff --git a/tests/resources/charts/gateway-test/charts/test/Chart.yaml b/tests/resources/charts/gateway-test/charts/test/Chart.yaml new file mode 100644 index 00000000..ecec00c5 --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: test +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "0.1.0" diff --git a/tests/resources/charts/gateway-test/charts/test/certs/invalid-ca/ca.crt b/tests/resources/charts/gateway-test/charts/test/certs/invalid-ca/ca.crt new file mode 100644 index 00000000..324a4960 --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/certs/invalid-ca/ca.crt @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC+jCCAeICCQCIIuahgVg3RTANBgkqhkiG9w0BAQsFADA/MQswCQYDVQQGEwJQ +TDEKMAgGA1UECAwBQTEMMAoGA1UECgwDU0FQMRYwFAYDVQQDDA10ZXN0LW90aGVy +LWNhMB4XDTI0MDMxMjEyMTExOVoXDTI1MDMxMjEyMTExOVowPzELMAkGA1UEBhMC +UEwxCjAIBgNVBAgMAUExDDAKBgNVBAoMA1NBUDEWMBQGA1UEAwwNdGVzdC1vdGhl +ci1jYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALX1TzEMihcmLDFv +1Hr8CPi9PQ6IOiKZzHe+cPhb5o/qr52jbqoqcPcLPhWMt/COsdu9YJTS4N4v6AnF +573CwjaIuXiHuR8Nwej+L0yHqxQJSS4OOWa4fzQulUI341ObKQe5LWGYXJ9tDHHQ +DUbZfS4mCSHSCBavKJxorUfaihO9piKHlbcADZUl8ruAsuQwWGY3/PufcUj28k7o +BkImnS+kDNiTrWENSJlktOtwJeLWWSm3STkWZumvoK0//vig+/EMofSrdnwNGB8m +eN44GC18t/6JkBt+Ccy0EB51Jcmxz7kWPs5efAXzTYk93vt3KzsH3oqmleSVBX83 +58BvsRcCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAWX17M2c3pzF6eroZtxljYiou +hh0uEMFMzQubj57iyeYJzOkjWWrWkHImvOJhF/2eLIqjJt7DIlUjSGxEOXg+zYZP +zp1et/dGhmSsIuJ3m4IxLzkjajeRBnqhPe4lE/bgilqDgpHTElCLuIY3eP4fkwYp +G0QDO8mEz2tghfsZFM1iK1vs1DXTCCr6XHivA/fQZyN45Be/0BDqYnTtTIZr9/uL +aa6EXHCVrE3sSfQlwA0qTvqpgVQR7RO/QmzZeVavRQbeDC+Gt9wl//nWlwqLKx+I +3Y9X3+v/iO7OEglxp1QZAHHO63gtWm54EXtOEeWUnUUWA/cdA9gNQQocZNawMA== +-----END CERTIFICATE----- diff --git a/tests/resources/charts/gateway-test/charts/test/certs/invalid-ca/ca.key b/tests/resources/charts/gateway-test/charts/test/certs/invalid-ca/ca.key new file mode 100644 index 00000000..4b728467 --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/certs/invalid-ca/ca.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC19U8xDIoXJiwx +b9R6/Aj4vT0OiDoimcx3vnD4W+aP6q+do26qKnD3Cz4VjLfwjrHbvWCU0uDeL+gJ +xee9wsI2iLl4h7kfDcHo/i9Mh6sUCUkuDjlmuH80LpVCN+NTmykHuS1hmFyfbQxx +0A1G2X0uJgkh0ggWryicaK1H2ooTvaYih5W3AA2VJfK7gLLkMFhmN/z7n3FI9vJO +6AZCJp0vpAzYk61hDUiZZLTrcCXi1lkpt0k5Fmbpr6CtP/74oPvxDKH0q3Z8DRgf +JnjeOBgtfLf+iZAbfgnMtBAedSXJsc+5Fj7OXnwF802JPd77dys7B96KppXklQV/ +N+fAb7EXAgMBAAECggEAKbL1Gg2Am/uAhzfUnvaha7eahXkMsZ9Db3GyXAhbl0G2 +S08H7nFZgBQQf0nHYZaiBfSpbJHDPMgHyi2ThTZb4bmFn6yi7Q3vEWEnH8e7mhTi +s25JE1RWunOuewVp0GAvj/iNAN+04khQYMjIMiNnf6rxztFeTyyHBwkqJNxdZlZe +BrT41N3LqFsvjhz5kWuEzBYWs5SFV7Co62+fD2gJW4atWt4w9QywELVsYM8k72nS +cuZ/cxgiCpDLej6M9MgZZvYPNs9x0sWjDUOH/1zW1vr98cVbfwf4DcZdKNHux6Dt +iwl5awok5EAxlJT3KY2e5Xy9yulBioImrEj7t1h2GQKBgQDdefY3Gm8b1EsSLnGu +tYYFiDCC2TnUMRCdXs9+ZSmIgylv/mi1kGu5TIh1YPNEG0ZBqkmPS6p9d0sgDwDH +cagAIpfDK7c1H4w61lQYUo2O0RvExULDsAAFQxcz7GTADLeDaiBuNy2VpzNtIEw2 +xaD9dYwCqReiY2cpzW0QZ5EUtQKBgQDSUl71Rpjg4EntZdztIW/WnYy0fDzcNSqE +zH8ZepXx7SFd4N/Ru27dofVE1oi3MQIBojfxbuLQ6DYAsbZBnLz7WGCeIp32W/vz +LAotojyTSL7jBJXH5XV6CJvFNzXIufc6Hzd29XUvAm6TEC5TKd/rwQYxJX/OtVNE +30qgzBu6GwKBgDXf4xrIXVrBq3lCvvimw3E5DcPmn4CUZtxBIew3I4FHlp7dng78 +kJfEnDUhXkuk7tQuXjJzT4exqx6jR6c8aIeP4qbhTXGouO3fERnRiwnAqCaXbYQ4 +neipx00kJeXpsgJPoI/u8DHFOGdFQgTY0i6Vl3dWNp+T2pZ6mBszdkE5AoGBAK95 +6B4uS6j7mNKH9V6nUh82jcmcCk8T0KjB0Z1ZaLdTSE6CK1taTXJ/CRro/2IQcoMY +bCJ0iKsRwtSrcMunUQlHwDzP1wlPz5MggFF4lZ+wxwqzrZ/9MxmhCw3tNWOGvN1y +ZB1NR/rzxXvPuUbLnjadcmQYzFyTbqj8v9AO22dXAoGBAKCNHJjXj1Gurs5lQ1tG +F/YwoJAOuXBy3mmDGYOOJ688Q4d56M9D+SWK6USVWvggx95ZmQ4SToS3ZUDnNPyV +l2TNvzx1Dq2a/vbT0dy1UNkk7epTWH4K0laY//P8P+ytB++XcWGu3orJdx1z8UMT +u12YURbB7Y87lg6kiYWHl6eN +-----END PRIVATE KEY----- diff --git a/tests/resources/charts/gateway-test/charts/test/certs/invalid-ca/client.crt b/tests/resources/charts/gateway-test/charts/test/certs/invalid-ca/client.crt new file mode 100644 index 00000000..fc38caed --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/certs/invalid-ca/client.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDHTCCAgWgAwIBAgIJAOkxC9iq+eDQMA0GCSqGSIb3DQEBCwUAMD8xCzAJBgNV +BAYTAlBMMQowCAYDVQQIDAFBMQwwCgYDVQQKDANTQVAxFjAUBgNVBAMMDXRlc3Qt +b3RoZXItY2EwHhcNMjQwMzEyMTIxMTIwWhcNMjUwMzEyMTIxMTIwWjA/MQswCQYD +VQQGEwJQTDEKMAgGA1UECAwBQTEMMAoGA1UECgwDU0FQMRYwFAYDVQQDDA10ZXN0 +LW90aGVyLWNhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwQui3NW+ +KkSknJxpgE8xxjihwZ10xfQKTie8zj8hrwEhkxqc1xHid68XSQ51oE/k+AdtbUoE +pDLWySSh4fLIb46Ox+t1RFMlsGitp0WRRSYs0Ri8Hldqpqb22r7xZvIscCEGtTjR +bwVuWcli8AUNdFY6Ak/sZjmv8wnj1OQWfm+v4YxOtgvsTq9s13/xY6Whg7DTWh+w +kWFYOKpJV1x1aT7QnN7q6/DXYYYHE3nW3sJMCGq71JGbsdK8dOyAM42sYz+8n5fh +4Dv0G46c+mFIPqnrMQrp1PVAL5nm3piUMkKH9kfOIWvGWQO9FZNOL4tG4qN661It +Zknw3zcha77f4wIDAQABoxwwGjAYBgNVHREEETAPgg10ZXN0LW90aGVyLWNhMA0G +CSqGSIb3DQEBCwUAA4IBAQAfm2inwlQB/X/VIun4bcKcVLlCZtvQOjXhsS+Z//Ju +1N8j1kg46asnF3JG+sH0Cndfr95br9+HNsn64B9B/n1MKkc9iMQUaEC52gcGMMxM +Tf3Gz7Kut5p3voOThnILPUXeY9lCisTZ0UEgHdTIgy/RnJ34sRB+hABNTjzGNBNt +tKI4qbTERIAadNIfY2z9HvuechG981UbZEnMZmt8/B8loNp/rOPfVBLAh0toK7om +7VMJ72p8L0QSoYKJuVZ3yVanHB5MvbGUvX0TSb5tSGbKrJDZoiwpIbrfn9judhrW +kuWwF5NB2wkMdmY//HkaiFrtQUdeorGrq0duX6Fu/aFe +-----END CERTIFICATE----- diff --git a/tests/resources/charts/gateway-test/charts/test/certs/invalid-ca/client.csr b/tests/resources/charts/gateway-test/charts/test/certs/invalid-ca/client.csr new file mode 100644 index 00000000..44e9c58d --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/certs/invalid-ca/client.csr @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICrzCCAZcCAQAwPzELMAkGA1UEBhMCUEwxCjAIBgNVBAgMAUExDDAKBgNVBAoM +A1NBUDEWMBQGA1UEAwwNdGVzdC1vdGhlci1jYTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAMELotzVvipEpJycaYBPMcY4ocGddMX0Ck4nvM4/Ia8BIZMa +nNcR4nevF0kOdaBP5PgHbW1KBKQy1skkoeHyyG+OjsfrdURTJbBoradFkUUmLNEY +vB5Xaqam9tq+8WbyLHAhBrU40W8FblnJYvAFDXRWOgJP7GY5r/MJ49TkFn5vr+GM +TrYL7E6vbNd/8WOloYOw01ofsJFhWDiqSVdcdWk+0Jze6uvw12GGBxN51t7CTAhq +u9SRm7HSvHTsgDONrGM/vJ+X4eA79BuOnPphSD6p6zEK6dT1QC+Z5t6YlDJCh/ZH +ziFrxlkDvRWTTi+LRuKjeutSLWZJ8N83IWu+3+MCAwEAAaArMCkGCSqGSIb3DQEJ +DjEcMBowGAYDVR0RBBEwD4INdGVzdC1vdGhlci1jYTANBgkqhkiG9w0BAQsFAAOC +AQEAUsdzm7CNn8kKo3ZexfR4utj0mVljCHgLWLWVDgOYDkJ/eQ6PT2bZ/VjSBABI +XMPcgeigN+6I5CcsTEQ8RIBCw9q0YsOS0oI7GtLUA6bQbv6OUGdyP/4tVenJryHt +c4DVNRybl+YzkaIEmIvuikiNUfzYrRRmDRlQJOGmk4rFlTlgyKpRQoRwWMtwXSyD +KvM/F5sTV8YgOzcuXDr7bLbf8MRwrnBb+UvKlRJ91x0Z1EzruS40bIu8yVfVG84p +3oJoBdD1ROgoLEcCnSsq3IlN6uTlNm4CUAmbxudNa0jrNI7PcmiMcfbl+krV8ZBH +SWkSacW6AMKvsGDJjL4MdU4YQA== +-----END CERTIFICATE REQUEST----- diff --git a/tests/resources/charts/gateway-test/charts/test/certs/invalid-ca/client.key b/tests/resources/charts/gateway-test/charts/test/certs/invalid-ca/client.key new file mode 100644 index 00000000..947000ef --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/certs/invalid-ca/client.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAwQui3NW+KkSknJxpgE8xxjihwZ10xfQKTie8zj8hrwEhkxqc +1xHid68XSQ51oE/k+AdtbUoEpDLWySSh4fLIb46Ox+t1RFMlsGitp0WRRSYs0Ri8 +Hldqpqb22r7xZvIscCEGtTjRbwVuWcli8AUNdFY6Ak/sZjmv8wnj1OQWfm+v4YxO +tgvsTq9s13/xY6Whg7DTWh+wkWFYOKpJV1x1aT7QnN7q6/DXYYYHE3nW3sJMCGq7 +1JGbsdK8dOyAM42sYz+8n5fh4Dv0G46c+mFIPqnrMQrp1PVAL5nm3piUMkKH9kfO +IWvGWQO9FZNOL4tG4qN661ItZknw3zcha77f4wIDAQABAoIBAFQEZ3ZrhF9LDsWm +gXg5f3VA8o2cpNT+uHl5a//rlBJhkKZAX+BuxTzHtH+0TldeTk3wlZyKKWj5Q2e5 +jMcU7k03I0c5YAlDktSrSmDRsz8ANWMvu7gM3br4Udm0XsYqQlLu3MeEmgoSuAtV +zbyexlNKr+aPuFhpZP2G4WSnfG68FXipGY/L2YQ39RhgxYESajlX6laxpLUYDUg9 +D1P3PuIk98pmco7dewW+YNx3Ngkzk1ivNDC99P0zpy/JfkMxe6imoSGObD5lzgfD +WKnCGxcIPC9GkDo8zgvN9z48QH0Mm0V6JaIlCSWhR5wbn9eYHU+w1y4jYemg3RYL +Ej6rGBECgYEA6WtS0nW390OVlafRvMiqZdvDXh6yfulC25u8VovL+DpC4PJ60cjt +bfEDSQBH0QbbYCpCsbZasH1upB3kab6eyHbWhAhH/eVb96xDEVN/FC83nfm08cCU +YLMwIGca4veB6Kf/sgsnmJ6rnubIS0v6go8iBHWk0tbkHlTWq+j+fPkCgYEA07hy +kNLKb6mADnPdywpUHvJJNJZRlSY5Ff8tlilpvo40GD09xj90lx8Om1VVReC4HFcw +8a3TWaSqRxsZFhm/ThlbNnQLIhum6KqOF8fsZotvpujr8LsOinMIsu0I760kAsuX +ADdg/IdeweVrJcFze0vwMOt/q2HAdzWoiU/5xrsCgYEAjjkykcHggezQLAvBJAIw +sTeiZqrVn7aJYj4WF7W+ZlU5gs68Py7qXF7J3aUqHRbMfF/Dm3y87WTAEYeVMUlQ +flzKgFB7bRxfWR3BD8GMYMQUY1FPCy6IOhN0c4nfPAQLR7N1fQqG6dtkPsHnsNlu +njaQR59W+pCtFj4jP0QMLCECgYBpZYva3qSaG8Y8659BAX5I/ZJF1IL+fc2zTpny +A+G5U+9JFcuX0mUHChXqa/uMUsc0jI838LGjEZ8W0L2XS+/5QBQxMmmMbDmV37nm +ysa7cbR+Ybt61pPxhjyRXgCx1/5yScl8+RSWAgnA+qVxYTFM8su6frHKrlnyvkqN +OLv+GwKBgGGNIp/ie4vMgWNa1JuaKD1bpzRUq9aI3bIsCRJw/rRkpephAFAZTh8e +F4Knbqj7okpCRu6lVyMmf1IKeRxXz+pSr2tHlp9JAsdt271pP5DWa8iwjEfmIIHU +Nf2Op/ydxNO9sZbW9PXWM449RWGt1yweZD4lGIMq9oXJbJL8VhAc +-----END RSA PRIVATE KEY----- diff --git a/tests/resources/charts/gateway-test/charts/test/certs/invalid-ca/server.crt b/tests/resources/charts/gateway-test/charts/test/certs/invalid-ca/server.crt new file mode 100644 index 00000000..c682341d --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/certs/invalid-ca/server.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDHTCCAgWgAwIBAgIJAOkxC9iq+eDPMA0GCSqGSIb3DQEBCwUAMD8xCzAJBgNV +BAYTAlBMMQowCAYDVQQIDAFBMQwwCgYDVQQKDANTQVAxFjAUBgNVBAMMDXRlc3Qt +b3RoZXItY2EwHhcNMjQwMzEyMTIxMTIwWhcNMjUwMzEyMTIxMTIwWjA/MQswCQYD +VQQGEwJQTDEKMAgGA1UECAwBQTEMMAoGA1UECgwDU0FQMRYwFAYDVQQDDA10ZXN0 +LW90aGVyLWNhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0oKsACRp +RQuOEKmEV3V+nsHaxrAbRdAaLJHltoHkI5He1xdOIvhBWps/Ms81XydybOOrbbrz +qZdhYxG1WwrP1NVf3lfG06I//TeBS9vTjLecXc1c/1kJIWeHbgq11dg/E2vxDUVJ +2jIV4xNf/lcmV9nb3C5x5PUGNfju7im/DV/+9x+dN/kFBry5AbXwwZzm76AZ19sn +EN6sx02sFXIKTXNi1xNo8AQcjZfPPrHIQZH1wUxC2zVaRE0SNzMlII/djJgBOAKz +RoMPK7DxPg4VQphDFErtuUhxmcFadyFEBRCn2lGDShRXv/7AWLZr//2cVjAV69Xt +wjVfD94U+tFhFwIDAQABoxwwGjAYBgNVHREEETAPgg10ZXN0LW90aGVyLWNhMA0G +CSqGSIb3DQEBCwUAA4IBAQAW5ZtDgr5vgzt8OkbZUgPisB19GypSLv6tg+wETh78 +Cz/Yqa2K67/z6doYUFzR+uoqJthS+8uuuMR9XNBbVjpFfZuLQDFO88tNaCqPArVw +UEZb5iL9rMBIKm+tkbl/gLp/nECMIiIn4a1t9BA3BRR3nuJwgkFINjf4XNM/wZK5 +4W3iptOYeSVnAZ4c3mMj8Gl26vwXy1gynglDbvL2PYxl3cV6bSk3DqE2bAt5SE7G +Gx40iUUjtGx7wD0QqtFjCVNpQeTD19ELrl4+fX/K4kkaiFjjAWqE/+KmwXfXqN7q +jPEwf/npYKRMt5a+JDotAsLdNwoAzQfglVVRvVs0Xkui +-----END CERTIFICATE----- diff --git a/tests/resources/charts/gateway-test/charts/test/certs/invalid-ca/server.csr b/tests/resources/charts/gateway-test/charts/test/certs/invalid-ca/server.csr new file mode 100644 index 00000000..b4f9fa1e --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/certs/invalid-ca/server.csr @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICrzCCAZcCAQAwPzELMAkGA1UEBhMCUEwxCjAIBgNVBAgMAUExDDAKBgNVBAoM +A1NBUDEWMBQGA1UEAwwNdGVzdC1vdGhlci1jYTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBANKCrAAkaUULjhCphFd1fp7B2sawG0XQGiyR5baB5COR3tcX +TiL4QVqbPzLPNV8ncmzjq22686mXYWMRtVsKz9TVX95XxtOiP/03gUvb04y3nF3N +XP9ZCSFnh24KtdXYPxNr8Q1FSdoyFeMTX/5XJlfZ29wuceT1BjX47u4pvw1f/vcf +nTf5BQa8uQG18MGc5u+gGdfbJxDerMdNrBVyCk1zYtcTaPAEHI2Xzz6xyEGR9cFM +Qts1WkRNEjczJSCP3YyYATgCs0aDDyuw8T4OFUKYQxRK7blIcZnBWnchRAUQp9pR +g0oUV7/+wFi2a//9nFYwFevV7cI1Xw/eFPrRYRcCAwEAAaArMCkGCSqGSIb3DQEJ +DjEcMBowGAYDVR0RBBEwD4INdGVzdC1vdGhlci1jYTANBgkqhkiG9w0BAQsFAAOC +AQEAk6YOJmRp+2mhymrdtBJdAbNEYsHgKw90oLFTvlxG9l1rGsrrTF1mYRfFG/cX +19S/yxIshcJmBBB1o/U9fQFMbDq4uuATjGX5sSh7IALkDOq0yVwObYuj1McguQo9 +B7IRUV5LO/lkOB5dY/LWPXOt9MKTv1Q3vTuSkMdRRe6nhiQ3+n2GcV8TVg2rCSdX +gvVs3e3HaZ7Yiz4nZ7sWVmytSK8Rnrz81Ss9iFHPcEVeiZlN3CUQUejhjf1/4DJ8 +jaZz8B0Aw2F7SH/NxNSu0KWEMaBYuZ5+DIWGYgQKCUktF2i2a7I1LQwzyViAY+P6 +ZXFmhzf0N49oueKgX5XqH6AKlA== +-----END CERTIFICATE REQUEST----- diff --git a/tests/resources/charts/gateway-test/charts/test/certs/invalid-ca/server.key b/tests/resources/charts/gateway-test/charts/test/certs/invalid-ca/server.key new file mode 100644 index 00000000..5b8c6d82 --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/certs/invalid-ca/server.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA0oKsACRpRQuOEKmEV3V+nsHaxrAbRdAaLJHltoHkI5He1xdO +IvhBWps/Ms81XydybOOrbbrzqZdhYxG1WwrP1NVf3lfG06I//TeBS9vTjLecXc1c +/1kJIWeHbgq11dg/E2vxDUVJ2jIV4xNf/lcmV9nb3C5x5PUGNfju7im/DV/+9x+d +N/kFBry5AbXwwZzm76AZ19snEN6sx02sFXIKTXNi1xNo8AQcjZfPPrHIQZH1wUxC +2zVaRE0SNzMlII/djJgBOAKzRoMPK7DxPg4VQphDFErtuUhxmcFadyFEBRCn2lGD +ShRXv/7AWLZr//2cVjAV69XtwjVfD94U+tFhFwIDAQABAoIBAQCbkihs3nvRo+10 +kOKWA+X0i40UAvfUyytcvuHF1A523wmRac679z3NKSg2c32c+bkNkd+R83S5Y398 +SIz/YGkhgCMeXT46DxE9IDT0i9u2hccQZ4GP0Av4XNtwTof9JpfO0ZnOVeNzVkpo +i1wIyf0zNXTPLp/LNe1GG9bvuXhQ98ZfuvBYZsOpNdQJUMN5gSqzRMMplfhaKkg2 +KwF31ydbGhTBZhqhSW2oaWJVCMz09+BkKMmnrSBNdYR1NV1KBkx3Scj6znBflmxW +K2zNHF78xuioXtFPt2B7Lqu8uZvZWT30SescYUlMct9dgn09CesXVwXTrZDkvinr +MUb6OMYhAoGBAO8Iug2TFrPX9mlUQLXOcagX3pNnsQQe4Mn1x2rw4xvrw/s6VJyu +qXm3ffCvvMLyTZ3i2SY2I+VLHj6FeAv7BeeZZEj00CtyXvvALIPZHxmlPa6BFP9u +xln+l3VbS1cyFUry39uz3WELMSrWaV2wcssiq0L7uaIo38IaOb07EOixAoGBAOFz +quHNbgGJ5DShUAPd3qD9Yc9uYJmmVbiSuXVgXhMpwAYJXxnaFSlo7lPYR2s33kaA +BpvnrdwHqUkNlKBqPRwsOOoGFnZ7JGYQNXsg/E1cnZ8MuwfQCTVTCkV1c7mnasRa +IJ+b8TdG9gUupqo2qW+wrreYyhsNzFYAyHgkZFhHAoGAdQvF5v2+YSP/8gWihiPn +vZKql21v3X+tPNeP5Yq8+qAQ4ETox6wzKnmyPpgfCyqQ3R4GjNJ380A8OAstBFjP +xF91HtBZ2txvLEEmyw0XUHx8XqWwfX9luw2SZpHkq3bHvGJ/QVqqrWlIkxxYjdrn +6xY33F3cwU3Ye3hSC5oPppECgYEAl/04mJ27qcHiXTDbFqA+9F2d0Q/ig/NFGvef +m+fpxBWDZQ5wVKdXWOFquo+2Jiw152VsDzLzXMC1eZB0QGke5Z1SiUKtZhbChSQs +SeQE88qaYJ1egXfYnWBsLkNuTxz0t4bjM3cX+WIXfYrjxSCwvaFpSFDy/6YfuWMx +wv0VwQUCgYAZ4kpCK4bdzRMf2jBZCFkcZUtLKSYWj5kgCTFgtNzpKesjKRP19+FW +PX5Hpi2oxgS2ivXaZ7hMU5AhMyPQ/bNwy2A1Xhvex+hktATw3Gq+/NAeHEeGy5iX +bENXVVE53x4rnbSpXxDoKF4yla+EBRHOrcRmHTdgaO1pKZ1YZKCnUQ== +-----END RSA PRIVATE KEY----- diff --git a/tests/resources/charts/gateway-test/charts/test/certs/negative/ca.crt b/tests/resources/charts/gateway-test/charts/test/certs/negative/ca.crt new file mode 100644 index 00000000..4ef28553 --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/certs/negative/ca.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDLjCCAhYCCQDlLPrl+EG3ZzANBgkqhkiG9w0BAQsFADBZMQswCQYDVQQGEwJQ +TDEKMAgGA1UECAwBQTEMMAoGA1UECgwDU0FQMTAwLgYDVQQDDCdtb2NrLWFwcGxp +Y2F0aW9uLnRlc3Quc3ZjLmNsdXN0ZXIubG9jYWwwHhcNMjQwMzEyMTIxMTE5WhcN +MjUwMzEyMTIxMTE5WjBZMQswCQYDVQQGEwJQTDEKMAgGA1UECAwBQTEMMAoGA1UE +CgwDU0FQMTAwLgYDVQQDDCdtb2NrLWFwcGxpY2F0aW9uLnRlc3Quc3ZjLmNsdXN0 +ZXIubG9jYWwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9adsQ3NJj +UsSh4dmV4U0A5RSKrexJS/Y/MrLCGz9c7r1JvRuLpI+AlGMWYnPLW87Gz0XFpHVJ +wX4YnpKxUcQrbNcMhCo2sOvZr/9ignWFtJK3HGpv5Bz7K/ZBPtTYpEYT6LuiKO1K +Haw61AcnsfLc4Dr1WsuTuEYJlR1zYfzp/CkiuwZJslb9De5Vkav6TVkn65qcPNt8 +GHuARxr9jrBn1vEKBKvCYWBTG5Ia8HA18YCJvAJcMYpBE0Vr0QXOuhBMNrcGwYm2 +ZvMrb734ZlYXFSW/tbaps9Zoc5/DoRUxesZbItRMEAKJ49PxLO32tliEAyWf4fEa +1yeUnLSvriDDAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAILey15A7O60YSDPKgLR +UTvB9WU7W1ulPeMgzqFjub6HOenTsVxqAKZGYniJUPrZw6QKi30XkcJQIFqcrWQi +N3tt//EAKotZ14rxZ0WT7n3je98gMVzPrOEr8xLYi39xGBXHfWfrBhAlOXdtOlgq +/kO0T8eleL9+LUUq784JUdjl+cigDhrAaQ/5UuZ/E9WSW+7QnGMC1B1STjel6Zuc +MPSoSPK7F3zP4a5AtFk333UIihmOwkgeMTdtrYY8phr4VvYJ4PmPwhR4cmHnTNmf +rwna3C0yyR9jv6RtB6V7tq3EMJBfViUIvSVyHeMN2+JKyrBWXF4FT8kC8zp3DwUm +jN0= +-----END CERTIFICATE----- diff --git a/tests/resources/charts/gateway-test/charts/test/certs/negative/ca.key b/tests/resources/charts/gateway-test/charts/test/certs/negative/ca.key new file mode 100644 index 00000000..b6a4f0f4 --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/certs/negative/ca.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC9adsQ3NJjUsSh +4dmV4U0A5RSKrexJS/Y/MrLCGz9c7r1JvRuLpI+AlGMWYnPLW87Gz0XFpHVJwX4Y +npKxUcQrbNcMhCo2sOvZr/9ignWFtJK3HGpv5Bz7K/ZBPtTYpEYT6LuiKO1KHaw6 +1AcnsfLc4Dr1WsuTuEYJlR1zYfzp/CkiuwZJslb9De5Vkav6TVkn65qcPNt8GHuA +Rxr9jrBn1vEKBKvCYWBTG5Ia8HA18YCJvAJcMYpBE0Vr0QXOuhBMNrcGwYm2ZvMr +b734ZlYXFSW/tbaps9Zoc5/DoRUxesZbItRMEAKJ49PxLO32tliEAyWf4fEa1yeU +nLSvriDDAgMBAAECggEAR5KEUK7gYN+ZpYHt8hCcREZLqMtniZrGhcLmgSpCmx8r +L33htraL8w4fEwpIrwMV81HHD5PBLgmLWEozLAW1lqMd74DRYrEfrbYvTk31knxV +JBP8tCMCQHawKp9PVj1crZE3tWK5p1PnDKOpwHohRw0DukqAumTbMivCYSMZqmAT +riaJYBh30mabiHEBLhcvkLh8lg8i2LsD9Jjx3SZcVpwrGaADiR2YtFLTOhVS08Uv +Lvp1SrvGhnyu9lTBI+XTq/vqrPCKsfM3JKSbzKf6HamUYweAGiIbjnro0uhpLLD6 +BXe9N1PMJjRO/w2jvO16zJItGePjyvrkx4W2nf71gQKBgQDlOz3xp67UK2Dmre80 +riplj7Vd9AL6n+5AdFR0yktoc7+t1SJG9j9XzSzKTH+Ix8g/oasxF3FBoLnZ0z9M +tV0xnbEXTXfu1vnsQxsB6CTwBpgZvTC2JLpyPZP8YFMoOvLTET1xI7oKvvlTyZHo +XSqk25cTjwCd8nODJglOlyRoUwKBgQDTiEb1OIKKRhfmiwO/Id9oyE1BbW3DgK3C +TsK9dXZf7TiygNAQQ/7zCuED9jbhTJWkxBLsbGUib8VUaJsS3SrXIyJbCDErsn/p +rJ+oKTEpI24ZJyg4iEJmBayT53KFFF0f6/ig/GlIsc/Gn07oi5kaJOr/tKiduWCL +JFN9i2SX0QKBgQCqU3KbdLT7AaBmxybORftKq5Vf0kfEYcFuMwHuJcISQq9SQuPN +RnuaieGWD3FT+N5aKY5CU+Dbmsl9iPGn1bsBeuJzJiTPWv0pCFOw/wUzNDMgLOtc +67191TN4ezpO0j5LhqvYvWsnQO+RylyYA2IETQXcio0yz0v1TvXrZ3Kt8QKBgQCe +y5jpEYj9oGzkxssDOrxp/qPwT+Osdfb6/QE4FOvOS1jat9R5wXGspigRP04nh8R2 +sjK6hQzO8zUhjn2LhbhZVKi/ycCP2yonE02vgWzEQzKtczXAapnd2LibN45C1Oyr +wAsfXxzyU3l007b634EJnVlEqCxEaxtMmPKMNo5HYQKBgGQQpp3F0SVAMO5IOZ7B +2aDT7lYKCyDg5OoXAJ1/MQNWG6GaXGWAZwE4NHBtocA9Hp+/7ouLwV24ntVQ9Gv4 +leGyO5GxRdbGZn9YUE9huAfxP7Y4ejij82alJctcs+77XGdURKWbp+0NECQdpa2O +VOUeSCHmcdgSXQHmOdcGDB3e +-----END PRIVATE KEY----- diff --git a/tests/resources/charts/gateway-test/charts/test/certs/negative/client.crt b/tests/resources/charts/gateway-test/charts/test/certs/negative/client.crt new file mode 100644 index 00000000..1d42e07f --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/certs/negative/client.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDazCCAlOgAwIBAgIJAOkxC9iq+eDOMA0GCSqGSIb3DQEBCwUAMFkxCzAJBgNV +BAYTAlBMMQowCAYDVQQIDAFBMQwwCgYDVQQKDANTQVAxMDAuBgNVBAMMJ21vY2st +YXBwbGljYXRpb24udGVzdC5zdmMuY2x1c3Rlci5sb2NhbDAeFw0yNDAzMTIxMjEx +MTlaFw0yNTAzMTIxMjExMTlaMFkxCzAJBgNVBAYTAlBMMQowCAYDVQQIDAFBMQww +CgYDVQQKDANTQVAxMDAuBgNVBAMMJ21vY2stYXBwbGljYXRpb24udGVzdC5zdmMu +Y2x1c3Rlci5sb2NhbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM/h +JSE+dkK0OsEb9ahdwkSn4kmtDtl6MeiRKCXjJgSH0qjPeWWQEqSPhTlCJbHosXHr +DCHoXG/UsbmThIpZwOttZ9OdzwrNXLZT5T7QtKdqLqh/wfYyzx5kFjO+sTcFhuCn +fmiaXoqtZpQXjqYfAJ0lY8VnZJQpEMoftLPDlTju5HoMkBMdANeKtqfvNzPAXnKr +jlaRa7GS/X4RguRSEhkVTryYs3hRqS3ynTdj54d3uWxdIb03QqH5p9VfjHRfLdUk +itIxMYXgHYclN9K5bcHDH+56WvOOeRtJCZGExHO+vK7fh84TN83dHCWZK8iEn/dq +1lv9GsIrj+pHtj5SB2sCAwEAAaM2MDQwMgYDVR0RBCswKYInbW9jay1hcHBsaWNh +dGlvbi50ZXN0LnN2Yy5jbHVzdGVyLmxvY2FsMA0GCSqGSIb3DQEBCwUAA4IBAQAL +jU+uhzGjpO54mrJSOTCBNb4LQbLrGDF5MjjzVoXbXm4j3c7a0wzIhTQ9tnV0PvW3 +ozrF0qmE4ihfoLF1WRzB1e+kAT26VpDNEsTs05J8mt58jKuLifrv2OY3Wv65zj0l +cDETnR8xaLL/jfLPVGqDdt1CUbhxb999Nv0rW0Jg+rsqzXzpIs+4Aiwf6FsaAhZh +7YxtbcA89R6Go6liN9wwx6jq2n1GcXlf/itx9Tru5ouv3vZUwS51EHMMHm2Sdueo +mwAxyAf/ALdEAv339qz4s5d4814RuQD1oAoYu7uEiTChOuOnqGLvq5XPSwK8qsq7 +Qeyr7rSduo0I2586qnRy +-----END CERTIFICATE----- diff --git a/tests/resources/charts/gateway-test/charts/test/certs/negative/client.csr b/tests/resources/charts/gateway-test/charts/test/certs/negative/client.csr new file mode 100644 index 00000000..7f7d9ade --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/certs/negative/client.csr @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIC4zCCAcsCAQAwWTELMAkGA1UEBhMCUEwxCjAIBgNVBAgMAUExDDAKBgNVBAoM +A1NBUDEwMC4GA1UEAwwnbW9jay1hcHBsaWNhdGlvbi50ZXN0LnN2Yy5jbHVzdGVy +LmxvY2FsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz+ElIT52QrQ6 +wRv1qF3CRKfiSa0O2Xox6JEoJeMmBIfSqM95ZZASpI+FOUIlseixcesMIehcb9Sx +uZOEilnA621n053PCs1ctlPlPtC0p2ouqH/B9jLPHmQWM76xNwWG4Kd+aJpeiq1m +lBeOph8AnSVjxWdklCkQyh+0s8OVOO7kegyQEx0A14q2p+83M8BecquOVpFrsZL9 +fhGC5FISGRVOvJizeFGpLfKdN2Pnh3e5bF0hvTdCofmn1V+MdF8t1SSK0jExheAd +hyU30rltwcMf7npa8455G0kJkYTEc768rt+HzhM3zd0cJZkryISf92rWW/0awiuP +6ke2PlIHawIDAQABoEUwQwYJKoZIhvcNAQkOMTYwNDAyBgNVHREEKzApgidtb2Nr +LWFwcGxpY2F0aW9uLnRlc3Quc3ZjLmNsdXN0ZXIubG9jYWwwDQYJKoZIhvcNAQEL +BQADggEBAGy7cJLILEJ+ZnYliPgdGBbsuXKnhhQxqW/0nlq7Vn7OeO0ExxfODw9w +4sXWuCTOa4aUlBqpn8d3YSJr4d5RADt2NR/KjxtBhHqwSo0g6yCqG5Vhr2KEQIe/ +I3z+HVpmgEEr66gko6/H3zfQAU7J7wwujfpyWm95d9HvOmqRhth2xnNNnThsjTiV +8yPxehA6bU4gzwYUm+qJ2F2Uu86ybAEGFtRV8RfupQbb2nzyfJv/RAEpbpCAR9MD +Yk2HkkGbvlnSJosQ21PlYvw0A5ZuBiq9MiNBsnAa8iF9jGPXEwXP86yLX2h4G7xB +gg/eQ5n8VMpuXxiDyRrONYngXTsiYxQ= +-----END CERTIFICATE REQUEST----- diff --git a/tests/resources/charts/gateway-test/charts/test/certs/negative/client.key b/tests/resources/charts/gateway-test/charts/test/certs/negative/client.key new file mode 100644 index 00000000..678c296d --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/certs/negative/client.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAz+ElIT52QrQ6wRv1qF3CRKfiSa0O2Xox6JEoJeMmBIfSqM95 +ZZASpI+FOUIlseixcesMIehcb9SxuZOEilnA621n053PCs1ctlPlPtC0p2ouqH/B +9jLPHmQWM76xNwWG4Kd+aJpeiq1mlBeOph8AnSVjxWdklCkQyh+0s8OVOO7kegyQ +Ex0A14q2p+83M8BecquOVpFrsZL9fhGC5FISGRVOvJizeFGpLfKdN2Pnh3e5bF0h +vTdCofmn1V+MdF8t1SSK0jExheAdhyU30rltwcMf7npa8455G0kJkYTEc768rt+H +zhM3zd0cJZkryISf92rWW/0awiuP6ke2PlIHawIDAQABAoIBAQC2yHfWYE6p3kFf +NQ9u6Gn95kRhlepdrUUfAit0DOOLzkWbqzpJ5EGQMqXor9HnOfx0d0Emu2Iz7qgK +zbwXzk2EdKF7f+Hh1Kq1otUKw4ZlQkceX5+TtB9L0KN5Ai5ee9yZwoyyuzFv7IIq +qwAB73ahtpOgqoXUhLs/jltcSRf3gvcPv7daADmwnRndr1leoaF/hbvjS5OMv2IT +aQ3F6d6VUzfVpJKZg1PSn4FYxIY1qc7O5hqm1lym9KZGP/D4tELmEXc/oo/cWsuS +i8HSE6qj+fFwzDv+m6FdLhZRtP1/0h4pIwW08ITHmmKoXmlEyjZ+OI4VQNARAqu0 +DM3vEWo5AoGBAP4CzBwAmlIhK+WMs4c13eo/Ab9ssty07sclUgIQCgfxNUIy3Afy +6/X8al5mcWRHdggKhnsHFC5EHN/qrcmmzWcXRt0W5Nbx+SErF3BjpzjFOXMC0yST +puPkOPCT7EhNaplyxvyOhAFSfzxB63XNgG2CQ1iQhdwOfZfVoeu0q0RPAoGBANGB +3tC6B9/7COsr7Dgww8TVMlV2wv9zwkn2F6+2FA0XZhZ+jIZ/LHKhcA95vKJLIpHi +08vy62LQzWQaTu1j+oqg5ivb0X1IdgRxheYLJduECnQKKU/6QKxX+tw0XtjKVI4p +GU8J/ZV9L/ksPgg0x8FigR4oOv59k0i9fxw5ClglAoGBAK2qLfCLPPcf9Mopq2ir +HIEV+NTutU8OaR5A1tPQMXuCn34WFbddj5QLspG+CpKcBQe0YoNksJh9Oxygb5cp +8s8j6/AmwehvYXwa4RiXGXJH7WJDsSYVyQmQNJnPGMHKJDKrdX6g1YGt7I2/KAPP +r5mvcOnxTYPJaHbRubXUPTAjAoGAGR+cy6TzWs2sxR7QRfC7GTiDv7HtMlr8Wogz +UPPhtawvptToHxzTBLANUx3DHCcsbxgnU9a+mWv2pWFuQ5NwsP0YfPvwRDjTRjci +2nJNyOQtqLqrN5cH+GLYh12UXiTtPNr62PqWuT146kV+7tb9eVhJqYcjg+8lIVzw +CD9i2S0CgYBKeCAjx6wYdvN/Zqn53I8YxgNgxWBJh+8RHBz6zOhhGQQWEfADkDmp +PjcKcyCnO52G20QA0y0/VNMKw35oZYSfC6oLT58ZBkeqEy8dQ994dYgACAq6BOyx +B3ZCtkOakOzq9xAFxJ35XaXjKSl9e0xPNzdbfIDOp69m3kV/U0UEWw== +-----END RSA PRIVATE KEY----- diff --git a/tests/resources/charts/gateway-test/charts/test/certs/negative/server.crt b/tests/resources/charts/gateway-test/charts/test/certs/negative/server.crt new file mode 100644 index 00000000..17160d8e --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/certs/negative/server.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDazCCAlOgAwIBAgIJAOkxC9iq+eDNMA0GCSqGSIb3DQEBCwUAMFkxCzAJBgNV +BAYTAlBMMQowCAYDVQQIDAFBMQwwCgYDVQQKDANTQVAxMDAuBgNVBAMMJ21vY2st +YXBwbGljYXRpb24udGVzdC5zdmMuY2x1c3Rlci5sb2NhbDAeFw0yNDAzMTIxMjEx +MTlaFw0yNTAzMTIxMjExMTlaMFkxCzAJBgNVBAYTAlBMMQowCAYDVQQIDAFBMQww +CgYDVQQKDANTQVAxMDAuBgNVBAMMJ21vY2stYXBwbGljYXRpb24udGVzdC5zdmMu +Y2x1c3Rlci5sb2NhbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKwy +l8RsLkqz6/I36/ytAfU75D+K8oUqzKoaBMWwS7xL+52pMersTsAP9be9B4qBu5+j +DGAWF1fAicseOYWZ0FFIFX65BQFmp+OCC5n3kyziMmbP0VJr7i/ILf75w5VTlsz/ +uZPnDJySW+H9J+BeW+NyMeuvX2ALwAy2ZLdT5sPDvt7m2avah4fFJIxV9AbPUdzB +hBcWo4M4PY0ePiUaDm3nv9c6sXHqtQu493mEWKO0o/tL4aATEm6QdFJjhCrZEL0W +T/Ws/Pm0CSy+wZh7eXJ2sonW7iGu0MDRKJQWz7yRJaugSY3YnJe9brYTPzLkd6sj +vLxvw5HdWDth5tkCKG0CAwEAAaM2MDQwMgYDVR0RBCswKYInbW9jay1hcHBsaWNh +dGlvbi50ZXN0LnN2Yy5jbHVzdGVyLmxvY2FsMA0GCSqGSIb3DQEBCwUAA4IBAQCK +POco+zTxJ0XsgCbtV8p0CqcqTzazYAwkj7M0V+ncVYuXfknz/DxV71dB0iTYkY5I +Fquf3zt00eK4y7fzgjLQEyZqh8uaIUlXvpMvpd867QlM+p7+NT5uuuW/TiwxLP54 +y2H9heMq+NCmwHRq1YI9htnFil7DaFGyNXaqrJj21RaiZT3S6B+oNPgYBA2PdJVq +6aUKZ1n2/9N+frJ/rFvTBqqIAqbIYSFXrvb2d64WCIcY/RKBR2Ilt1X8mn3ZKk4k +0dAHUREc8lCDOIdj6cUNy7vxuTd/FjqTXxcIIJxsMaRAD3iCLElMRMYSnY7uFQKL +jX6ZeR0ei6/D/j2PPdCH +-----END CERTIFICATE----- diff --git a/tests/resources/charts/gateway-test/charts/test/certs/negative/server.csr b/tests/resources/charts/gateway-test/charts/test/certs/negative/server.csr new file mode 100644 index 00000000..ad8a006b --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/certs/negative/server.csr @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIC4zCCAcsCAQAwWTELMAkGA1UEBhMCUEwxCjAIBgNVBAgMAUExDDAKBgNVBAoM +A1NBUDEwMC4GA1UEAwwnbW9jay1hcHBsaWNhdGlvbi50ZXN0LnN2Yy5jbHVzdGVy +LmxvY2FsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArDKXxGwuSrPr +8jfr/K0B9TvkP4ryhSrMqhoExbBLvEv7nakx6uxOwA/1t70HioG7n6MMYBYXV8CJ +yx45hZnQUUgVfrkFAWan44ILmfeTLOIyZs/RUmvuL8gt/vnDlVOWzP+5k+cMnJJb +4f0n4F5b43Ix669fYAvADLZkt1Pmw8O+3ubZq9qHh8UkjFX0Bs9R3MGEFxajgzg9 +jR4+JRoObee/1zqxceq1C7j3eYRYo7Sj+0vhoBMSbpB0UmOEKtkQvRZP9az8+bQJ +LL7BmHt5cnayidbuIa7QwNEolBbPvJElq6BJjdicl71uthM/MuR3qyO8vG/Dkd1Y +O2Hm2QIobQIDAQABoEUwQwYJKoZIhvcNAQkOMTYwNDAyBgNVHREEKzApgidtb2Nr +LWFwcGxpY2F0aW9uLnRlc3Quc3ZjLmNsdXN0ZXIubG9jYWwwDQYJKoZIhvcNAQEL +BQADggEBAEC85gqso/tBKu9xBcZKDBrQQl4wWyDjUw35X99AKQtZrqT/CcaKITV8 +1jBnXS5rk01QEj1M6w50cuwKnew1ABALav0dVewBtRD9Aa78+JHWpcp7LyPSDct7 +6XjXp9i7XoPzmpLqithoetQJxyxDx2uL8JOJIadoJre7M+/BnzhADeGHCS0cpVBR +8W5xkCtP6y5fn5h0mC/jLRGhq7tA20H6k20q4Y8tdGswo5F4pMuohw15KmYbAUQw +0tiqVdMDLq9IoSgvYPbgxoOR1lsXgZwq1B/MOOFEtMffiMcLrh6ndiONf1OgPJ8f +i9hM65I803Y9ZrMJXmJiKfUcp3ceUkQ= +-----END CERTIFICATE REQUEST----- diff --git a/tests/resources/charts/gateway-test/charts/test/certs/negative/server.key b/tests/resources/charts/gateway-test/charts/test/certs/negative/server.key new file mode 100644 index 00000000..159ed44e --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/certs/negative/server.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpgIBAAKCAQEArDKXxGwuSrPr8jfr/K0B9TvkP4ryhSrMqhoExbBLvEv7nakx +6uxOwA/1t70HioG7n6MMYBYXV8CJyx45hZnQUUgVfrkFAWan44ILmfeTLOIyZs/R +UmvuL8gt/vnDlVOWzP+5k+cMnJJb4f0n4F5b43Ix669fYAvADLZkt1Pmw8O+3ubZ +q9qHh8UkjFX0Bs9R3MGEFxajgzg9jR4+JRoObee/1zqxceq1C7j3eYRYo7Sj+0vh +oBMSbpB0UmOEKtkQvRZP9az8+bQJLL7BmHt5cnayidbuIa7QwNEolBbPvJElq6BJ +jdicl71uthM/MuR3qyO8vG/Dkd1YO2Hm2QIobQIDAQABAoIBAQCRiKzmMLwrHMdU +TtkfE6Vs+zJcVfXEgLi7JwRThD1uJhXBWUc8En44KwT0RknCUQUe1XHXH7SY0Lxk +s+XPuYDrwW2RTZQia/2G9dkSRsDXlVEdvZRfAaMsNRZSwgsAAMaZ+aOBkiwBhF0t +sYTrRzSIFXKFjBGiniuxUtHqc3m8hyejQYoEfwfRioDf6xrPN4RaGqrpE+JaBbQG +U4Hay14GbvA3ngMgmXlJYcWEV26UVq4tFv4xpCwxGB1/X6+ovGtn/64glWsRTvMb +zak8BEnP1cSaXGrMAjIzzXGw3WzKib/nDhHzCQcbgmIdhZl1mF0Bz4DtdVKEe07d +uf7suvRNAoGBANbGJ3Zj6/1fzlfA5FWIZnMKMN47HQJdirsG9ZEjm5KZE0EGhjnB +fNbCwPQZZlFwxWuIqp6/Zfw/UoOiEeXiglAhmnA9cPUZkh4UrcK7nMJGor7jficG +DwmNQoXjhnH/KMor31ntgBbhVRLj+e/RtoaxANioxL0Vv4bDhaW2jjd3AoGBAM1A +Q39o0OqvgM1UMgYMs4PverGqKDYQVqHyqxqPmL3G5TR6uAg8Isg67pSX+i+dEJ/5 +SqMY3fDBaU8Xr4/eovwPnLwWq5z8szpCIFg1mRQr76mR8PJwwj4J39DuEQIE8TaS +QUyAh275ookkn5kjj8rfq0+TppalY/U6M2kPN6A7AoGBAMKH+HZjSvzUKjGRpT9T +rHfGYzzmjf/2ehGs2//6II9H1wiuwCTP/CMJg3uVBff+DNK5ltDyy40OTc6snUl7 +QE0UIq5G+GkIIDDeygP3qqTNFduQclMmSbh9GiPrUXsvgeKcmlD5rWsL7eKOW3O8 +n3agHAQh2RDrAe8uaX8POwFBAoGBAI9GcNebn1pzsIGkaFb4vsc2gHtMwE0dEpxx +/SbJXmH7WTxM/fIhqFYFbU2k2Swrg9Nn/cXkMelB2fUwH4labINvkoVpfdpUO/hK ++LEamQUPtni0O3HBbJZJ5ka+KHk0Yf0qExMIFYJOGDuLqS0JOfLwN3GRLBS01xXz +zrdju/zJAoGBALGCYXxGpK8Rs1EVb7+WF5y/AwMngkvtZjYo4vNgn92PPgO+5Y8G +60YpIMqytDh7vecjx2g7UVp9xRB1PUT9sGQajRrAPOF6zY2RkGXHvsVCF0/4fcEb +7JeNT+1lJGPW/Bduzzd74qSDJ/QyB+Km7PRQx19lLHhrz+uPXrW6J+f4 +-----END RSA PRIVATE KEY----- diff --git a/tests/resources/charts/gateway-test/charts/test/certs/positive/ca.crt b/tests/resources/charts/gateway-test/charts/test/certs/positive/ca.crt new file mode 100644 index 00000000..18f0c610 --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/certs/positive/ca.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDLjCCAhYCCQC6YSuCf2n/jzANBgkqhkiG9w0BAQsFADBZMQswCQYDVQQGEwJQ +TDEKMAgGA1UECAwBQTEMMAoGA1UECgwDU0FQMTAwLgYDVQQDDCdtb2NrLWFwcGxp +Y2F0aW9uLnRlc3Quc3ZjLmNsdXN0ZXIubG9jYWwwHhcNMjQwMzEyMTIxMTE5WhcN +MjUwMzEyMTIxMTE5WjBZMQswCQYDVQQGEwJQTDEKMAgGA1UECAwBQTEMMAoGA1UE +CgwDU0FQMTAwLgYDVQQDDCdtb2NrLWFwcGxpY2F0aW9uLnRlc3Quc3ZjLmNsdXN0 +ZXIubG9jYWwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC/FAKyCbQq +jBAvUibELXjBK0Adp4f2oCdb/JuEiy0F5vYi8dBval1kXC8Hh3pKPBHfuPAJXzFg +DsoHynDJUdDCtsV7F7LFJ5o/ufQpLz7UkhViYCEF1ANeeD4vLNc6DDNy77brPXtj +7o31j8rTk3ZgKQCo8vpO2sJNjk+KGWjmZS+P+8QtmFYx+LAGr2pHACnZpsuRQVLL +ubZeJnkg676FZj/KWgdk+IVES8Wod+tzMeeGyE9KK2baqAdx0/Y0ifdXP5x1X2Db +XafPt2PQq+Y1UnM5zsjPxXxsPj+VHcsPbXrifVStz7/TgfjU3fwwS/yZ4X/8qfIQ +pJl+kYHVj1DbAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAHLvfIqClXqYPKbmKWiD +7xyz/ULQkcpVXBMMtHytrZ0SUBjqK3TUdgmWJwdNuGQpx9GhvJXc3P9qhJeOhkoZ +zjbWQUL1k15cdFtT46fGEizuPkGqGz0IOIfqkSBifFpKnrzcxt9+Z5BryIwB3nm8 +j5o08MeS20gTM2ngbVdEdXQv6wGHPTtlS0s0HY82+odcm+oGGOVIyylC0EZQClTS +Bcmy26Oid/9VVymF/q7tc3mPPNNltGVp9b1CUBXDD8G1OaN8pZ54cgMLSzgahNfk +H0U4scgVfaN/Lc71E1uunnwBaV22NZcWYvVFV3WY3FMB9atClJEqD9OOqGXerzfH ++jE= +-----END CERTIFICATE----- diff --git a/tests/resources/charts/gateway-test/charts/test/certs/positive/ca.key b/tests/resources/charts/gateway-test/charts/test/certs/positive/ca.key new file mode 100644 index 00000000..1aa41ce3 --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/certs/positive/ca.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC/FAKyCbQqjBAv +UibELXjBK0Adp4f2oCdb/JuEiy0F5vYi8dBval1kXC8Hh3pKPBHfuPAJXzFgDsoH +ynDJUdDCtsV7F7LFJ5o/ufQpLz7UkhViYCEF1ANeeD4vLNc6DDNy77brPXtj7o31 +j8rTk3ZgKQCo8vpO2sJNjk+KGWjmZS+P+8QtmFYx+LAGr2pHACnZpsuRQVLLubZe +Jnkg676FZj/KWgdk+IVES8Wod+tzMeeGyE9KK2baqAdx0/Y0ifdXP5x1X2DbXafP +t2PQq+Y1UnM5zsjPxXxsPj+VHcsPbXrifVStz7/TgfjU3fwwS/yZ4X/8qfIQpJl+ +kYHVj1DbAgMBAAECggEAD00K6jbctouAwElT0WHSyaUs/TLtMFKi1DrmOTbr5A0a +qLG0fzeFQwQev/uZT1iAFeo5TobQ7WBBzV3oqjZjATShm7nKFv+U2oWJh8LAxUTt +cXNBMbZIjsgSMrTkh0Fy3UFU5IGH3/i6ZW+eTlMAp7Kg2uaaJLZf2NYMiIKAY/KS +2Nx/+NWcyoMeZm2acQzy6b9cNH90o0SS76JdBB+89XPktuNmhht2oFsE215UQAF9 +PUoVC76DGtdiUjWNY7qrYxvTsn2TeJ+ufQhB0mTeLtA5+sj67jM360wg9C2EHP0m +D70Dp+B6PIvr7RbqaYWKj7yCIa+/pdndzvI2+nKYAQKBgQDtugeYJykQpkx1op+b +INebcUdPWmHq3Q0mi7Ufqj3c5mndSRj749A7yG3NT/jRx1i/xhfgvvXyT9SAVI0d +GAnYnzkS4q7MhRKcW1qblJeObX5k2AfstTu1Zs6R5PhdYDTmKU3/F+9XjQtoPKj6 +3rlwrjcp9hK5ACpdJT/lZtNRGQKBgQDNxAjQcqZpcSP9DA/ESbNKLbm7PuOkrXG4 ++4zUdtZp9AR5ZMK7blQnWKo0IbT4+NYq7ylLIzr5jKf1ecVOgyG5XSMVXMYkbMYD +z1m3Uyry6wcjss2a+RFIp5SLVOddj/CglSHmhIEVsE8gi5Sjo4aj27XzxbfwNLJA +mhkgf3AsEwKBgQDHDxfe2yOysl2hvwvAnQ6NNZyNoNQPEww485E1s5rbhwCsb9IA +0fECrkDrQ4TJPBBffONvqNdPEHOTBbmn3AIaprDm1HOkA+XikUhcsF77v0mv7Ykt +N1CJBE4CsmUZ4z5IX9vUt9kNSah8nxasAqXq6aZ9d3ST/sR6fH91etWFuQKBgQCL +SgTddn8IKbq+9YdGzM09ja6I/o2DUJYHLuGqgberia/trTPVRV5aND8jgx3K3Ee+ +UJ+XaYXmoDyig4f5GfOeU1oIgADxb2Cr+5Uz8GzGfCsdE1Dzc18r26VGnHbycxnk +2o9USKZJVEx8L4CzNWNTUMve9R0K0eFIsggIY7w/WQKBgHyevSW+TYHA35AfvQrW +Js//+9Q82IHK7nS2HeSKqneavJgyN76zWhBQCpB9GcKlnTYnjuj48ehzidWNIhOA +frBONzQZkGVKfpybcDiJ0vVn8hMvRHqggC2Z7MNy9crUdgO6npIOxQoLeo0sUCA3 +gYHtNHv0//OcyA6JXVKQAOcg +-----END PRIVATE KEY----- diff --git a/tests/resources/charts/gateway-test/charts/test/certs/positive/client.crt b/tests/resources/charts/gateway-test/charts/test/certs/positive/client.crt new file mode 100644 index 00000000..cc9cdea0 --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/certs/positive/client.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDazCCAlOgAwIBAgIJAOkxC9iq+eDMMA0GCSqGSIb3DQEBCwUAMFkxCzAJBgNV +BAYTAlBMMQowCAYDVQQIDAFBMQwwCgYDVQQKDANTQVAxMDAuBgNVBAMMJ21vY2st +YXBwbGljYXRpb24udGVzdC5zdmMuY2x1c3Rlci5sb2NhbDAeFw0yNDAzMTIxMjEx +MTlaFw0yNTAzMTIxMjExMTlaMFkxCzAJBgNVBAYTAlBMMQowCAYDVQQIDAFBMQww +CgYDVQQKDANTQVAxMDAuBgNVBAMMJ21vY2stYXBwbGljYXRpb24udGVzdC5zdmMu +Y2x1c3Rlci5sb2NhbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALla +/a1nh8anG0mpqOO/UtBDR/fGGgMRKL055DU/oAlKeoc9e3HBcAPp5cBbp+wyzcD3 +qoAzFK+Mp01J59t5uDFS6Sg1nqoPv+ujaj491q2WxN25WziahjBK29Z9MyoR9gCb +QT8QgxRT/hGALUQoKbbApVG++U/z0f6qhaOoseBaO7R8FXux8Q1dTG4vtIJv/mK+ +uKXFakDJdQh8h/kEMaO7pYBqhOpi8obAQL3dJQxQr7Jje0UX9pzeDDiZcy8b72TC +s/VgliiZTx/1c4cPris21kPcTMoOZ1DRbmrsEDKJfdAAfr11vKRJ7NPRSwFD7oC2 +ni9S9XKztcKwScP3YEkCAwEAAaM2MDQwMgYDVR0RBCswKYInbW9jay1hcHBsaWNh +dGlvbi50ZXN0LnN2Yy5jbHVzdGVyLmxvY2FsMA0GCSqGSIb3DQEBCwUAA4IBAQBu +dQqU671woPZW+gdWLexLOaRe/cS+cuvq/E7AV2Q5LzzradcTFf3mNi0THfORCl9s +3olkJaTcuHW4zP72f1/2B7xFv9Jmice40hdd9+GLucpZqH9hUG0RxzOEu8f9uupM +PyBML0tipwBq+pUOcbh6OHkOPv9FUMHW3GIzlSAEh+KmEmMK8Cxo95lS0fc4d7OI +VcFqKuIKez3+GmOX7leX5n1wWagNNalyzl/C9giSuWeFpboYmbFZSqn/MKl+otPz +rl440x+XbYZN+nK5Fg9+jbLi93aJj/LwkDoWUNGmofmKYdRhwBSg3hkOETHd77WU +Ad11TpbvdE3ONm9HOq0e +-----END CERTIFICATE----- diff --git a/tests/resources/charts/gateway-test/charts/test/certs/positive/client.csr b/tests/resources/charts/gateway-test/charts/test/certs/positive/client.csr new file mode 100644 index 00000000..5f49e4b9 --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/certs/positive/client.csr @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIC4zCCAcsCAQAwWTELMAkGA1UEBhMCUEwxCjAIBgNVBAgMAUExDDAKBgNVBAoM +A1NBUDEwMC4GA1UEAwwnbW9jay1hcHBsaWNhdGlvbi50ZXN0LnN2Yy5jbHVzdGVy +LmxvY2FsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuVr9rWeHxqcb +Samo479S0ENH98YaAxEovTnkNT+gCUp6hz17ccFwA+nlwFun7DLNwPeqgDMUr4yn +TUnn23m4MVLpKDWeqg+/66NqPj3WrZbE3blbOJqGMErb1n0zKhH2AJtBPxCDFFP+ +EYAtRCgptsClUb75T/PR/qqFo6ix4Fo7tHwVe7HxDV1Mbi+0gm/+Yr64pcVqQMl1 +CHyH+QQxo7ulgGqE6mLyhsBAvd0lDFCvsmN7RRf2nN4MOJlzLxvvZMKz9WCWKJlP +H/Vzhw+uKzbWQ9xMyg5nUNFuauwQMol90AB+vXW8pEns09FLAUPugLaeL1L1crO1 +wrBJw/dgSQIDAQABoEUwQwYJKoZIhvcNAQkOMTYwNDAyBgNVHREEKzApgidtb2Nr +LWFwcGxpY2F0aW9uLnRlc3Quc3ZjLmNsdXN0ZXIubG9jYWwwDQYJKoZIhvcNAQEL +BQADggEBAA1Wz1ttX4V/q5CwwcOG646m1g5bj9N/ejaDQpFrdCJQU8thCPIpjDHw +ubDRW/9p7ReXkHWhnT3rk5S2QOa5NNStdZ9fcDHWNku0nwj+AFx17OmuITG7Q77y +RWHGW21ewoJe4wwsDbrmVIvk3ZeD6ziP2xP2C3U8IVZ+kAdc56HB8cczjXhyDyYD +M1Q8e8ZHilWot+l9hYD8QQOFvaretzQbrfCG824GWpt61tk+omeYbUBzIlY5Y5HA +jhc7hKOwWXCWtp0u774sudnpsh0Fkgwi+eEhbgtdivz/4hzMvCp0CSZbgpqkXGan +VNzKT6E+TAe28EwP9KqL/ZYda9s6NZs= +-----END CERTIFICATE REQUEST----- diff --git a/tests/resources/charts/gateway-test/charts/test/certs/positive/client.key b/tests/resources/charts/gateway-test/charts/test/certs/positive/client.key new file mode 100644 index 00000000..c097dba5 --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/certs/positive/client.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAuVr9rWeHxqcbSamo479S0ENH98YaAxEovTnkNT+gCUp6hz17 +ccFwA+nlwFun7DLNwPeqgDMUr4ynTUnn23m4MVLpKDWeqg+/66NqPj3WrZbE3blb +OJqGMErb1n0zKhH2AJtBPxCDFFP+EYAtRCgptsClUb75T/PR/qqFo6ix4Fo7tHwV +e7HxDV1Mbi+0gm/+Yr64pcVqQMl1CHyH+QQxo7ulgGqE6mLyhsBAvd0lDFCvsmN7 +RRf2nN4MOJlzLxvvZMKz9WCWKJlPH/Vzhw+uKzbWQ9xMyg5nUNFuauwQMol90AB+ +vXW8pEns09FLAUPugLaeL1L1crO1wrBJw/dgSQIDAQABAoIBAQCy8ZtSW08Dg7Se +awK3zK+AjFPgawoVx+0Ssd8VYTV5gsPD6KFSczNXM+owyMvXBj0JfJDIb4ga6qlh +vmXuxxYB2E9sGEfzWn0oWn1pVX353EJ25Emi3duKp9qQuhI5HVnnv/s/jQtfBq+T +6bDJyhRrcJSp1LsQaw1i1PFrzKLdOdgNO0gA81tNAspmAM1+Kk8xVGAbmQkkRKUx +o4JTW0GounZoIJfg7fyfEi3dKyUakphnNu10WVKR2tqoAQhj/mTVl9FhIZtN73Gr +cCzdWj4I3+pmPBTBW3BWA5CoilZ27GqfFTStaeccrUOyXTfPgNEn4u03sm2jfpxB +zh8aFx5hAoGBAPYJGXX3Pel8Se8qKS447qqdgZl/0PEvQOIY9+SOr8NUGCuXgoGY +8DPMgopDDoeLyeOdNMIrhtO6SPKQBYyz2T5frEhj4UUI1HyfmT0mF6GdEBATPmiQ +AKJogIACQBDzDh2n52zfbUKxyZXfGpOOAcUMuIytn4sS/ex6Rs1IcI5tAoGBAMDc +wlNmdzur6t1fK5jxriMMVNWbA3ss4NSvNr3yTnRlhQlV22WkPbngPUGAAaDFweZB +B4FKSZzfLYp8a1cQx2Cr5D51QE73HkB9CO7s+CoxX5faT3cKI3Me7GPJjIORdiRI +oTMRSZOpiiNP9XavQqURefcVLzr+Zi7rpgFCmr/NAoGBAILoOInRsTloDhaYwix7 +0lEpWOmJXmzVjZo/WrZbTR2Kwwl+pcu6yiNlbxeNsk9gi1z2Kjod2rEQ7vtQsgM5 +Nh+/2/TwX83Rcu2UJX6po+0zmnZTJuOPqya+n5B8ogXirOIOkk4VWxcfbXi2qndU +GZD0wcToJHlk84I9VSqonmrJAoGAb9RR9awXfQk9oWkazY9tyrLOyiEdTqICKDEE +y/UhWsq27mfTVMd8ZzhILJ+90ex5dzrD0Es0Dfs22/MzBoQbJ8nkCfdQ97jA2OHn +eSr85vJEHLgglcTSM2F97oqiqHODDpzyo7rlb/LBv6IQkeYj/bT5hLTK8ykqNRC8 +7EQjmQ0CgYEAy4M6fhO8cp7x3Prevv0eM7L/uLPfsrCMxjRDNLZnUVO6gWIwTUGQ +uMzJGsISv8mg30w8QTSWN9nIjQhB1khOwrmTaASRMCOAFNDyR3U7meN0tzW7oXrY +D6kFQD/KXU9UEUyy9ZvMibEqZbFzkNG+LdiCC4UpgKPntkH6OYsRzi4= +-----END RSA PRIVATE KEY----- diff --git a/tests/resources/charts/gateway-test/charts/test/certs/positive/server.crt b/tests/resources/charts/gateway-test/charts/test/certs/positive/server.crt new file mode 100644 index 00000000..f06f315d --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/certs/positive/server.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDazCCAlOgAwIBAgIJAOkxC9iq+eDLMA0GCSqGSIb3DQEBCwUAMFkxCzAJBgNV +BAYTAlBMMQowCAYDVQQIDAFBMQwwCgYDVQQKDANTQVAxMDAuBgNVBAMMJ21vY2st +YXBwbGljYXRpb24udGVzdC5zdmMuY2x1c3Rlci5sb2NhbDAeFw0yNDAzMTIxMjEx +MTlaFw0yNTAzMTIxMjExMTlaMFkxCzAJBgNVBAYTAlBMMQowCAYDVQQIDAFBMQww +CgYDVQQKDANTQVAxMDAuBgNVBAMMJ21vY2stYXBwbGljYXRpb24udGVzdC5zdmMu +Y2x1c3Rlci5sb2NhbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL5S +9RpdEnd0AwmWfRE7yJGNX7x2YGwLF8jl9m4nzP2NoO+sYSICyulxRkCp3jywzLk6 +FeLMCQqhaQgmW8woGpwMDah1Blx4SNkYQrEOL39U8R7ertNbhgJt8CADi0YN2c3k +9AoYq/gjWclRkmepzTlwb+x3x9m7o5FKof8u8bC8/LHsekWJHDU4l1wbdX40APCK +jAMYxjTMIGK3qLHnKWdJdn3/642G37laKjAhGyWZMRAVcycDKp++s6t1DANu1C2N +A+63UNpAQCQmDei+2hu4o1HW995h/Dl3lg39DeMiRrWTlBSRad9ReyDJ2HuB0Zeh +46zU/oKc9XzntJd/Cd0CAwEAAaM2MDQwMgYDVR0RBCswKYInbW9jay1hcHBsaWNh +dGlvbi50ZXN0LnN2Yy5jbHVzdGVyLmxvY2FsMA0GCSqGSIb3DQEBCwUAA4IBAQC5 +XmOua74PU8PD7Ifkt5uJLGsb0rBRlLuS3aALXARYAu7oDj+TZR8b7f7Th8sGNm4C +dkjWW4Ke4Q9ts2pCsmM5SZyu6Xd20dZRE8Vc+OQFRHfbW8fHsAGob1gyypQbhG/y +xZNcCx/FLUKiVMJ5X8s72COIqHGbGfqSvhBxkClPgS1h9aeyapwSeYbDz2bSeSI4 +SRZZdvYOQYltERLjluXxsQsHE1HmFJ5jUknulES4brDEuXSRvAzXEkiSnDkdypV7 +WYibqLEX88XEn9lgl0LYgJLoYMr9sziJFtCLRkI09DV6MJs1V6VO7r4Nu12QGD/U +TmdknuKuy9cffLnjfUiz +-----END CERTIFICATE----- diff --git a/tests/resources/charts/gateway-test/charts/test/certs/positive/server.csr b/tests/resources/charts/gateway-test/charts/test/certs/positive/server.csr new file mode 100644 index 00000000..469db94b --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/certs/positive/server.csr @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIC4zCCAcsCAQAwWTELMAkGA1UEBhMCUEwxCjAIBgNVBAgMAUExDDAKBgNVBAoM +A1NBUDEwMC4GA1UEAwwnbW9jay1hcHBsaWNhdGlvbi50ZXN0LnN2Yy5jbHVzdGVy +LmxvY2FsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvlL1Gl0Sd3QD +CZZ9ETvIkY1fvHZgbAsXyOX2bifM/Y2g76xhIgLK6XFGQKnePLDMuToV4swJCqFp +CCZbzCganAwNqHUGXHhI2RhCsQ4vf1TxHt6u01uGAm3wIAOLRg3ZzeT0Chir+CNZ +yVGSZ6nNOXBv7HfH2bujkUqh/y7xsLz8sex6RYkcNTiXXBt1fjQA8IqMAxjGNMwg +YreosecpZ0l2ff/rjYbfuVoqMCEbJZkxEBVzJwMqn76zq3UMA27ULY0D7rdQ2kBA +JCYN6L7aG7ijUdb33mH8OXeWDf0N4yJGtZOUFJFp31F7IMnYe4HRl6HjrNT+gpz1 +fOe0l38J3QIDAQABoEUwQwYJKoZIhvcNAQkOMTYwNDAyBgNVHREEKzApgidtb2Nr +LWFwcGxpY2F0aW9uLnRlc3Quc3ZjLmNsdXN0ZXIubG9jYWwwDQYJKoZIhvcNAQEL +BQADggEBABKS9ForQ7abll9++o3CuQZ2+ZM1QYH2TrnHbjVsG0tl8PpSmPgkIgmr +lvqk9UnUcr8kV1iq+RqKkGctPgm3DUAX86h/TjbKfIO09IdK/7zMYZ4pgI+tYD4V +UHOTjlxCUn4HWkyHDGOjOfZNcRNBFJPIFrvlPjTftBJ9LMswyiXZLvrMvXU31Qj3 +vmtS8mBZ8zv6n/by5dNCUpneF88/2RkTj3ICdcJjgN3hqFqD5UlT0lN31BVKOq3f +Cnkff2S1byLNH7Jkv1w5taoTQzpeUStHnf7Er30It8r/hpDoAoGd7FSm3j4WJFEY +K/B5XB+3DhahLJrs3qhwRk3iZ/c77ZM= +-----END CERTIFICATE REQUEST----- diff --git a/tests/resources/charts/gateway-test/charts/test/certs/positive/server.key b/tests/resources/charts/gateway-test/charts/test/certs/positive/server.key new file mode 100644 index 00000000..b402f780 --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/certs/positive/server.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAvlL1Gl0Sd3QDCZZ9ETvIkY1fvHZgbAsXyOX2bifM/Y2g76xh +IgLK6XFGQKnePLDMuToV4swJCqFpCCZbzCganAwNqHUGXHhI2RhCsQ4vf1TxHt6u +01uGAm3wIAOLRg3ZzeT0Chir+CNZyVGSZ6nNOXBv7HfH2bujkUqh/y7xsLz8sex6 +RYkcNTiXXBt1fjQA8IqMAxjGNMwgYreosecpZ0l2ff/rjYbfuVoqMCEbJZkxEBVz +JwMqn76zq3UMA27ULY0D7rdQ2kBAJCYN6L7aG7ijUdb33mH8OXeWDf0N4yJGtZOU +FJFp31F7IMnYe4HRl6HjrNT+gpz1fOe0l38J3QIDAQABAoIBACtgyv5kQiY5qcuQ +ohbAcnlCKJTSwi095gDi8OSwa5dKpWia+FSBIHBOYf2w+bcJcM+yvnQ/nrvuh/rU +i02fwljYonBHo9iFjcz1K5YhLpAt8vrfNCd2D7gUCIuzYxXnaEH2MezvLJrUq80n +q1+3ItA5oTjbIBCvJJuj0AJSV8G5G/UXlMl1VVPRPfAeM+ib7J9nCFIFFW+OlKoi +n+VJJkXGKDyIs5caYEBu0Jlmvdpqq5UtxmCMTVuVKjGV4JkM9pSFv2KXB9aEadQt +X3msclWDmonWzEdDhgsSVWrRqCADduZZDJhQGdAzW2jVGGQVVLWqEW5wwhXb0jnD +bdFEu70CgYEA92FKsvJ/0LeMav3bAowaQsA4v6pYvnQCh5tlh5EutA62zoytFHtp +0QiIkmRdCvV32pyQj/vzUwLgQywhxoC1SChDfzJDkKPSpxSvt7DtmcHY792Dtlpi +eOLI9FrHyYlq1vaVMaXw8DzuMXAqaAgkdNgh1JoYpqcl22iMAXRaofcCgYEAxPS1 +SNLEwteFTpriySxouyHpbbkqTJrtjMTu/7njS6G/Xt5nUZTHdf0VpmT2YMELsQx5 +tBUyzoOdXinNXTbx+UVueJQ83ZQwebzrR5130l9/EogzRe+zZSFOUjm6YkNXK5Jd +ktNSVn6zxxKy2oBlFAdiL0A368U4i1shLa8NfcsCgYEA0y5YQZlo6bm3gqLBs1P9 +GxzTlTOL3NJWUoOjUe7rqsSg5IUNQF32wH8Db82D7FYPAi4D7xbL6wKahl2HW9kG +aNoOfOhg63oe24l6VFsTCt6EHojA5wwT4lTf7lINGgxYi7gnNyINJFkvkj7JxNOm +o6TahI8kGii41axTUO6ObJMCgYEAkZiQdtAQUjS+QBhxc+PXXBa6l7kdEtoopzph +rzt8Ukm0zW29lOpV9NvtaD8UfvvWJ8CgK0bMcyuKZrSiMrlOcUYpXwu+XtKQbz3/ +88XtcN/VcR6sQJPs2uKfIlu4c7FyPCyL7eE36ebqAUzKWIo3rnGy3FktvaXioenx +AfN5FrcCgYEA9qt7FEkmrvyxjjpfNc4niNQUz1Hho0Y13hpIxsJoHIia3XIr7P9r +GVjsbtbraGKHjy2vT+Fw/8u3FJGQBnIYgFL3owPP3kWqOowQb5e18kDcW8/KFyE+ +2BuRv0safg1R/reEGJEuRAm2Pw3VU3xqS09Q2em93YdWNjmAdloBz7E= +-----END RSA PRIVATE KEY----- diff --git a/tests/resources/charts/gateway-test/charts/test/templates/_helpers.tpl b/tests/resources/charts/gateway-test/charts/test/templates/_helpers.tpl new file mode 100644 index 00000000..9cba1391 --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/templates/_helpers.tpl @@ -0,0 +1,12 @@ + +{{/* +Create a URL for container images +*/}} +{{- define "imageurl" -}} +{{- $registry := default $.reg.path $.img.containerRegistryPath -}} +{{- if hasKey $.img "directory" -}} +{{- printf "%s/%s/%s:%s" $registry $.img.directory $.img.name $.img.version -}} +{{- else -}} +{{- printf "%s/%s:%s" $registry $.img.name $.img.version -}} +{{- end -}} +{{- end -}} diff --git a/tests/resources/charts/gateway-test/charts/test/templates/applications/code-rewriting.yaml b/tests/resources/charts/gateway-test/charts/test/templates/applications/code-rewriting.yaml new file mode 100644 index 00000000..19977c60 --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/templates/applications/code-rewriting.yaml @@ -0,0 +1,47 @@ +apiVersion: applicationconnector.kyma-project.io/v1alpha1 +kind: Application +metadata: + name: code-rewriting + namespace: "{{ .Values.global.namespace }}" +spec: + description: Code Rewriting + skipVerify: true + labels: + app: code-rewriting + services: + - displayName: code 500 + name: code 500 + providerDisplayName: code 500 + description: Should return 502 given 500 + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/unsecure/code/500" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/code-rewriting/code-500" + - displayName: code 503 + name: code 503 + providerDisplayName: code 503 + description: Should return 502 given 503 + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/unsecure/code/503" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/code-rewriting/code-503" + - displayName: code 502 + name: code 502 + providerDisplayName: code 502 + description: Should return 502 given 502 + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/unsecure/code/502" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/code-rewriting/code-502" + - displayName: code 123 + name: code 123 + providerDisplayName: code 123 + description: Should return 200 given 123 + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/unsecure/code/123" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/code-rewriting/code-123" diff --git a/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/basic-auth-negative.yaml b/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/basic-auth-negative.yaml new file mode 100644 index 00000000..1507127e --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/basic-auth-negative.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Secret +metadata: + name: basic-test-negative-case + namespace: kyma-system +type: Opaque +data: + password: {{ "passwd" | b64enc }} + username: {{ "user" | b64enc }} + diff --git a/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/basic-auth-positive.yaml b/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/basic-auth-positive.yaml new file mode 100644 index 00000000..311f41fb --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/basic-auth-positive.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: basic-test + namespace: kyma-system +type: Opaque +data: + password: {{ "passwd" | b64enc }} + username: {{ "user" | b64enc }} diff --git a/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/mtls-negative-case.yaml b/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/mtls-negative-case.yaml new file mode 100644 index 00000000..f2025ecb --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/mtls-negative-case.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Secret +metadata: + name: mtls-negative-case + namespace: kyma-system +type: Opaque +data: + {{- $files := .Files }} + crt: {{ $files.Get "certs/positive/client.crt" | b64enc }} + key: {{ $files.Get "certs/positive/client.key" | b64enc }} \ No newline at end of file diff --git a/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/mtls-negative-expired-client-cert.yaml b/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/mtls-negative-expired-client-cert.yaml new file mode 100644 index 00000000..b8e71c3b --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/mtls-negative-expired-client-cert.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Secret +metadata: + name: mtls-negative-expired-client-cert + namespace: kyma-system +type: Opaque +data: + # Client certificate expired on 02.08.2022 + crt: bm90QmVmb3JlPUF1ZyAgMSAwMDowMDowMCAyMDIyIEdNVApub3RBZnRlcj1BdWcgIDIgMDA6MDA6MDAgMjAyMiBHTVQKLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURkakNDQWw2Z0F3SUJBZ0lVZldyTzlHRG1rR0FCRktMdzYvL0tVcDdYMnE0d0RRWUpLb1pJaHZjTkFRRUwKQlFBd1dURUxNQWtHQTFVRUJoTUNVRXd4Q2pBSUJnTlZCQWdNQVVFeEREQUtCZ05WQkFvTUExTkJVREV3TUM0RwpBMVVFQXd3bmJXOWpheTFoY0hCc2FXTmhkR2x2Ymk1MFpYTjBMbk4yWXk1amJIVnpkR1Z5TG14dlkyRnNNQjRYCkRUSXlNRGd3TVRBd01EQXdNRm9YRFRJeU1EZ3dNakF3TURBd01Gb3dXVEVMTUFrR0ExVUVCaE1DVUV3eENqQUkKQmdOVkJBZ01BVUV4RERBS0JnTlZCQW9NQTFOQlVERXdNQzRHQTFVRUF3d25iVzlqYXkxaGNIQnNhV05oZEdsdgpiaTUwWlhOMExuTjJZeTVqYkhWemRHVnlMbXh2WTJGc01JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBCk1JSUJDZ0tDQVFFQTZSMU1VeVRTT2FyNGtPTUdMV1BWZUU4WXkwRTRlcGV3NS9aL2ZZNlQ5d1BWYUdSTE9RSEYKLzhCVjVEeHhGLzN0K0Y0S21tSHJ6TjlyUDN5T3VnVnY4bEhlek9VeFhmZWFrN0hMR2VGV3k5TGxmc3BtODhJMwpxR3BHM2FaUUhZS0VGRk1IUDREcGdkM3JUUVlhNVFnMDRNZDZuZjk1SFY1YzJtTlErb2pqUWt0cWROSUpPb2p5Clk4WjdlaWMyRURtMWxqYkdCTFZESEY1aUY3QjQ1bXp0OTgzZElaeC81TW9OMnpjdlF5OU9GMlkvcm1EL284QkQKd3llR2xvejVqTHNGRUJqbWdBSWFrSXBiVkZqekk4Tlp3cW0wOTZSM2JuZCtJdml3eS96YkRxYlNxQ2Qvdi9qRApIT0x2M0pIRXNLL0VxNng0VGZhL1lqTFozbjRrNmhCVHVRSURBUUFCb3pZd05EQXlCZ05WSFJFRUt6QXBnaWR0CmIyTnJMV0Z3Y0d4cFkyRjBhVzl1TG5SbGMzUXVjM1pqTG1Oc2RYTjBaWEl1Ykc5allXd3dEUVlKS29aSWh2Y04KQVFFTEJRQURnZ0VCQUxpMVI2MjhpOXZXWkkrNnJKZHN4MEhtZUZLUGNVenV3R1ptU2t2TC9rTHliOUE3b2V6SQpPbkRLL0EzOFpGcmpzTE9YQWRJZkxXS0laMkh5b21FNG9HMldNL3phN1BCWDc4dXpycWw5bFR2eWtQOFRkdm5qCkpTN2l4cWZucW1UNDdLVHFOSjRleXMzd2NOU29kTHlydWNDUWlLeUllMEFUQ3RUSUY2WnNGR3Q2WkN0WS84czYKQTJ0Rk92dmhTWm01UytsZGVEQ3FMajJMOW1oZEY0RzlMRmJ5Q2pTbVNDWEFSbDVpanBjb2pEWEVDRTdobTk5YwpnbU1scjE2Uzh1Nm5mWEVRdXFwTys4MU4xTEVpTzVyUzRwUGxBdGgyVk5YOXc0WHBmbEM1VDA0bzA5S21HYzhLCjhVTjB1YXJpRGx2NW93SEZlWVRsZkZ6eGhUUDJBOWFPL3U4PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== + key: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBNlIxTVV5VFNPYXI0a09NR0xXUFZlRThZeTBFNGVwZXc1L1ovZlk2VDl3UFZhR1JMCk9RSEYvOEJWNUR4eEYvM3QrRjRLbW1IcnpOOXJQM3lPdWdWdjhsSGV6T1V4WGZlYWs3SExHZUZXeTlMbGZzcG0KODhJM3FHcEczYVpRSFlLRUZGTUhQNERwZ2QzclRRWWE1UWcwNE1kNm5mOTVIVjVjMm1OUStvampRa3RxZE5JSgpPb2p5WThaN2VpYzJFRG0xbGpiR0JMVkRIRjVpRjdCNDVtenQ5ODNkSVp4LzVNb04yemN2UXk5T0YyWS9ybUQvCm84QkR3eWVHbG96NWpMc0ZFQmptZ0FJYWtJcGJWRmp6SThOWndxbTA5NlIzYm5kK0l2aXd5L3piRHFiU3FDZC8Kdi9qREhPTHYzSkhFc0svRXE2eDRUZmEvWWpMWjNuNGs2aEJUdVFJREFRQUJBb0lCQUhWalJZNFEyclFqZm13bgpobkxROVN4U1dGL3lCZWpsL2pXeEVWNCtzQkFScENPZmJhblZWTW1ISnpsNW5sSEFrMWNndENJdDhUb0h2OUFHCmZ6RDVqL2ZzZGsramtvcUpKeFA4MGhQRVA1c0FKb1VFazNkb2MvS2hJZkozejV3c255cEU3VDl6UVNNZWgyRVEKRS9jRmZPczhTR2pMdjBla3Z3bFNQZk1MZjdWZm9vK2h6K2krUW5jMXFRR2lPVW90Tk11WmoxcU85Mm5Ib2toMApxcWRRWlFOa1pUQm1sd2Y0bGNpeVJsTVUwRHJJNmd1bDNRazNpd2hueU1Kb2h4dVJxT3RSNzJjTS9SVFpsMHdWClh4WmJ5VkM1R2dhRUR0aUtFaE9xdmpPN2N4WU9BU3U3aGVDbWRnRFdMMGFSVjE1ai9Sb0twRUtkeWt0SHFrRjQKcWVLZndBa0NnWUVBL2pKaHBqSWh5N09Vajh1cW1scHE4L0tyRG5zUG1FK0VqbzFxOWdiTDByRVZSaktMNHpLaAp2SDdrdHJjQUkrMkpaS0hlSDVmVUJSMVYxK3R5WXM5OHUwOFpHSmQ0TFdjNjFFUTQvbEtFVUhaUU41TnpJOHprCmhWbjBhNWZOQjlLMDQ4aU1XQTh6ZlgyQ2JvWWltT0V3ZC9BZUtwdzZjN0E0UXU5RVRGckNhMGNDZ1lFQTZzU2gKb2V4U0pxbE9RMjk1VjlVWWtNTkkrNkR1ZGNoSUZkamY2bUp2NEo1aGhQSmJDY1doZkZjdnZmNkQ0SzBDei8rVwpybmhreWNlU1dDRHNuN3laN1VBbHAwVXp2bk1IYXBqUXUwN3JpcGI0UGExdlE2MkxRYWhTWnFJZmkxeFVqQ1NmCm1GcDJZSXVRV2FWOWFlYUdmSnY0anRnZUJ6VUxFYngwMGdpb3lQOENnWUVBOUR5Q09JWm9sR2xxYjdOWHExRCsKL0grSVBiU2Q2bEZVNHdjYjQySHFTdmtjb01NR1Iza3BqNHc0d3hvWUIyMC9Hckt3VXBpMS9XZ1BTQlFRWnNKSApiVTExcG53NjJ4MFptRVFvb3F1ME4vOUYyZkJScSs4OURxZTh3ZmdyNXIxY1VwUXB6SjVtY2NlN0graS9xemFMCk5HSkJDZDNzQjZZa21LTitjd0t0VlJjQ2dZQkRKaFM1RkxmMm1PeHF1Mkt3cmFIR0hpVXMyNzM0OEYwMTZuODUKTWdpZjdZMGxFcERaZmE2UHV2eEwwcFZ6Mk9oNkI3ZllsVlQycGQrRTEzMzJ2bUlraXZsNkc0QU9WQ1psNWVtbAorWS9EWnlUL3R6Q2c0ZTEzelNZc2R1aWcycnJRRHRXYkpSekF4b3AyS2JCeWJ0NCttL24vR1crVlRpV3BZQWJsCjRGWXVqd0tCZ1FDT3VUenVZS1QrVHZuU3hDVWdZbUN3SWs5MlVxUHJCS3J5UE1sekxMdFBaMlF3UjNpRGlWTzEKUlpxNGtkc1NzU05SNUJKUzlrVW92dUpubG1hb0ZBM2NLZXhRdk9mcmFVdXNGcXVHb3NWR3hrSGo3S24ydnFadQppK0NDNTZPNHRRMnNhOXZ6MFNpTWZoMzdFRlh6eUFVODNpWGY2aTB0MGlzb0cvYmFpTlNDZ2c9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo= \ No newline at end of file diff --git a/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/mtls-negative-expired-server-cert.yaml b/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/mtls-negative-expired-server-cert.yaml new file mode 100644 index 00000000..e96af97d --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/mtls-negative-expired-server-cert.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Secret +metadata: + name: mtls-negative-expired-server-cert + namespace: kyma-system +type: Opaque +data: + # Valid client certificate expiring on 17.12.2049 + crt: bm90QmVmb3JlPUF1ZyAgMSAwMDowMDowMCAyMDIyIEdNVApub3RBZnRlcj1EZWMgMTcgMDA6MDA6MDAgMjA0OSBHTVQKLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURkakNDQWw2Z0F3SUJBZ0lVWnVOZ3FCQTdSUEs4bStSSnhuaXRTS21xZFdjd0RRWUpLb1pJaHZjTkFRRUwKQlFBd1dURUxNQWtHQTFVRUJoTUNVRXd4Q2pBSUJnTlZCQWdNQVVFeEREQUtCZ05WQkFvTUExTkJVREV3TUM0RwpBMVVFQXd3bmJXOWpheTFoY0hCc2FXTmhkR2x2Ymk1MFpYTjBMbk4yWXk1amJIVnpkR1Z5TG14dlkyRnNNQjRYCkRUSXlNRGd3TVRBd01EQXdNRm9YRFRRNU1USXhOekF3TURBd01Gb3dXVEVMTUFrR0ExVUVCaE1DVUV3eENqQUkKQmdOVkJBZ01BVUV4RERBS0JnTlZCQW9NQTFOQlVERXdNQzRHQTFVRUF3d25iVzlqYXkxaGNIQnNhV05oZEdsdgpiaTUwWlhOMExuTjJZeTVqYkhWemRHVnlMbXh2WTJGc01JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBCk1JSUJDZ0tDQVFFQTRpZHp6dFhjZG9Dd21YNm9HeSt5elFjYW5iZVpmZ2xBTFlrVktUUVhrYXoyWW8wWXV3cGQKS1VhSjZEbkVUTVVneGYwb3RUWENBSU9HM1cySTZyRzc1UXBsVWZTNDVzNWQ4MVE5WGhMRm9Oa0o1ZWlzUW1paQpCSURQOC9qQ2RHSk04Z2hpa1A1bk1xQ0hRdGk1bkR3MmtINCtiZDNIVzgyWTFWWkg5Y3ArdUpDa251clMvbDArCnFSUkxndWZUY0c0VU5iaGEwWnZaNUcwMStTdnJxVXBudmVaQUdlcTlOaWlhUXhmSkFkcTdISHMrMnFDOGtEczQKUllOVFJNTUMrYXVueDV3M1g4eEUyR0oxRkdjSjNmRkszTFhsNnRXaWFBajVHNjloSnpoNG5HMzdEVy9aMEF0UgovM3B2UnB2K0k1WCtTUWZUVHBqQzBGbld6Z1JrZ2hucWV3SURBUUFCb3pZd05EQXlCZ05WSFJFRUt6QXBnaWR0CmIyTnJMV0Z3Y0d4cFkyRjBhVzl1TG5SbGMzUXVjM1pqTG1Oc2RYTjBaWEl1Ykc5allXd3dEUVlKS29aSWh2Y04KQVFFTEJRQURnZ0VCQUxQSmVJNGtGRlV4YlNVN2NtcHFjaFpWWUx0dmhYRkU4Y0YrUUZTRzhTOXZIOUVxdnlYNApNNW8wSkQzeDU3T0dnK0liVHJRQXRXVG5zbnNBbGk4d3FMQTJVYlZZTThOM0JRSFBSNHZUMkZjRndHQmpQR3hGCmlGcDZlUGxvbTJ0bTBEVjNXbzkzdG0wWnhmRXpQYXlHOHJhc0puc3psT0hVOEtqT2ppOUtjN0F4WXBhODh4MDIKdTN4RjNUMkhZMkExeS9NdmJpeFpwNG12YWZiTFJOOUFqUWY3QXJxNEQ5cThlS0ZhY3EwYndFaVlTK3daZG9kNQp0RDM5MktweVNPZDBFMld4dHozNTRsZmI0cEIzK2k5Z21Ma3lUbUhHZWJzMWdBUmV1TENQTW5OQThTZTdHVGNkCjA0ZlltR3pmNjVySDNqM1d2MzQ0dy81M2dtRWdrOXg0TGswPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== + key: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcFFJQkFBS0NBUUVBNGlkenp0WGNkb0N3bVg2b0d5K3l6UWNhbmJlWmZnbEFMWWtWS1RRWGthejJZbzBZCnV3cGRLVWFKNkRuRVRNVWd4ZjBvdFRYQ0FJT0czVzJJNnJHNzVRcGxVZlM0NXM1ZDgxUTlYaExGb05rSjVlaXMKUW1paUJJRFA4L2pDZEdKTThnaGlrUDVuTXFDSFF0aTVuRHcya0g0K2JkM0hXODJZMVZaSDljcCt1SkNrbnVyUwovbDArcVJSTGd1ZlRjRzRVTmJoYTBadlo1RzAxK1N2cnFVcG52ZVpBR2VxOU5paWFReGZKQWRxN0hIcysycUM4CmtEczRSWU5UUk1NQythdW54NXczWDh4RTJHSjFGR2NKM2ZGSzNMWGw2dFdpYUFqNUc2OWhKemg0bkczN0RXL1oKMEF0Ui8zcHZScHYrSTVYK1NRZlRUcGpDMEZuV3pnUmtnaG5xZXdJREFRQUJBb0lCQVFDWUFySzUzVkFocXlDSgpHL1E4eWRQaU1oczIxZGpyT2FhVXRPYXZXbDlaUUt3ZjAvMUNnNVhaRDV2VXB6ZUY3cDYzMWhGTnRFT2hlc2JsCkFTSWR0cmU0SFVPN1VjWVRCYlZxd0QyN2hOeW40QnJpR1lIbjVWSzV1aWVOTXJEcDc4VU9qb3BLTVdZR1JwYUUKWFE1dHNKOXdnaHJPV0ZzUEh1UFN5ZnIyZ0ZTckV2TEdYRUQxUVBFdUhqcllZeDBMcVJDVSt4NUhvajBkLzZyYgpOMEJjako5SHVwMDZLejN4QlBxemgyeHp1Q2kyYjdGc01vQndkTGcyNGM3bHJsZCtLb09ZdFczLzJSZUJPdXpHCk1HWjFPZlJJcERjdk9xNXlBWXZuYjcya0xJV3E2ZUxCVkx2ekFJZnBLRFBtbmVkd0p2dXVpcUZldktzQVY5eXAKSlo1NjdXeXBBb0dCQVBPbHZvT3pxRUwrN0c4RUM5TW9Vc0lOWVRMMXdYU3VUTzVvczlpc0doRzJHNlprajRWWgpPNlY4Qm1ET2tYT0NtRzNpTWY4M0NSVGs3Um5MVExiRVJJRkxzNFpnend0N2trVHVXSnlzTkRUeGZuMmFVeUlwCm9iNWFvWVozcHNiTXBIbmxNSGN2R1l4SXFVK0JzQVpnbGtOYTdSQVVwTnN5bkJWYzNpZEV5R0hmQW9HQkFPMmUKcWx1dlI4b096VC9jbzVCS1pWS05jQ2V2bHhJNmVXd1NrRDJaaGNuZU1aWFcyY3Z5NERzUWFHQWNKelJsaWpvNAo0RXFsN2VQM0VYbVZYNEtQWXlkSFpZWGZubEkwWVdrSHloR015anBueWFYUWZ2c3AyU1BpM0lKQTZCZ2htbThKCkxyMWFTcmtLaXE3bis3dmtVYitESktBSk0wZXoyc1dzTkNMMC9XTGxBb0dCQUpkcU1YTjNldUhudXRkakZGWXQKZ1FESGY5aERrZTRKUkJZRlMzOGp0Uys4bElKYmpEVzZ0cTZvM08zY2NkZnZHUHR3enRGa1NtaUp2QytEZ0RFMAoxNzNpWmJibEFzYUlET1o1bU9nRXZJMEtaeWwzZHFLTWJNLzNVdHBXRVhjS1JremFlYndYc1REVkZ5TXAzVktaClE4aW9BUnMxT1I1ZjNWQUpYcVhZd1E3UkFvR0Fid0ZvWkZ5R0ZRYkZLOGhQUU9FQVpJaGVsS3VhejVFeG1DTXoKN3hNQlJVVGZ0VGdobHYxbmN6QS9FbWNVaVkzRi9WMEVxdHJKUDIzMFkvQThKaW9HRUJ0eWVnLzFUa0hhSDg3Ygp2MGNlVWhxYVFUUWRuZ2Yyd0tVQ2puYno5aEg4cTFLRzJ6NkxHZGFxNHZyTXh3SHFqcVVkUHdZTlJybm13ZUdvCm1Zd0pzMkVDZ1lFQXROSHI2TEx6VkVIaWU3MzhZV0doYnRSUGRPWGdFMStqVTdhZGZ4RXJtckVLL0ZZdkl5dmoKVWZ1WHlrZ3ZBeWF1Q2tiS2RsRFZkd1d5K2NuTXNCSkFSbFY5a1BGK2xwaTVweUpBL2J6blF4VVJHYmZ1UHNkUQpHZmRjUUhyMjVjR0xxN3E2Z0xaRUJxS0JpMmFZc3BkcmtNQTFIY0txNDhmVUNtNmZyMGVyelVzPQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo= \ No newline at end of file diff --git a/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/mtls-negative-other-ca.yaml b/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/mtls-negative-other-ca.yaml new file mode 100644 index 00000000..6ae6ceac --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/mtls-negative-other-ca.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Secret +metadata: + name: mtls-negative-other-ca + namespace: kyma-system +type: Opaque +data: + {{- $files := .Files }} + crt: {{ $files.Get "certs/negative/client.crt" | b64enc }} + key: {{ $files.Get "certs/negative/client.key" | b64enc }} diff --git a/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/mtls-oauth-nagative-incorrect-clientid.yaml b/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/mtls-oauth-nagative-incorrect-clientid.yaml new file mode 100644 index 00000000..cce16c4f --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/mtls-oauth-nagative-incorrect-clientid.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Secret +metadata: + name: mtls-oauth-negative-incorrect-clientid + namespace: kyma-system +type: Opaque +data: + {{- $files := .Files }} + crt: {{ $files.Get "certs/positive/client.crt" | b64enc }} + key: {{ $files.Get "certs/positive/client.key" | b64enc }} + clientId: {{ "incorrect" | b64enc }} diff --git a/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/mtls-oauth-nagative-other-ca.yaml b/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/mtls-oauth-nagative-other-ca.yaml new file mode 100644 index 00000000..3bbb0851 --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/mtls-oauth-nagative-other-ca.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Secret +metadata: + name: mtls-oauth-negative-other-ca + namespace: kyma-system +type: Opaque +data: + {{- $files := .Files }} + crt: {{ $files.Get "certs/invalid-ca/client.crt" | b64enc }} + key: {{ $files.Get "certs/invalid-ca/client.key" | b64enc }} + clientId: {{ "clientID" | b64enc }} diff --git a/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/mtls-oauth-negative-case.yaml b/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/mtls-oauth-negative-case.yaml new file mode 100644 index 00000000..02377d6e --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/mtls-oauth-negative-case.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Secret +metadata: + name: mtls-oauth-negative-case + namespace: kyma-system +type: Opaque +data: + {{- $files := .Files }} + crt: {{ $files.Get "certs/positive/client.crt" | b64enc }} + key: {{ $files.Get "certs/positive/client.key" | b64enc }} + clientId: {{ "clientID" | b64enc }} \ No newline at end of file diff --git a/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/mtls-oauth-negative-expired-client-cert.yaml b/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/mtls-oauth-negative-expired-client-cert.yaml new file mode 100644 index 00000000..743fd941 --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/mtls-oauth-negative-expired-client-cert.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Secret +metadata: + name: mtls-oauth-negative-expired-client-cert + namespace: kyma-system +type: Opaque +data: + crt: bm90QmVmb3JlPUF1ZyAgMSAwMDowMDowMCAyMDIyIEdNVApub3RBZnRlcj1BdWcgIDIgMDA6MDA6MDAgMjAyMiBHTVQKLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURkakNDQWw2Z0F3SUJBZ0lVZldyTzlHRG1rR0FCRktMdzYvL0tVcDdYMnE0d0RRWUpLb1pJaHZjTkFRRUwKQlFBd1dURUxNQWtHQTFVRUJoTUNVRXd4Q2pBSUJnTlZCQWdNQVVFeEREQUtCZ05WQkFvTUExTkJVREV3TUM0RwpBMVVFQXd3bmJXOWpheTFoY0hCc2FXTmhkR2x2Ymk1MFpYTjBMbk4yWXk1amJIVnpkR1Z5TG14dlkyRnNNQjRYCkRUSXlNRGd3TVRBd01EQXdNRm9YRFRJeU1EZ3dNakF3TURBd01Gb3dXVEVMTUFrR0ExVUVCaE1DVUV3eENqQUkKQmdOVkJBZ01BVUV4RERBS0JnTlZCQW9NQTFOQlVERXdNQzRHQTFVRUF3d25iVzlqYXkxaGNIQnNhV05oZEdsdgpiaTUwWlhOMExuTjJZeTVqYkhWemRHVnlMbXh2WTJGc01JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBCk1JSUJDZ0tDQVFFQTZSMU1VeVRTT2FyNGtPTUdMV1BWZUU4WXkwRTRlcGV3NS9aL2ZZNlQ5d1BWYUdSTE9RSEYKLzhCVjVEeHhGLzN0K0Y0S21tSHJ6TjlyUDN5T3VnVnY4bEhlek9VeFhmZWFrN0hMR2VGV3k5TGxmc3BtODhJMwpxR3BHM2FaUUhZS0VGRk1IUDREcGdkM3JUUVlhNVFnMDRNZDZuZjk1SFY1YzJtTlErb2pqUWt0cWROSUpPb2p5Clk4WjdlaWMyRURtMWxqYkdCTFZESEY1aUY3QjQ1bXp0OTgzZElaeC81TW9OMnpjdlF5OU9GMlkvcm1EL284QkQKd3llR2xvejVqTHNGRUJqbWdBSWFrSXBiVkZqekk4Tlp3cW0wOTZSM2JuZCtJdml3eS96YkRxYlNxQ2Qvdi9qRApIT0x2M0pIRXNLL0VxNng0VGZhL1lqTFozbjRrNmhCVHVRSURBUUFCb3pZd05EQXlCZ05WSFJFRUt6QXBnaWR0CmIyTnJMV0Z3Y0d4cFkyRjBhVzl1TG5SbGMzUXVjM1pqTG1Oc2RYTjBaWEl1Ykc5allXd3dEUVlKS29aSWh2Y04KQVFFTEJRQURnZ0VCQUxpMVI2MjhpOXZXWkkrNnJKZHN4MEhtZUZLUGNVenV3R1ptU2t2TC9rTHliOUE3b2V6SQpPbkRLL0EzOFpGcmpzTE9YQWRJZkxXS0laMkh5b21FNG9HMldNL3phN1BCWDc4dXpycWw5bFR2eWtQOFRkdm5qCkpTN2l4cWZucW1UNDdLVHFOSjRleXMzd2NOU29kTHlydWNDUWlLeUllMEFUQ3RUSUY2WnNGR3Q2WkN0WS84czYKQTJ0Rk92dmhTWm01UytsZGVEQ3FMajJMOW1oZEY0RzlMRmJ5Q2pTbVNDWEFSbDVpanBjb2pEWEVDRTdobTk5YwpnbU1scjE2Uzh1Nm5mWEVRdXFwTys4MU4xTEVpTzVyUzRwUGxBdGgyVk5YOXc0WHBmbEM1VDA0bzA5S21HYzhLCjhVTjB1YXJpRGx2NW93SEZlWVRsZkZ6eGhUUDJBOWFPL3U4PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== + key: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBNlIxTVV5VFNPYXI0a09NR0xXUFZlRThZeTBFNGVwZXc1L1ovZlk2VDl3UFZhR1JMCk9RSEYvOEJWNUR4eEYvM3QrRjRLbW1IcnpOOXJQM3lPdWdWdjhsSGV6T1V4WGZlYWs3SExHZUZXeTlMbGZzcG0KODhJM3FHcEczYVpRSFlLRUZGTUhQNERwZ2QzclRRWWE1UWcwNE1kNm5mOTVIVjVjMm1OUStvampRa3RxZE5JSgpPb2p5WThaN2VpYzJFRG0xbGpiR0JMVkRIRjVpRjdCNDVtenQ5ODNkSVp4LzVNb04yemN2UXk5T0YyWS9ybUQvCm84QkR3eWVHbG96NWpMc0ZFQmptZ0FJYWtJcGJWRmp6SThOWndxbTA5NlIzYm5kK0l2aXd5L3piRHFiU3FDZC8Kdi9qREhPTHYzSkhFc0svRXE2eDRUZmEvWWpMWjNuNGs2aEJUdVFJREFRQUJBb0lCQUhWalJZNFEyclFqZm13bgpobkxROVN4U1dGL3lCZWpsL2pXeEVWNCtzQkFScENPZmJhblZWTW1ISnpsNW5sSEFrMWNndENJdDhUb0h2OUFHCmZ6RDVqL2ZzZGsramtvcUpKeFA4MGhQRVA1c0FKb1VFazNkb2MvS2hJZkozejV3c255cEU3VDl6UVNNZWgyRVEKRS9jRmZPczhTR2pMdjBla3Z3bFNQZk1MZjdWZm9vK2h6K2krUW5jMXFRR2lPVW90Tk11WmoxcU85Mm5Ib2toMApxcWRRWlFOa1pUQm1sd2Y0bGNpeVJsTVUwRHJJNmd1bDNRazNpd2hueU1Kb2h4dVJxT3RSNzJjTS9SVFpsMHdWClh4WmJ5VkM1R2dhRUR0aUtFaE9xdmpPN2N4WU9BU3U3aGVDbWRnRFdMMGFSVjE1ai9Sb0twRUtkeWt0SHFrRjQKcWVLZndBa0NnWUVBL2pKaHBqSWh5N09Vajh1cW1scHE4L0tyRG5zUG1FK0VqbzFxOWdiTDByRVZSaktMNHpLaAp2SDdrdHJjQUkrMkpaS0hlSDVmVUJSMVYxK3R5WXM5OHUwOFpHSmQ0TFdjNjFFUTQvbEtFVUhaUU41TnpJOHprCmhWbjBhNWZOQjlLMDQ4aU1XQTh6ZlgyQ2JvWWltT0V3ZC9BZUtwdzZjN0E0UXU5RVRGckNhMGNDZ1lFQTZzU2gKb2V4U0pxbE9RMjk1VjlVWWtNTkkrNkR1ZGNoSUZkamY2bUp2NEo1aGhQSmJDY1doZkZjdnZmNkQ0SzBDei8rVwpybmhreWNlU1dDRHNuN3laN1VBbHAwVXp2bk1IYXBqUXUwN3JpcGI0UGExdlE2MkxRYWhTWnFJZmkxeFVqQ1NmCm1GcDJZSXVRV2FWOWFlYUdmSnY0anRnZUJ6VUxFYngwMGdpb3lQOENnWUVBOUR5Q09JWm9sR2xxYjdOWHExRCsKL0grSVBiU2Q2bEZVNHdjYjQySHFTdmtjb01NR1Iza3BqNHc0d3hvWUIyMC9Hckt3VXBpMS9XZ1BTQlFRWnNKSApiVTExcG53NjJ4MFptRVFvb3F1ME4vOUYyZkJScSs4OURxZTh3ZmdyNXIxY1VwUXB6SjVtY2NlN0graS9xemFMCk5HSkJDZDNzQjZZa21LTitjd0t0VlJjQ2dZQkRKaFM1RkxmMm1PeHF1Mkt3cmFIR0hpVXMyNzM0OEYwMTZuODUKTWdpZjdZMGxFcERaZmE2UHV2eEwwcFZ6Mk9oNkI3ZllsVlQycGQrRTEzMzJ2bUlraXZsNkc0QU9WQ1psNWVtbAorWS9EWnlUL3R6Q2c0ZTEzelNZc2R1aWcycnJRRHRXYkpSekF4b3AyS2JCeWJ0NCttL24vR1crVlRpV3BZQWJsCjRGWXVqd0tCZ1FDT3VUenVZS1QrVHZuU3hDVWdZbUN3SWs5MlVxUHJCS3J5UE1sekxMdFBaMlF3UjNpRGlWTzEKUlpxNGtkc1NzU05SNUJKUzlrVW92dUpubG1hb0ZBM2NLZXhRdk9mcmFVdXNGcXVHb3NWR3hrSGo3S24ydnFadQppK0NDNTZPNHRRMnNhOXZ6MFNpTWZoMzdFRlh6eUFVODNpWGY2aTB0MGlzb0cvYmFpTlNDZ2c9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo= + clientId: {{ "clientID" | b64enc }} \ No newline at end of file diff --git a/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/mtls-oauth-negative-expired-server-cert.yaml b/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/mtls-oauth-negative-expired-server-cert.yaml new file mode 100644 index 00000000..2a536df0 --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/mtls-oauth-negative-expired-server-cert.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Secret +metadata: + name: mtls-oauth-negative-expired-server-cert + namespace: kyma-system +type: Opaque +data: + crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUQwRENDQXJpZ0F3SUJBZ0lVSkV2L3RtUndLOXRzdWhpQVlaT2Q4NjN1UHlBd0RRWUpLb1pJaHZjTkFRRUwKQlFBd1pqRUxNQWtHQTFVRUJoTUNVRXd4Q2pBSUJnTlZCQWdNQVVFeEREQUtCZ05WQkFvTUExTkJVREU5TURzRwpBMVVFQXd3MGJXOWpheTFoY0hCc2FXTmhkR2x2Ymk1MFpYTjBMbk4yWXk1amJIVnpkR1Z5TG14dlkyRnNkR1Z6CmRDMXZkR2hsY2kxallUQWVGdzB5TWpBeE1ERXdPVEV3TVRCYUZ3MHlNakF4TURJd09URXdNVEJhTUdZeEN6QUoKQmdOVkJBWVRBbEJNTVFvd0NBWURWUVFJREFGQk1Rd3dDZ1lEVlFRS0RBTlRRVkF4UFRBN0JnTlZCQU1NTkcxdgpZMnN0WVhCd2JHbGpZWFJwYjI0dWRHVnpkQzV6ZG1NdVkyeDFjM1JsY2k1c2IyTmhiSFJsYzNRdGIzUm9aWEl0ClkyRXdnZ0VpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElCRHdBd2dnRUtBb0lCQVFERzFDaHE2WmI3eUdaS0ZwUmgKV01sUDh6L0NtZEFZbktpRkxKNVhpMFlXakpZZXlUSUdXRWM4bm5SK1VOalZKejZjN0w5cCs4bFEvK1ZGZUpmRwpmYTM4WnhHQVZxNitpcXdJOVpwSTd2SlVZRnd0SDJLQXA5cDdZRHdOVllUbXZEb3lyTFh3L1c3c1p1Q2c3QWVBCkh5MGlBcGtBQkk1Tmg4dWFMNkk3eE8xK0dCSVhNYVkzNlFDZXpvejdvbkoyUURBVjlybVEzQndpZUtwMkNUV1QKS3ZHaVBac04rTlVLbndjdVoxbThpUHQxYmozaGsvS3p1MGxvR2RVaXJiOFFUNkFzRnJ2ZzdGNFBYVmFsb1FSNwpJMy9paWpxZFBERmlEOHVwOXltdE9ja2tjQlorSmhqVDI4OTZxcFIzQVJVaGl0dXhreENpMUZHeEd1YlJNREFpCitmcExBZ01CQUFHamRqQjBNRElHQTFVZEVRUXJNQ21DSjIxdlkyc3RZWEJ3YkdsallYUnBiMjR1ZEdWemRDNXoKZG1NdVkyeDFjM1JsY2k1c2IyTmhiREFkQmdOVkhRNEVGZ1FVY25wWGg2b2xoei9tV01LTE1sVTR1Z0gxRUpNdwpId1lEVlIwakJCZ3dGb0FVMzRJa3V5V2NoVlN2ZXlNQXZmdDZOdE1JZkdVd0RRWUpLb1pJaHZjTkFRRUxCUUFECmdnRUJBQmNvYmVNSmkrQVZ1MGZtY0szYzM1RXhQU0pZTWlOYms1VXU0QmgzUlloY0tJeFFyUjY4T1FYT3ZzMEkKVVNaYmRkRjhrQU1KOFpwTGJWQmJnZ2NmUktrVUxPYko3TklyTEF2UmR6cVVzeThMc0VMZUM5cVlVM0E0TUFEWApHWU8zZjlWbnFIelpmRFNMTjFzOGoyK1JQU0hGL3lZbGxETWhCbkVJa1NwdXdWbXprMGFoZEtXRmthcG1xbEZMCkRSZ2JsTThMMVZTbGpXb3hGYUdLNUZFQ2dGZzJIcHJDc2o3SHl0TFR0KzEwc1F5ZU9vVktUVU9lNjhWeFlzR2kKbXd3bnhzTGtHdkdGWTNaVU9JQk5BY0ovdlJLSER4R1pPS29QblIrRmlodkJvZkR1RlBzbnRSTnBIRncrNVJsUQpjL1lUdThmQ1U4QnRpeGwvYkRxdTZUWmtyazg9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0= + key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2Z0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktnd2dnU2tBZ0VBQW9JQkFRREcxQ2hxNlpiN3lHWksKRnBSaFdNbFA4ei9DbWRBWW5LaUZMSjVYaTBZV2pKWWV5VElHV0VjOG5uUitVTmpWSno2YzdMOXArOGxRLytWRgplSmZHZmEzOFp4R0FWcTYraXF3STlacEk3dkpVWUZ3dEgyS0FwOXA3WUR3TlZZVG12RG95ckxYdy9XN3NadUNnCjdBZUFIeTBpQXBrQUJJNU5oOHVhTDZJN3hPMStHQklYTWFZMzZRQ2V6b3o3b25KMlFEQVY5cm1RM0J3aWVLcDIKQ1RXVEt2R2lQWnNOK05VS253Y3VaMW04aVB0MWJqM2hrL0t6dTBsb0dkVWlyYjhRVDZBc0Zydmc3RjRQWFZhbApvUVI3STMvaWlqcWRQREZpRDh1cDl5bXRPY2trY0JaK0poalQyODk2cXBSM0FSVWhpdHV4a3hDaTFGR3hHdWJSCk1EQWkrZnBMQWdNQkFBRUNnZ0VBQlgrem1IVmFZQjlFT1BOVDZqZE81ZitudVVXUXZFV0U0WjRBeVJJSWY3SW0KcXJaTXhHRW5velVNcXJ1b3E0aDQwbFUzM0FJREtOTFM3KzlzWHloMXFkL2QyNHRLTE9uZjVTV0p2VStpY3hQeApLS3hRQ0pmYjBvS3dWbndSZjJJZ1IrdC80cWpYcXdFVFlFLzJ5eVBSbHptMEtveDF0UTQyNHM1RGNkeTU1cjFPCklNbEhPcXhFcE84YWtiY2ZacHE4cVdqMUFkU2VCRk5WdXJQaUxmQjMxN2pQd25RSkp2MU9NWWxsQXJWa1NDcisKNDlMcmk1QjRBL0UrQkh6SnhCQ1lyaC9IRCtCVlhxY3JDSDBKNEQ3TWEwVWJWMER5Vml5UWtwK2c5MGhoQ21HMwpnYVpKUmdkczJnQzI2cGRlR0E1and2Tjd3ZUYveUtsRVEwZXRib0NHS1FLQmdRREtJYUN3czVHbVZUYlRlaDJ0CnRFWmY1MWZwNVdRL1R2V1VJaWtsdHBXU1RRUHQwQWVtZFJxL1FQak5OS3YycnR0dDZIT0RHcW9iSnFVTENjaGYKbEU1cmc0N09EYnV5OHVPWmtVOEYyMTRjZDlSbnU3alNzcFd3ZkEwakpndFhiblZ2aVlXeUtjam90QVQwTXhQcgpON1VqV2dva20yQWg3amRMU2hvMFF4bkkwd0tCZ1FENzBUVzNCa2llYUdzN3IzY3hhUytvd1VoaUdMTnEyajN4Ckd2QmtXVmx5TzM5aHlCcm5aUk92ak9HRFE4eCtxSmRTN2V6Q2xGVmpsanc2V2dra1V6U1ZQSHkrb094RjE5T3UKM00xalhCZC9PMUh6VU1WUkcvYmJxaTJZeThhb0gyRWplbzE4VE9XbWhYUnFNb045TDV4Q3RkbDRnMGxMNnBJZgpPb3FlanltZHFRS0JnUURCZjZPbXhLQS96UCtwUHhPK1AvL0d1MTZycUU5cFU1dEFiZHRhSVFuYWZpT3V1eUUzCnRvOGVXNEpTWDRQbnFNaWkxSTRRQ2F5aVJVSmw2TDJLMGh5b1M4NmZid0lxY3Q1ekdtbTl2NXkrUC9CMFJYN1AKSk9xcmduWEpHaGh0WUc3SGthME5PM2I3WGFvSVpBVkRmWmJIK3VBTzN6Y09CRSttb1krb1REd1l4UUtCZ1FEcgo4TWpRZFEzRGhuaTYwcHZ1YXV6aHhEK3EwaFFCa1B5cWxLQWFsZkVON0J0ZEpkMjNZMmcvZXRPdFp2QUsyTEg0ClhMOFNUV040VE1LZnRjNk0vM3pzTzJGeVIxczUwWkFnYmZmdkdkRldQK0Y0QmZ6ckV6V0grZnFCQ0tWWXp4WDMKNVJMK0hScXJuSzFIOTQ1bDFCOG9EalQyQ3FTNWdjNXBmak4xZnhQeUNRS0JnQzVJR2FnbldPSG9XVEpnUG95YQptY1QrMk1NUGdJMzhaL2xzM0lmd1dIc2hUbE5sQ2k2RklnMkQrUUYrZk80ZGdYZS85K0dESERvM3J5MDU2Z2VhCng4T1FYZ2dxa0lJdkxDUnZGVlVId0M3Z0MyekpHakJ1V2dYUDhsYjRDNG4yemlQd1c5M1c2RkpXMnpwMzRacSsKU29JaUNXUEh6RXF3WjgxbEJwZ1VkVTZYCi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K + clientId: {{ "clientID" | b64enc }} \ No newline at end of file diff --git a/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/mtls-oauth-positive.yaml b/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/mtls-oauth-positive.yaml new file mode 100644 index 00000000..1d83b2b2 --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/mtls-oauth-positive.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Secret +metadata: + name: mtls-oauth-positive + namespace: kyma-system +type: Opaque +data: + {{- $files := .Files }} + crt: {{ $files.Get "certs/positive/client.crt" | b64enc }} + key: {{ $files.Get "certs/positive/client.key" | b64enc }} + clientId: {{ "clientID" | b64enc }} + diff --git a/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/mtls-positive.yaml b/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/mtls-positive.yaml new file mode 100644 index 00000000..f5540a3e --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/mtls-positive.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Secret +metadata: + name: mtls-positive + namespace: kyma-system +type: Opaque +data: + {{- $files := .Files }} + crt: {{ $files.Get "certs/positive/client.crt" | b64enc }} + key: {{ $files.Get "certs/positive/client.key" | b64enc }} diff --git a/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/oauth-negative-incorrect-id.yaml b/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/oauth-negative-incorrect-id.yaml new file mode 100644 index 00000000..94ffed21 --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/oauth-negative-incorrect-id.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: wrong-oauth-test + namespace: kyma-system +type: Opaque +data: + clientId: {{ "bad id" | b64enc }} + clientSecret: {{ "bad secret" | b64enc }} diff --git a/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/oauth-negative-invalid-token.yaml b/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/oauth-negative-invalid-token.yaml new file mode 100644 index 00000000..13d0df23 --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/oauth-negative-invalid-token.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: oauth-test-negative-case + namespace: kyma-system +type: Opaque +data: + clientId: {{ "clientID" | b64enc }} + clientSecret: {{ "clientSecret" | b64enc }} diff --git a/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/oauth-positive.yaml b/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/oauth-positive.yaml new file mode 100644 index 00000000..af54df50 --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/oauth-positive.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: oauth-test + namespace: kyma-system +type: Opaque +data: + clientId: {{ "clientID" | b64enc }} + clientSecret: {{ "clientSecret" | b64enc }} diff --git a/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/redirect-basic-auth.yml b/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/redirect-basic-auth.yml new file mode 100644 index 00000000..e6405b0d --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/redirect-basic-auth.yml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: redirect-basic + namespace: kyma-system +type: Opaque +data: + password: {{ "passwd" | b64enc }} + username: {{ "user" | b64enc }} diff --git a/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/request-parameters-negative.yaml b/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/request-parameters-negative.yaml new file mode 100644 index 00000000..f62520b2 --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/request-parameters-negative.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Secret +metadata: + name: wrong-request-parameters-test + namespace: kyma-system +type: Opaque +stringData: + headers: |- + {"Hkey1":["Wrong-value"],"Wrong-key":["Hval22"]} + queryParameters: |- + {"Wrong-key":["Qval1"],"Qkey2":["Qval21","Qval22","Additional-value"]} \ No newline at end of file diff --git a/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/request-parameters.yaml b/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/request-parameters.yaml new file mode 100644 index 00000000..847ac1ee --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/templates/applications/credentials/request-parameters.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Secret +metadata: + name: request-parameters-test + namespace: kyma-system +type: Opaque +stringData: + headers: |- + {"Hkey1":["Hval1"],"Hkey2":["Hval21","Hval22"]} + queryParameters: |- + {"Qkey1":["Qval1"],"Qkey2":["Qval21","Qval22"]} \ No newline at end of file diff --git a/tests/resources/charts/gateway-test/charts/test/templates/applications/manual.yml b/tests/resources/charts/gateway-test/charts/test/templates/applications/manual.yml new file mode 100644 index 00000000..8a174203 --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/templates/applications/manual.yml @@ -0,0 +1,23 @@ +apiVersion: applicationconnector.kyma-project.io/v1alpha1 +kind: Application +metadata: + name: complex-cases + namespace: "{{ .Values.global.namespace }}" +spec: + description: Endpoints for complex tests + skipVerify: true + labels: + app: complex-cases + services: + - displayName: oauth-expired-token-renewal + name: oauth-expired-token-renewal + providerDisplayName: Kyma + description: Should renew the OAuth token after the expiration time + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/oauth/ok" + credentials: + secretName: oauth-test + authenticationUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/oauth/token?token_lifetime=5s" + type: OAuth diff --git a/tests/resources/charts/gateway-test/charts/test/templates/applications/methods-with-body.yml b/tests/resources/charts/gateway-test/charts/test/templates/applications/methods-with-body.yml new file mode 100644 index 00000000..1c8a7265 --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/templates/applications/methods-with-body.yml @@ -0,0 +1,40 @@ +apiVersion: applicationconnector.kyma-project.io/v1alpha1 +kind: Application +metadata: + name: methods-with-body + namespace: "{{ .Values.global.namespace }}" +spec: + description: |- + Verify if methods, specified by `descritpion`, + are correctly forwarded, including their body + skipVerify: true + labels: + app: methods-with-body + services: + - displayName: post + name: post + providerDisplayName: post + description: POST + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/unsecure/echo" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/methods-with-body/post" + - displayName: delete + name: delete + providerDisplayName: delete + description: DELETE + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/unsecure/echo" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/methods-with-body/delete" + - displayName: put + name: put + providerDisplayName: put + description: PUT + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/unsecure/echo" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/methods-with-body/put" diff --git a/tests/resources/charts/gateway-test/charts/test/templates/applications/missing-resources-error-handling.yml b/tests/resources/charts/gateway-test/charts/test/templates/applications/missing-resources-error-handling.yml new file mode 100644 index 00000000..0b87dc65 --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/templates/applications/missing-resources-error-handling.yml @@ -0,0 +1,101 @@ +apiVersion: applicationconnector.kyma-project.io/v1alpha1 +kind: Application +metadata: + name: missing-resources-error-handling + namespace: "{{ .Values.global.namespace }}" +spec: + description: Missing resources + skipVerify: true + labels: + app: missing-resources-error-handling + services: + - displayName: application-doesnt-exist + name: application-doesnt-exist + providerDisplayName: Kyma + description: Should return 404 when application doesn't exist + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/unsecure/ok" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/where-is-app/idk" + - displayName: service-doesnt-exist + name: service-doesnt-exist + providerDisplayName: Kyma + description: Should return 404 when service doesn't exist + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/unsecure/ok" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/missing-resources-error-handling/where-is-service" + - displayName: missing-secret-oauth + name: missing-secret-oauth + providerDisplayName: Kyma + description: Should return 500 when secret containing OAuth credentials is missing in the cluster + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/oauth/ok" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/missing-resources-error-handling/missing-secret-oauth" + credentials: + secretName: where-is-the-secret + authenticationUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/server/oauth/token?client_secret=clientSecret" + type: OAuth + - displayName: missing-secret-basic-auth + name: missing-secret-basic-auth + providerDisplayName: Kyma + description: Should return 500 when secret containing Basic Auth credentials is missing in the cluster + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/basic/ok" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/missing-resources-error-handling/missing-secret-basic-auth" + credentials: + secretName: where-is-the-secret + type: Basic + - displayName: missing-secret-oauth-mtls + name: missing-secret-oauth-mtls + providerDisplayName: Kyma + description: Should return 500 when secret containing OAuth mTLS credentials is missing in the cluster + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/basic/ok" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/missing-resources-error-handling/missing-secret-oauth-mtls" + credentials: + secretName: where-is-the-secret + authenticationUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/server/oauth/token?client_secret=clientSecret" + type: OAuthWithCert + - displayName: missing-secret-certgen-mtls + name: missing-secret-certgen-mtls + providerDisplayName: Kyma + description: Should return 500 when secret containing Cert Gen mTLS credentials is missing in the cluster + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/basic/ok" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/missing-resources-error-handling/missing-secret-certgen-mtls" + credentials: + secretName: where-is-the-secret + type: CertificateGen + - displayName: missing-request-parameters-header + name: missing-request-parameters-header + providerDisplayName: Kyma + description: Should return 500 when secret and request parameters credentials is missing in the cluster + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/basic/ok" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/missing-resources-error-handling/missing-request-parameters-header" + requestParametersSecretName: where-are-the-paramterers + credentials: + secretName: basic-test + type: Basic + - displayName: non-existing-target-url + name: non-existing-target-url + providerDisplayName: Kyma + description: Should return 502 when target url is not resolvable + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "http://bad.bad.svc.cluster.local:8080/v1/api/unsecure/ok" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/missing-resources-error-handling/non-existing-target-url" diff --git a/tests/resources/charts/gateway-test/charts/test/templates/applications/negative-authorisation.yml b/tests/resources/charts/gateway-test/charts/test/templates/applications/negative-authorisation.yml new file mode 100644 index 00000000..79329289 --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/templates/applications/negative-authorisation.yml @@ -0,0 +1,254 @@ +apiVersion: applicationconnector.kyma-project.io/v1alpha1 +kind: Application +metadata: + name: negative-authorisation + namespace: {{ .Values.global.namespace }} +spec: + description: Negative authorisation + skipVerify: true + labels: + app: negative-authorisation + services: + - displayName: bad oauth token + name: bad-oauth-token + providerDisplayName: OAuth + description: Should return 401 for OAuth with a wrong token + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/oauth/ok" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/negative-authorisation/bad-oauth-token" + credentials: + secretName: oauth-test-negative-case + authenticationUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/oauth/bad-token" + type: OAuth + - displayName: wrong oauth secret + name: wrong-oauth-secret + providerDisplayName: OAuth + description: Should return 502 for OAuth with a wrong secret + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/oauth/ok" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/negative-authorisation/wrong-oauth-secret" + credentials: + secretName: wrong-oauth-test + authenticationUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/oauth/token" + type: OAuth + - displayName: mtls-oauth-other-ca + name: mtls-oauth-other-ca + providerDisplayName: mTLS-OAuth + description: Should return 500 for mTLS Oauth with client certificate generated from other CA + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/oauth/ok" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/negative-authorisation/mtls-oauth-other-ca" + credentials: + secretName: mtls-oauth-negative-other-ca + authenticationUrl: "https://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8090/v1/api/mtls-oauth/token" + type: OAuthWithCert + - displayName: mtls-oauth-incorrect-clientid + name: mtls-oauth-incorrect-clientid + providerDisplayName: mTLS-OAuth + description: Should return 500 for mTLS Oauth with valid certificate but invalid client id + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/oauth/ok" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/negative-authorisation/mtls-oauth-incorrect-clientid" + credentials: + secretName: mtls-oauth-negative-incorrect-clientid + authenticationUrl: "https://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8090/v1/api/mtls-oauth/token" + type: OAuthWithCert + - displayName: mtls-oauth-negative-expired-client-cert + name: mtls-oauth-negative-expired-client-cert + providerDisplayName: mTLS-OAuth + description: Should return 500 for mTLS Oauth with expired client certificate + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/oauth/ok" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/negative-authorisation/mtls-oauth-negative-expired-client-cert" + credentials: + secretName: mtls-oauth-negative-expired-client-cert + authenticationUrl: "https://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8090/v1/api/mtls-oauth/token" + type: OAuthWithCert + - displayName: mtls-oauth-negative-expired-server-cert + name: mtls-oauth-negative-expired-server-cert + providerDisplayName: mTLS-OAuth + description: Should return 500 for mTLS Oauth with expired server certificate + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/oauth/ok" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/negative-authorisation/mtls-oauth-negative-expired-server-cert" + credentials: + secretName: mtls-oauth-negative-expired-server-cert + authenticationUrl: "https://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8091/v1/api/mtls-oauth/token" + type: OAuthWithCert + - displayName: mtls-negative-other-ca + name: mtls-negative-other-ca + providerDisplayName: mTLS + description: Should return 502 for mTLS with client certificate generated from other CA + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "https://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8090/v1/api/mtls/ok" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/negative-authorisation/mtls-negative-other-ca" + credentials: + secretName: mtls-negative-other-ca + type: CertificateGen + - displayName: mtls-negative-expired-client-cert + name: mtls-negative-expired-client-cert + providerDisplayName: mTLS + description: Should return 502 for mTLS with expired client certificate + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "https://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8090/v1/api/mtls/ok" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/negative-authorisation/mtls-negative-expired-client-cert" + credentials: + secretName: mtls-negative-expired-client-cert + type: CertificateGen + - displayName: mtls-negative-expired-server-cert + name: mtls-negative-expired-server-cert + providerDisplayName: mTLS + description: Should return 502 for mTLS with expired server certificate + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "https://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8091/v1/api/mtls/ok" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/negative-authorisation/mtls-negative-expired-server-cert" + credentials: + secretName: mtls-negative-expired-client-cert + type: CertificateGen + - displayName: bad csrf token basic + name: bad-csrf-token-basic + providerDisplayName: Basic with CSRF + description: Should return 403 for Basic Auth with a bad CSRF token + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/csrf-basic/ok" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/negative-authorisation/bad-csrf-token-basic" + credentials: + secretName: basic-test-negative-case + type: Basic + csrfInfo: + tokenEndpointURL: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/csrf/bad-token" + - displayName: bad csrf endpoint basic + name: bad-csrf-endpoint-basic + providerDisplayName: Basic with CSRF + description: Should return 502 for Basic Auth with a bad CSRF token endpoint + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/csrf-basic/ok" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/negative-authorisation/bad-csrf-endpoint-basic" + credentials: + secretName: basic-test-negative-case + type: Basic + csrfInfo: + tokenEndpointURL: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/csrf/nonexistingpath" + - displayName: bad csrf token oauth + name: bad-csrf-token-oauth + providerDisplayName: OAuth with CSRF + description: Should return 403 for OAuth with a bad CSRF token + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/csrf-oauth/ok" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/negative-authorisation/bad-csrf-token-oauth" + credentials: + secretName: oauth-test-negative-case + authenticationUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/oauth/token" + type: OAuth + csrfInfo: + tokenEndpointURL: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/csrf/bad-token" + - displayName: bad csrf endpoint oauth + name: bad-csrf-endpoint-oauth + providerDisplayName: OAuth with CSRF + description: Should return 502 for OAuth with a bad CSRF token endpoint + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/csrf-oauth/ok" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/negative-authorisation/bad-csrf-endpoint-oauth" + credentials: + secretName: oauth-test-negative-case + authenticationUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/oauth/token" + type: OAuth + csrfInfo: + tokenEndpointURL: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/csrf/nonexistingpath" + - displayName: bad csrf token mtls oauth + name: bad-csrf-token-mtls-oauth + providerDisplayName: mTLS-OAuth with CSRF + description: Should return 403 for mTLS OAuth with a bad CSRF token + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/csrf-oauth/ok" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/negative-authorisation/bad-csrf-token-mtls-oauth" + credentials: + secretName: mtls-oauth-negative-case + authenticationUrl: "https://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8090/v1/api/mtls-oauth/token" + type: OAuthWithCert + csrfInfo: + tokenEndpointURL: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/csrf/bad-token" + - displayName: bad csrf endpoint mtls oauth + name: bad-csrf-endpoint-mtls-oauth + providerDisplayName: mTLS-OAuth with CSRF + description: Should return 502 for mTLS OAuth with a bad CSRF token endpoint + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/csrf-oauth/ok" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/negative-authorisation/bad-csrf-endpoint-mtls-oauth" + credentials: + secretName: mtls-oauth-negative-case + authenticationUrl: "https://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8090/v1/api/mtls-oauth/token" + type: OAuthWithCert + csrfInfo: + tokenEndpointURL: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/csrf/nonexistingpath" + - displayName: bad csrf token mtls + name: bad-csrf-token-mtls + providerDisplayName: mTLS with CSRF + description: Should return 403 for mTLS with a bad CSRF token + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "https://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8090/v1/api/csrf-mtls/ok" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/negative-authorisation/bad-csrf-token-mtls" + credentials: + secretName: mtls-negative-case + type: CertificateGen + csrfInfo: + tokenEndpointURL: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/csrf/bad-token" + - displayName: bad csrf endpoint mtls + name: bad-csrf-endpoint-mtls + providerDisplayName: mTLS with CSRF + description: Should return 502 for mTLS with a bad CSRF token endpoint + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "https://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8090/v1/api/csrf-mtls/ok" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/negative-authorisation/bad-csrf-endpoint-mtls" + credentials: + secretName: mtls-negative-case + type: CertificateGen + csrfInfo: + tokenEndpointURL: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/csrf/nonexistingpath" + - displayName: basic-auth-with-wrong-request-parameters + name: basic-auth-with-wrong-request-parameters + providerDisplayName: Basic + description: Should return 400 when calling endpoint protected with Basic Auth with wrong additional request parameters + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/request-parameters-basic/ok" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/negative-authorisation/basic-auth-with-wrong-request-parameters" + requestParametersSecretName: wrong-request-parameters-test + credentials: + secretName: basic-test + type: Basic diff --git a/tests/resources/charts/gateway-test/charts/test/templates/applications/path-related-error-handling.yml b/tests/resources/charts/gateway-test/charts/test/templates/applications/path-related-error-handling.yml new file mode 100644 index 00000000..acdcca70 --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/templates/applications/path-related-error-handling.yml @@ -0,0 +1,29 @@ +apiVersion: applicationconnector.kyma-project.io/v1alpha1 +kind: Application +metadata: + name: path-related-error-handling + namespace: "{{ .Values.global.namespace }}" +spec: + description: Path handling + skipVerify: true + labels: + app: path-related-error-handling + services: + - displayName: missing-srv-app + name: missing-srv-app + providerDisplayName: Kyma + description: Should return 400 when service and application are missing in the path + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/unsecure/ok" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080" + - displayName: missing-srv + name: missing-srv + providerDisplayName: Kyma + description: Should return 400 when service is missing in the path + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/unsecure/ok" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/path-related-error-handling" \ No newline at end of file diff --git a/tests/resources/charts/gateway-test/charts/test/templates/applications/positive-authorisation.yml b/tests/resources/charts/gateway-test/charts/test/templates/applications/positive-authorisation.yml new file mode 100644 index 00000000..e22ca854 --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/templates/applications/positive-authorisation.yml @@ -0,0 +1,141 @@ +apiVersion: applicationconnector.kyma-project.io/v1alpha1 +kind: Application +metadata: + name: positive-authorisation + namespace: {{ .Values.global.namespace }} +spec: + description: Authorisation + skipVerify: true + labels: + app: positive-authorisation + services: + - displayName: unsecure-always-ok + name: unsecure-always-ok + providerDisplayName: AlwaysOK + description: Should return 200 when calling unprotected endpoint + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/unsecure/ok" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/positive-authorisation/unsecure-always-ok" + - displayName: basic-auth-ok + name: basic-auth-ok + providerDisplayName: Basic + description: Should return 200 when calling endpoint protected with Basic Auth + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/basic/ok" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/positive-authorisation/basic-auth-ok" + credentials: + secretName: basic-test + type: Basic + - displayName: oauth + name: oauth + providerDisplayName: OAuth + description: Should return 200 when calling endpoint protected with OAuth + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/oauth/ok" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/positive-authorisation/oauth" + credentials: + secretName: oauth-test + authenticationUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/oauth/token" + type: OAuth + - displayName: mtls-oauth + name: mtls-oauth + providerDisplayName: mTLS-OAuth + description: Should return 200 when calling endpoint protected with mTLS OAuth + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/oauth/ok" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/positive-authorisation/mtls-oauth" + credentials: + secretName: mtls-oauth-positive + authenticationUrl: "https://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8090/v1/api/mtls-oauth/token" + type: OAuthWithCert + - displayName: mtls + name: mtls + providerDisplayName: mTLS + description: Should return 200 when calling endpoint protected with mTLS + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "https://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8090/v1/api/mtls/ok" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/positive-authorisation/mtls" + credentials: + secretName: mtls-positive + type: CertificateGen + - displayName: csrf basic + name: csrf-basic + providerDisplayName: Basic with CSRF + description: Should return 200 for Basic Auth with CSRF optimistic scenario + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/csrf-basic/ok" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/positive-authorisation/csrf-basic" + credentials: + secretName: basic-test + type: Basic + csrfInfo: + tokenEndpointURL: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/csrf/token" + - displayName: csrf-oauth + name: csrf-oauth + providerDisplayName: OAuth with CSRF + description: Should return 200 when calling endpoint protected with OAuth with CSRF + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/csrf-oauth/ok" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/positive-authorisation/csrf-oauth" + credentials: + secretName: oauth-test + authenticationUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/oauth/token" + type: OAuth + csrfInfo: + tokenEndpointURL: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/csrf/token" + - displayName: csrf-mtls-oauth + name: csrf-mtls-oauth + providerDisplayName: mTLS-OAuth with CSRF + description: Should return 200 when calling endpoint protected with mTLS OAuth with CSRF + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/csrf-oauth/ok" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/positive-authorisation/csrf-mtls-oauth" + credentials: + secretName: mtls-oauth-positive + authenticationUrl: "https://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8090/v1/api/mtls-oauth/token" + type: OAuthWithCert + csrfInfo: + tokenEndpointURL: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/csrf/token" + - displayName: csrf-mtls + name: csrf-mtls + providerDisplayName: mTLS with CSRF + description: Should return 200 when calling endpoint protected with mTLS with CSRF + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "https://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8090/v1/api/csrf-mtls/ok" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/positive-authorisation/csrf-mtls" + credentials: + secretName: mtls-positive + type: CertificateGen + csrfInfo: + tokenEndpointURL: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/csrf/token" + - displayName: basic-auth-with-request-parameters + name: basic-auth-with-request-parameters + providerDisplayName: Basic + description: Should return 200 when calling endpoint protected with Basic Auth with additional request parameters + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/request-parameters-basic/ok" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/positive-authorisation/basic-auth-with-request-parameters" + requestParametersSecretName: request-parameters-test + credentials: + secretName: basic-test + type: Basic diff --git a/tests/resources/charts/gateway-test/charts/test/templates/applications/proxy-cases.yml b/tests/resources/charts/gateway-test/charts/test/templates/applications/proxy-cases.yml new file mode 100644 index 00000000..54b558e1 --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/templates/applications/proxy-cases.yml @@ -0,0 +1,38 @@ +apiVersion: applicationconnector.kyma-project.io/v1alpha1 +kind: Application +metadata: + name: proxy-cases + namespace: "{{ .Values.global.namespace }}" +spec: + description: Proxying + skipVerify: true + labels: + app: proxy-cases + services: + - displayName: code 451 + name: code 451 + providerDisplayName: code 451 + description: Should return 451 forwarded from target endpoint + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/unsecure/code/451" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/proxy-cases/code-451" + - displayName: code 307 + name: code 307 + providerDisplayName: code 307 + description: Should return 307 forwarded from target endpoint + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/unsecure/code/307" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/proxy-cases/code-307" + - displayName: code 203 + name: code 203 + providerDisplayName: code 203 + description: Should return 203 forwarded from target endpoint + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/unsecure/code/203" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/proxy-cases/code-203" diff --git a/tests/resources/charts/gateway-test/charts/test/templates/applications/proxy-errors.yml b/tests/resources/charts/gateway-test/charts/test/templates/applications/proxy-errors.yml new file mode 100644 index 00000000..c0b46d34 --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/templates/applications/proxy-errors.yml @@ -0,0 +1,20 @@ +apiVersion: applicationconnector.kyma-project.io/v1alpha1 +kind: Application +metadata: + name: proxy-errors + namespace: "{{ .Values.global.namespace }}" +spec: + description: Proxying edge cases + skipVerify: true + labels: + app: proxy-errors + services: + - displayName: timeout + name: timeout + providerDisplayName: timeout + description: Should return 504 when target times out + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "http://{{ .Values.global.mockServiceName }}.{{ .Values.global.namespace }}.svc.cluster.local:8080/v1/api/unsecure/timeout" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/proxy-errors/timeout" diff --git a/tests/resources/charts/gateway-test/charts/test/templates/applications/redirect.yml b/tests/resources/charts/gateway-test/charts/test/templates/applications/redirect.yml new file mode 100644 index 00000000..40d5137d --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/templates/applications/redirect.yml @@ -0,0 +1,41 @@ +apiVersion: applicationconnector.kyma-project.io/v1alpha1 +kind: Application +metadata: + name: redirects + namespace: "{{ .Values.global.namespace }}" +spec: + description: Endpoints for redirect cases + skipVerify: true + labels: + app: redirect-cases + services: + - displayName: unsecured + name: unsecured + providerDisplayName: unsecured + description: Should return 200 when redirected to unsecured endpoint + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "http://{{ .Values.mockServiceName }}.{{ .Values.namespace }}.svc.cluster.local:8080/v1/api/redirect/ok" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/redirects/unsecured" + - displayName: basic + name: basic + providerDisplayName: basic + description: Should return 200 when redirected to basic-auth endpoint + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "http://{{ .Values.mockServiceName }}.{{ .Values.namespace }}.svc.cluster.local:8080/v1/api/redirect/basic" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/redirects/basic" + credentials: + secretName: redirect-basic + type: Basic + - displayName: external + name: external + providerDisplayName: external + description: Should return 200 when redirected to external service + id: "{{ uuidv4 }}" + entries: + - type: API + targetUrl: "http://{{ .Values.mockServiceName }}.{{ .Values.namespace }}.svc.cluster.local:8080/v1/api/redirect/external" + centralGatewayUrl: "http://central-application-gateway.kyma-system:8080/redirects/external" diff --git a/tests/resources/charts/gateway-test/charts/test/templates/service-account.yml b/tests/resources/charts/gateway-test/charts/test/templates/service-account.yml new file mode 100644 index 00000000..80fd71f1 --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/templates/service-account.yml @@ -0,0 +1,33 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ .Values.global.serviceAccountName }} + namespace: {{ .Values.global.namespace }} +automountServiceAccountToken: true +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ .Values.global.serviceAccountName }} +subjects: + - kind: ServiceAccount + name: {{ .Values.global.serviceAccountName }} + namespace: {{ .Values.global.namespace }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ .Values.global.serviceAccountName }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ .Values.global.serviceAccountName }} +rules: + - verbs: + - get + - list + apiGroups: + - "" + - applicationconnector.kyma-project.io + resources: + - "*" diff --git a/tests/resources/charts/gateway-test/charts/test/templates/test.yml b/tests/resources/charts/gateway-test/charts/test/templates/test.yml new file mode 100644 index 00000000..b31e0115 --- /dev/null +++ b/tests/resources/charts/gateway-test/charts/test/templates/test.yml @@ -0,0 +1,15 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: application-gateway-test + namespace: {{ .Values.global.namespace }} +spec: + template: + spec: + containers: + - name: application-gateway-test + image: {{ include "imageurl" (dict "reg" .Values.global.containerRegistry "img" .Values.global.images.gatewayTest) }} + imagePullPolicy: Always + restartPolicy: Never + serviceAccountName: {{ .Values.global.serviceAccountName }} + backoffLimit: 0 \ No newline at end of file diff --git a/tests/resources/charts/gateway-test/values.yaml b/tests/resources/charts/gateway-test/values.yaml new file mode 100644 index 00000000..63ec00cb --- /dev/null +++ b/tests/resources/charts/gateway-test/values.yaml @@ -0,0 +1,19 @@ +global: + containerRegistry: + path: "europe-docker.pkg.dev/kyma-project" + + images: + gatewayTest: + name: "gateway-test" + version: "v20230925-75c3a9a8" + directory: "prod" + mockApplication: + name: "mock-app" + version: "v20230925-75c3a9a8" + directory: "prod" + + serviceAccountName: "test-account" + namespace: "test" + + mockServiceName: "mock-application" + diff --git a/tests/resources/installation-config/mini-kyma-os.yaml b/tests/resources/installation-config/mini-kyma-os.yaml new file mode 100644 index 00000000..63eb2f7c --- /dev/null +++ b/tests/resources/installation-config/mini-kyma-os.yaml @@ -0,0 +1,9 @@ +--- +defaultNamespace: kyma-system +prerequisites: + - name: "istio" + namespace: "istio-system" + - name: "certificates" + namespace: "istio-system" +components: + - name: "application-connector" \ No newline at end of file diff --git a/tests/resources/installation-config/mini-kyma-skr.yaml b/tests/resources/installation-config/mini-kyma-skr.yaml new file mode 100644 index 00000000..d1a6e717 --- /dev/null +++ b/tests/resources/installation-config/mini-kyma-skr.yaml @@ -0,0 +1,10 @@ +--- +defaultNamespace: kyma-system +prerequisites: + - name: "istio" + namespace: "istio-system" + - name: "certificates" + namespace: "istio-system" +components: + - name: "application-connector" + - name: "compass-runtime-agent" \ No newline at end of file diff --git a/tests/resources/patches/central-application-connectivity-validator.json b/tests/resources/patches/central-application-connectivity-validator.json new file mode 100644 index 00000000..b676d8dd --- /dev/null +++ b/tests/resources/patches/central-application-connectivity-validator.json @@ -0,0 +1,24 @@ +[ + { + "op": "replace", + "path": "/spec/template/spec/containers/0/args", + "value": [ + "/app/centralapplicationconnectivityvalidator", + "--proxyPort=8080", + "--externalAPIPort=8081", + "--eventingPathPrefixV1=/%%APP_NAME%%/v1/events", + "--eventingPathPrefixV2=/%%APP_NAME%%/v2/events", + "--eventingPublisherHost=echoserver.test.svc.cluster.local", + "--eventingDestinationPath=/anything/rewrite", + "--eventingPathPrefixEvents=/%%APP_NAME%%/events", + "--appNamePlaceholder=%%APP_NAME%%", + ] + }, + { + "op": "add", + "path": "/spec/template/metadata/annotations", + "value": { + "traffic.sidecar.istio.io/excludeInboundPorts": "8080" + } + } +] diff --git a/tests/resources/patches/coredns.yaml b/tests/resources/patches/coredns.yaml new file mode 100644 index 00000000..1a7c466f --- /dev/null +++ b/tests/resources/patches/coredns.yaml @@ -0,0 +1,40 @@ +apiVersion: v1 +data: + Corefile: |2 + + .:53 { + errors + health + rewrite name regex (.*)\.local\.kyma\.dev istio-ingressgateway.istio-system.svc.cluster.local + ready + kubernetes cluster.local in-addr.arpa ip6.arpa { + pods insecure + fallthrough in-addr.arpa ip6.arpa + } + hosts /etc/coredns/NodeHosts { + reload 1s + fallthrough + } + prometheus :9153 + forward . tls://1.1.1.1 tls://1.0.0.1 { + tls_servername cloudflare-dns.com + health_check 5s + } + cache 30 + loop + reload + loadbalance + } + + NodeHosts: | + 172.18.0.3 k3d-kyma-server-0 + 172.18.0.2 k3d-kyma-registry + 172.18.0.4 k3d-kyma-agent-0 +kind: ConfigMap +metadata: + annotations: + objectset.rio.cattle.io/owner-gvk: k3s.cattle.io/v1, Kind=Addon + objectset.rio.cattle.io/owner-name: coredns + objectset.rio.cattle.io/owner-namespace: kube-system + name: coredns + namespace: kube-system diff --git a/tests/scripts/check-pod-logs.sh b/tests/scripts/check-pod-logs.sh new file mode 100755 index 00000000..acba6d06 --- /dev/null +++ b/tests/scripts/check-pod-logs.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +NAMESPACE=test +GOPATH=$(go env GOPATH) +JOB_NAME=$1 + +if [ $# -ne 1 ]; then + echo "Usage: check-pod-logs.sh " + exit 1 +fi + + +time_so_far=0 # we've already waited that many seconds +sleep_sec=5 # wait between checks +wait_timeout=900 # 15min -> 900sec +retval_complete=1 +retval_failed=1 +while [[ $retval_complete -ne 0 ]] && [[ $retval_failed -ne 0 ]] && [[ $time_so_far -le $wait_timeout ]]; do + sleep $sleep_sec + time_so_far=$((time_so_far+sleep_sec)) + + output=$(kubectl wait --for=condition=failed -n $NAMESPACE job/$JOB_NAME --timeout=0 2>&1) + retval_failed=$? + output=$(kubectl wait --for=condition=complete -n $NAMESPACE job/$JOB_NAME --timeout=0 2>&1) + retval_complete=$? +done + +if ([[ ${EXPORT_RESULT} == true ]]); then + kubectl -n $NAMESPACE logs -f job/$JOB_NAME | tee /dev/stderr | $GOPATH/bin/go-junit-report -subtest-mode exclude-parents -set-exit-code > junit-report.xml +else + kubectl -n $NAMESPACE logs -f job/$JOB_NAME +fi + +if [ $retval_failed -eq 0 ]; then + echo "Job failed. Please check logs." + exit 1 +fi diff --git a/tests/scripts/generate-self-signed-certs.sh b/tests/scripts/generate-self-signed-certs.sh new file mode 100755 index 00000000..9cd6422e --- /dev/null +++ b/tests/scripts/generate-self-signed-certs.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash + +if [ $# -ne 2 ]; then + echo "Usage: generate-self-signed-certs.sh

" + exit 1 +fi + +export APP_URL=$1 +export GATEWAY_TEST_CERTS_DIR=$2 +export SUBJECT="/C=PL/ST=A/O=SAP/CN=$APP_URL" + +mkdir -p "$GATEWAY_TEST_CERTS_DIR" + +echo "Generating certificate for domain: $APP_URL" +openssl version +openssl req -newkey rsa:2048 -nodes -x509 -days 365 -out "$GATEWAY_TEST_CERTS_DIR/ca.crt" -keyout "$GATEWAY_TEST_CERTS_DIR/ca.key" -subj $SUBJECT + +openssl genrsa -out "$GATEWAY_TEST_CERTS_DIR/server.key" 2048 +openssl genrsa -out "$GATEWAY_TEST_CERTS_DIR"/client.key 2048 + +openssl req -new \ + -key "$GATEWAY_TEST_CERTS_DIR/server.key" \ + -subj "$SUBJECT" \ + -reqexts SAN \ + -config <(cat /etc/ssl/openssl.cnf \ + <(printf "\n[SAN]\nsubjectAltName=DNS:%s" "$APP_URL")) \ + -out "$GATEWAY_TEST_CERTS_DIR/server.csr" + +openssl x509 -req -sha256 -days 365 -CA "$GATEWAY_TEST_CERTS_DIR/ca.crt" -CAkey "$GATEWAY_TEST_CERTS_DIR/ca.key" -CAcreateserial \ + -extensions SAN \ + -extfile <(cat /etc/ssl/openssl.cnf \ + <(printf "\n[SAN]\nsubjectAltName=DNS:%s" "$APP_URL" )) \ + -in "$GATEWAY_TEST_CERTS_DIR/server.csr" -out "$GATEWAY_TEST_CERTS_DIR/server.crt" + +openssl req -new \ + -key "$GATEWAY_TEST_CERTS_DIR/client.key" \ + -subj "$SUBJECT" \ + -reqexts SAN \ + -config <(cat /etc/ssl/openssl.cnf \ + <(printf "\n[SAN]\nsubjectAltName=DNS:%s" "$APP_URL")) \ + -out "$GATEWAY_TEST_CERTS_DIR/client.csr" + +openssl x509 -req -sha256 -days 365 -CA "$GATEWAY_TEST_CERTS_DIR/ca.crt" -CAkey "$GATEWAY_TEST_CERTS_DIR/ca.key" -CAcreateserial \ + -extensions SAN \ + -extfile <(cat /etc/ssl/openssl.cnf \ + <(printf "\n[SAN]\nsubjectAltName=DNS:%s" "$APP_URL")) \ + -in "$GATEWAY_TEST_CERTS_DIR/client.csr" -out "$GATEWAY_TEST_CERTS_DIR/client.crt" \ No newline at end of file diff --git a/tests/scripts/jobguard.sh b/tests/scripts/jobguard.sh new file mode 100755 index 00000000..f92a9653 --- /dev/null +++ b/tests/scripts/jobguard.sh @@ -0,0 +1,34 @@ +#!/bin/bash +export GO111MODULE=on + +ROOT_PATH=$(dirname "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)") + +KYMA_PROJECT_DIR=${KYMA_PROJECT_DIR:-"/home/prow/go/src/github.com/kyma-project"} +JOB_NAME_PATTERN=${JOB_NAME_PATTERN:-"(pre-main-kyma-components-.*)|(pre-main-kyma-tests-.*)|(pre-kyma-components-.*)|(pre-kyma-tests-.*)|(pull-.*-build)"} +TIMEOUT=${JOBGUARD_TIMEOUT:-"15m"} + +export TEST_INFRA_SOURCES_DIR="${KYMA_PROJECT_DIR}/test-infra" + +if [ -z "$PULL_PULL_SHA" ]; then + echo "WORKAROUND: skip jobguard execution - not on PR commit" + exit 0 +fi + +args=( + "-github-endpoint=http://ghproxy" + "-github-endpoint=https://api.github.com" + "-github-token-path=/etc/github/token" + "-fail-on-no-contexts=false" + "-timeout=$TIMEOUT" + "-org=$REPO_OWNER" + "-repo=$REPO_NAME" + "-base-ref=$PULL_PULL_SHA" + "-expected-contexts-regexp=$JOB_NAME_PATTERN" +) + +if [ -x "/prow-tools/jobguard" ]; then + /prow-tools/jobguard "${args[@]}" +else + cd "${ROOT_PATH}/cmd/jobguard" || exit 1 + go run main.go "${args[@]}" +fi diff --git a/tests/scripts/local-build.sh b/tests/scripts/local-build.sh new file mode 100755 index 00000000..bb230665 --- /dev/null +++ b/tests/scripts/local-build.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +if [ $# -ne 2 ]; then + echo "Usage: local_build.sh " + exit 1 +fi + +export DOCKER_TAG=$1 +export DOCKER_PUSH_REPOSITORY=$2 +make release diff --git a/tests/scripts/test-cra.sh b/tests/scripts/test-cra.sh new file mode 100755 index 00000000..f3313d10 --- /dev/null +++ b/tests/scripts/test-cra.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +./tests/components/application-connector/scripts/jobguard.sh + +service docker start +curl -s https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh | bash +curl -Lo kyma.tar.gz "https://github.com/kyma-project/cli/releases/download/$(curl -s https://api.github.com/repos/kyma-project/cli/releases/latest | grep tag_name | cut -d '"' -f 4)/kyma_Linux_x86_64.tar.gz" && mkdir kyma-release && tar -C kyma-release -zxvf kyma.tar.gz && chmod +x kyma-release/kyma && rm -rf kyma.tar.gz +kyma-release/kyma provision k3d +kubectl cluster-info +kyma-release/kyma deploy --ci --components-file tests/components/application-connector/resources/installation-config/mini-kyma-skr.yaml --source local --workspace $PWD +cd tests/components/application-connector + +# reconfigure DNS +kubectl apply -f resources/patches/coredns.yaml +kubectl -n kube-system delete pods -l k8s-app=kube-dns + +make -f Makefile.test-compass-runtime-agent test-compass-runtime-agent +failed=$? + +k3d cluster delete kyma +exit $failed diff --git a/tests/test/application-connectivity-validator/suite_test.go b/tests/test/application-connectivity-validator/suite_test.go new file mode 100644 index 00000000..b08c4fde --- /dev/null +++ b/tests/test/application-connectivity-validator/suite_test.go @@ -0,0 +1,136 @@ +package application_connectivity_validator + +import ( + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/suite" + + "github.com/kyma-project/kyma/tests/components/application-connector/internal/testkit/httpd" +) + +const v1EventsFormat = "http://central-application-connectivity-validator.kyma-system:8080/%s/v1/events" +const v2EventsFormat = "http://central-application-connectivity-validator.kyma-system:8080/%s/v2/events" +const publishRoutedFormat = "http://central-application-connectivity-validator.kyma-system:8080/%s/events" + +const XForwardedClientCertFormat = "Hash=hash1;Cert=\"cert\";Subject=\"O=client organization,CN=%s\";URI=,By=spiffe://cluster.local/ns/default/sa/echoserver;Hash=hash;Subject=\"\";URI=spiffe://cluster.local/ns/istio-system/sa/istio-ingressgateway-service-account" + +const standaloneAppName = "event-test-standalone" +const compassAppName = "event-test-compass" + +type ValidatorSuite struct { + suite.Suite +} + +func (vs *ValidatorSuite) SetupSuite() { +} + +func (vs *ValidatorSuite) TearDownSuite() { + _, err := http.Post("http://localhost:15000/quitquitquit", "", nil) + vs.Nil(err) + _, err = http.Post("http://localhost:15020/quitquitquit", "", nil) + vs.Nil(err) +} + +func TestValidatorSuite(t *testing.T) { + suite.Run(t, new(ValidatorSuite)) +} + +func (vs *ValidatorSuite) TestGoodCert() { + cli := httpd.NewCli(vs.T()) + + for _, testCase := range []struct { + appName string + expectedCName string + }{{ + appName: standaloneAppName, + expectedCName: standaloneAppName, + }, { + appName: compassAppName, + expectedCName: "clientId1", + }} { + v1Events := fmt.Sprintf(v1EventsFormat, testCase.appName) + v2Events := fmt.Sprintf(v2EventsFormat, testCase.appName) + routedEvents := fmt.Sprintf(publishRoutedFormat, testCase.appName) + endpoints := []string{v1Events, v2Events, routedEvents} + + for _, url := range endpoints { + vs.Run(fmt.Sprintf("Send request to %s URL", url), func() { + req, err := http.NewRequest(http.MethodGet, url, nil) + vs.Nil(err) + + req.Header.Add("X-Forwarded-Client-Cert", certFields(testCase.expectedCName)) + + res, _, err := cli.Do(req) + vs.Require().Nil(err) + vs.Equal(http.StatusOK, res.StatusCode) + }) + } + } +} + +func (vs *ValidatorSuite) TestBadCert() { + cli := httpd.NewCli(vs.T()) + + appNames := []string{standaloneAppName, compassAppName} + + for _, appName := range appNames { + v1Events := fmt.Sprintf(v1EventsFormat, appName) + v2Events := fmt.Sprintf(v2EventsFormat, appName) + routedEvents := fmt.Sprintf(publishRoutedFormat, appName) + endpoints := []string{v1Events, v2Events, routedEvents} + + for _, url := range endpoints { + vs.Run(fmt.Sprintf("Send request to %s URL with incorrect cname in header", url), func() { + req, err := http.NewRequest(http.MethodGet, url, nil) + vs.Nil(err) + + req.Header.Add("X-Forwarded-Client-Cert", certFields("nonexistant")) + + res, _, err := cli.Do(req) + vs.Require().Nil(err) + vs.Equal(http.StatusForbidden, res.StatusCode) + }) + + vs.Run(fmt.Sprintf("Send request to %s URL without subject in header", url), func() { + req, err := http.NewRequest(http.MethodGet, url, nil) + vs.Nil(err) + + req.Header.Add("X-Forwarded-Client-Cert", "Hash=hash1;Cert=\"cert\"") + + res, _, err := cli.Do(req) + vs.Require().Nil(err) + vs.Equal(http.StatusForbidden, res.StatusCode) + }) + + vs.Run(fmt.Sprintf("Send request to %s URL without header", url), func() { + req, err := http.NewRequest(http.MethodGet, url, nil) + vs.Nil(err) + + res, _, err := cli.Do(req) + vs.Require().Nil(err) + vs.Equal(http.StatusInternalServerError, res.StatusCode) + }) + } + } +} + +func (vs *ValidatorSuite) TestInvalidPathPrefix() { + const v3vents = "http://central-application-connectivity-validator.kyma-system:8080/event-test-compass/v3/events" + + cli := httpd.NewCli(vs.T()) + + req, err := http.NewRequest(http.MethodGet, v3vents, nil) + vs.Nil(err) + + req.Header.Add("X-Forwarded-Client-Cert", certFields("clientId1")) + + res, _, err := cli.Do(req) + vs.Require().Nil(err) + vs.Equal(http.StatusNotFound, res.StatusCode) +} + +func certFields(cname string) string { + return fmt.Sprintf(XForwardedClientCertFormat, cname) +} diff --git a/tests/test/application-connectivity-validator/tools.go b/tests/test/application-connectivity-validator/tools.go new file mode 100644 index 00000000..0d136f35 --- /dev/null +++ b/tests/test/application-connectivity-validator/tools.go @@ -0,0 +1,5 @@ +package application_connectivity_validator + +func validatorURL(app, path string) string { + return "http://central-application-connectivity-validator.kyma-system:8080/" + app + "/" + path +} diff --git a/tests/test/application-gateway/complex_test.go b/tests/test/application-gateway/complex_test.go new file mode 100644 index 00000000..7d94b0a4 --- /dev/null +++ b/tests/test/application-gateway/complex_test.go @@ -0,0 +1,28 @@ +package application_gateway + +import ( + "time" + + "github.com/kyma-project/kyma/tests/components/application-connector/internal/testkit/httpd" +) + +func (gs *GatewaySuite) TestComplex() { + gs.Run("OAuth token renewal", func() { + http := httpd.NewCli(gs.T()) + + url := gatewayURL("complex-cases", "oauth-expired-token-renewal") + gs.T().Log("Url:", url) + + // Authorize, then call endpoint + res, _, err := http.Get(url) + gs.Nil(err, "First request failed") + gs.Equal(200, res.StatusCode, "First request failed") + + time.Sleep(10 * time.Second) // wait for token to expire + + // Call endpoint, requiring token renewall + res, _, err = http.Get(url) + gs.Nil(err, "Second request failed") + gs.Equal(200, res.StatusCode, "Second request failed") + }) +} diff --git a/tests/test/application-gateway/runner_test.go b/tests/test/application-gateway/runner_test.go new file mode 100644 index 00000000..76e4860c --- /dev/null +++ b/tests/test/application-gateway/runner_test.go @@ -0,0 +1,109 @@ +package application_gateway + +import ( + "context" + "net/http" + "strconv" + "strings" + + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/kyma-project/kyma/tests/components/application-connector/internal/testkit/httpd" +) + +var applications = []string{"positive-authorisation", "negative-authorisation", "path-related-error-handling", "missing-resources-error-handling", "proxy-cases", "proxy-errors", "redirects", "code-rewriting"} + +func (gs *GatewaySuite) TestGetRequest() { + + for _, app := range applications { + app, err := gs.cli.ApplicationconnectorV1alpha1().Applications().Get(context.Background(), app, v1.GetOptions{}) + gs.Nil(err) + + gs.Run(app.Spec.Description, func() { + for _, service := range app.Spec.Services { + gs.Run(service.Description, func() { + http := httpd.NewCli(gs.T()) + + for _, entry := range service.Entries { + if entry.Type != "API" { + gs.T().Log("Skipping event entry") + continue + } + + expectedCode, err := getExpectedHTTPCode(service) + if err != nil { + gs.T().Log("Error during getting the error code from description -> applicationCRD") + gs.T().Fail() + } + + res, _, err := http.Get(entry.CentralGatewayUrl) + gs.Nil(err, "Request failed") + gs.Equal(expectedCode, res.StatusCode, "Incorrect response code") + } + }) + } + }) + } +} + +func (gs *GatewaySuite) TestResponseBody() { + app, err := gs.cli.ApplicationconnectorV1alpha1().Applications().Get(context.Background(), "proxy-cases", v1.GetOptions{}) + gs.Nil(err) + for _, service := range app.Spec.Services { + gs.Run(service.Description, func() { + http := httpd.NewCli(gs.T()) + + for _, entry := range service.Entries { + if entry.Type != "API" { + gs.T().Log("Skipping event entry") + continue + } + + expectedCode, err := getExpectedHTTPCode(service) + if err != nil { + gs.T().Log("Error during getting the error code from description -> applicationCRD") + gs.T().Fail() + } + + _, body, err := http.Get(entry.CentralGatewayUrl) + gs.Nil(err, "Request failed") + + codeStr := strconv.Itoa(expectedCode) + + gs.Equal(codeStr, string(body), "Incorrect body") + } + }) + } +} + +func (gs *GatewaySuite) TestBodyPerMethod() { + app, err := gs.cli.ApplicationconnectorV1alpha1().Applications().Get(context.Background(), "methods-with-body", v1.GetOptions{}) + gs.Nil(err) + for _, service := range app.Spec.Services { + gs.Run(service.Description, func() { + httpCli := httpd.NewCli(gs.T()) + + for _, entry := range service.Entries { + if entry.Type != "API" { + gs.T().Log("Skipping event entry") + continue + } + + method := service.Description + bodyBuf := strings.NewReader(service.Description) + + req, err := http.NewRequest(method, entry.CentralGatewayUrl, bodyBuf) + gs.Nil(err, "Preparing request failed") + + _, body, err := httpCli.Do(req) + gs.Nil(err, "Request failed") + + res, err := unmarshalBody(body) + gs.Nil(err, "Response body wasn't correctly forwarded") + + gs.Equal(service.Description, string(res.Body), "Request body doesn't match") + gs.Equal(service.Description, res.Method, "Request method doesn't match") + } + }) + } +} diff --git a/tests/test/application-gateway/suite_test.go b/tests/test/application-gateway/suite_test.go new file mode 100644 index 00000000..764ebabf --- /dev/null +++ b/tests/test/application-gateway/suite_test.go @@ -0,0 +1,34 @@ +package application_gateway + +import ( + "net/http" + "testing" + + cli "github.com/kyma-project/kyma/components/central-application-gateway/pkg/client/clientset/versioned" + "github.com/stretchr/testify/suite" + "k8s.io/client-go/rest" +) + +type GatewaySuite struct { + suite.Suite + cli *cli.Clientset +} + +func (gs *GatewaySuite) SetupSuite() { + cfg, err := rest.InClusterConfig() + gs.Require().Nil(err) + + gs.cli, err = cli.NewForConfig(cfg) + gs.Require().Nil(err) +} + +func (gs *GatewaySuite) TearDownSuite() { + _, err := http.Post("http://localhost:15000/quitquitquit", "", nil) + gs.Nil(err) + _, err = http.Post("http://localhost:15020/quitquitquit", "", nil) + gs.Nil(err) +} + +func TestGatewaySuite(t *testing.T) { + suite.Run(t, new(GatewaySuite)) +} diff --git a/tests/test/application-gateway/tools.go b/tests/test/application-gateway/tools.go new file mode 100644 index 00000000..135c49c6 --- /dev/null +++ b/tests/test/application-gateway/tools.go @@ -0,0 +1,30 @@ +package application_gateway + +import ( + "encoding/json" + "regexp" + "strconv" + + "github.com/kyma-project/kyma/components/central-application-gateway/pkg/apis/applicationconnector/v1alpha1" + "github.com/pkg/errors" + + test_api "github.com/kyma-project/kyma/tests/components/application-connector/internal/testkit/test-api" +) + +func getExpectedHTTPCode(service v1alpha1.Service) (int, error) { + re := regexp.MustCompile(`\d+`) + if codeStr := re.FindString(service.Description); len(codeStr) > 0 { + return strconv.Atoi(codeStr) + } + return 0, errors.New("Bad configuration") +} + +func gatewayURL(app, service string) string { + return "http://central-application-gateway.kyma-system:8080/" + app + "/" + service +} + +func unmarshalBody(body []byte) (test_api.EchoResponse, error) { + res := test_api.EchoResponse{} + err := json.Unmarshal(body, &res) + return res, err +} diff --git a/tests/test/compass-runtime-agent/config.go b/tests/test/compass-runtime-agent/config.go new file mode 100644 index 00000000..c004c398 --- /dev/null +++ b/tests/test/compass-runtime-agent/config.go @@ -0,0 +1,19 @@ +package compass_runtime_agent + +import "fmt" + +type config struct { + DirectorURL string `envconfig:"default=http://compass-director.compass-system.svc.cluster.local:3000/graphql"` + SkipDirectorCertVerification bool `envconfig:"default=false"` + OAuthCredentialsNamespace string `envconfig:"default=test"` + SystemNamespace string `envconfig:"default=kyma-system"` + CompassRuntimeAgentDeploymentName string `envconfig:"default=compass-runtime-agent"` + CompassNamespace string `envconfig:"default=kyma-system"` + OAuthCredentialsSecretName string `envconfig:"default=oauth-compass-credentials"` + TestingTenant string `envconfig:"default=tenant"` +} + +func (c *config) String() string { + return fmt.Sprintf("DirectorURL: %s, SkipDirectorCertVerification: %v, OAuthCredentialsNamespace: %s, IntegrationNamespace: %s, CompassNamespace: %s, OAuthCredentialsSecretName: %s, TestingTenant %s", + c.DirectorURL, c.SkipDirectorCertVerification, c.OAuthCredentialsNamespace, c.SystemNamespace, c.CompassNamespace, c.OAuthCredentialsSecretName, c.TestingTenant) +} diff --git a/tests/test/compass-runtime-agent/suite_test.go b/tests/test/compass-runtime-agent/suite_test.go new file mode 100644 index 00000000..c3e62ea2 --- /dev/null +++ b/tests/test/compass-runtime-agent/suite_test.go @@ -0,0 +1,166 @@ +package compass_runtime_agent + +import ( + "crypto/tls" + "fmt" + "net/http" + "os" + "testing" + "time" + + cli "github.com/kyma-project/kyma/components/central-application-gateway/pkg/client/clientset/versioned" + ccclientset "github.com/kyma-project/kyma/components/compass-runtime-agent/pkg/client/clientset/versioned" + "github.com/pkg/errors" + "github.com/stretchr/testify/suite" + "github.com/vrischmann/envconfig" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + + "github.com/kyma-project/kyma/tests/components/application-connector/test/compass-runtime-agent/testkit/applications" + "github.com/kyma-project/kyma/tests/components/application-connector/test/compass-runtime-agent/testkit/director" + "github.com/kyma-project/kyma/tests/components/application-connector/test/compass-runtime-agent/testkit/graphql" + initcra "github.com/kyma-project/kyma/tests/components/application-connector/test/compass-runtime-agent/testkit/init" + compassruntimeagentinittypes "github.com/kyma-project/kyma/tests/components/application-connector/test/compass-runtime-agent/testkit/init/types" + "github.com/kyma-project/kyma/tests/components/application-connector/test/compass-runtime-agent/testkit/oauth" + "github.com/kyma-project/kyma/tests/components/application-connector/test/compass-runtime-agent/testkit/random" +) + +type CompassRuntimeAgentSuite struct { + suite.Suite + applicationsClientSet *cli.Clientset + compassConnectionClientSet *ccclientset.Clientset + coreClientSet *kubernetes.Clientset + compassRuntimeAgentConfigurator initcra.CompassRuntimeAgentConfigurator + directorClient director.Client + appComparator applications.Comparator + testConfig config + rollbackTestFunc compassruntimeagentinittypes.RollbackFunc + formationName string +} + +func (cs *CompassRuntimeAgentSuite) SetupSuite() { + + err := envconfig.InitWithPrefix(&cs.testConfig, "APP") + cs.Require().Nil(err) + + cs.T().Logf("Config: %s", cs.testConfig.String()) + + cs.T().Logf("Init Kubernetes APIs") + cs.initKubernetesApis() + + cs.T().Logf("Configure Compass Runtime Agent for test") + cs.initCompassRuntimeAgentConfigurator() + cs.initComparators() + cs.configureRuntimeAgent() +} + +func (cs *CompassRuntimeAgentSuite) initKubernetesApis() { + var cfg *rest.Config + var err error + + cs.T().Logf("Initializing with in cluster config") + cfg, err = rest.InClusterConfig() + cs.Assert().NoError(err) + + if err != nil { + cs.T().Logf("Initializing kubeconfig") + kubeconfig, ok := os.LookupEnv("KUBECONFIG") + cs.Require().True(ok) + + cfg, err = clientcmd.BuildConfigFromFlags("", kubeconfig) + cs.Require().NoError(err) + } + + cs.applicationsClientSet, err = cli.NewForConfig(cfg) + cs.Require().NoError(err) + + cs.compassConnectionClientSet, err = ccclientset.NewForConfig(cfg) + cs.Require().NoError(err) + + cs.coreClientSet, err = kubernetes.NewForConfig(cfg) + cs.Require().NoError(err) +} + +func (cs *CompassRuntimeAgentSuite) initComparators() { + secretComparator, err := applications.NewSecretComparator(cs.coreClientSet, cs.testConfig.OAuthCredentialsNamespace, cs.testConfig.SystemNamespace) + cs.Require().NoError(err) + + applicationGetter := cs.applicationsClientSet.ApplicationconnectorV1alpha1().Applications() + cs.appComparator, err = applications.NewComparator(secretComparator, applicationGetter, "kyma-system", "kyma-system") +} + +func (cs *CompassRuntimeAgentSuite) configureRuntimeAgent() { + cs.T().Helper() + + var err error + runtimeName := "cratest" + cs.formationName = "cratest" + random.RandomString(5) + + cs.rollbackTestFunc, err = cs.compassRuntimeAgentConfigurator.Do(runtimeName, cs.formationName) + cs.Require().NoError(err) +} + +func (cs *CompassRuntimeAgentSuite) initCompassRuntimeAgentConfigurator() { + var err error + cs.directorClient, err = cs.makeCompassDirectorClient() + cs.Require().NoError(err) + + cs.compassRuntimeAgentConfigurator = initcra.NewCompassRuntimeAgentConfigurator( + initcra.NewCompassConfigurator(cs.directorClient, cs.testConfig.TestingTenant), + initcra.NewCertificateSecretConfigurator(cs.coreClientSet), + initcra.NewConfigurationSecretConfigurator(cs.coreClientSet), + initcra.NewCompassConnectionCRConfiguration(cs.compassConnectionClientSet.CompassV1alpha1().CompassConnections()), + initcra.NewDeploymentConfiguration(cs.coreClientSet, "compass-runtime-agent", cs.testConfig.CompassNamespace), + cs.testConfig.OAuthCredentialsNamespace) +} + +func (cs *CompassRuntimeAgentSuite) TearDownSuite() { + if cs.rollbackTestFunc != nil { + cs.T().Logf("Restore Compass Runtime Agent configuration") + err := cs.rollbackTestFunc() + + if err != nil { + cs.T().Logf("Failed to rollback test configuration: %v", err) + } + } + _, err := http.Post("http://localhost:15000/quitquitquit", "", nil) + if err != nil { + cs.T().Logf("Failed to quit sidecar: %v", err) + } + _, err = http.Post("http://localhost:15020/quitquitquit", "", nil) + if err != nil { + cs.T().Logf("Failed to quit sidecar: %v", err) + } +} + +func TestCompassRuntimeAgentSuite(t *testing.T) { + suite.Run(t, new(CompassRuntimeAgentSuite)) +} + +func (cs *CompassRuntimeAgentSuite) makeCompassDirectorClient() (director.Client, error) { + + secretsRepo := cs.coreClientSet.CoreV1().Secrets(cs.testConfig.OAuthCredentialsNamespace) + + if secretsRepo == nil { + return nil, fmt.Errorf("could not access secrets in %s namespace", cs.testConfig.OAuthCredentialsNamespace) + } + client := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: cs.testConfig.SkipDirectorCertVerification}, + }, + Timeout: 10 * time.Second, + } + + gqlClient := graphql.NewGraphQLClient(cs.testConfig.DirectorURL, true, cs.testConfig.SkipDirectorCertVerification) + if gqlClient == nil { + return nil, fmt.Errorf("could not create GraphQLClient for endpoint %s", cs.testConfig.DirectorURL) + } + + oauthClient, err := oauth.NewOauthClient(client, secretsRepo, cs.testConfig.OAuthCredentialsSecretName) + if err != nil { + return nil, errors.Wrap(err, "Could not create OAuthClient client") + } + + return director.NewDirectorClient(gqlClient, oauthClient, cs.testConfig.TestingTenant), nil +} diff --git a/tests/test/compass-runtime-agent/synchronisation_test.go b/tests/test/compass-runtime-agent/synchronisation_test.go new file mode 100644 index 00000000..ac19f018 --- /dev/null +++ b/tests/test/compass-runtime-agent/synchronisation_test.go @@ -0,0 +1,157 @@ +package compass_runtime_agent + +import ( + "context" + "fmt" + "time" + + "github.com/kyma-project/kyma/components/central-application-gateway/pkg/apis/applicationconnector/v1alpha1" + "k8s.io/apimachinery/pkg/api/errors" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/kyma-project/kyma/tests/components/application-connector/test/compass-runtime-agent/testkit/executor" + "github.com/kyma-project/kyma/tests/components/application-connector/test/compass-runtime-agent/testkit/random" +) + +const checkAppExistsPeriod = 10 * time.Second +const appCreationTimeout = 2 * time.Minute +const appUpdateTimeout = 2 * time.Minute + +const updatedDescription = "The app was updated" + +type ApplicationReader interface { + Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha1.Application, error) +} + +func (cs *CompassRuntimeAgentSuite) TestApplication() { + expectedAppName := "app1" + updatedAppName := "app1-updated" + + compassAppName := expectedAppName + random.RandomString(10) + + correctState := false + //Create Application in Director + applicationID, err := cs.directorClient.RegisterApplication(compassAppName, "Test Application for testing Compass Runtime Agent") + cs.Require().NoError(err) + + synchronizedCompassAppName := fmt.Sprintf("mp-%s", compassAppName) + + applicationInterface := cs.applicationsClientSet.ApplicationconnectorV1alpha1().Applications() + err = cs.assignApplicationToFormationAndWaitForSync(applicationInterface, synchronizedCompassAppName, applicationID) + cs.NoError(err) + + // Compare Application created by Compass Runtime Agent with expected result + + cs.Run("Compass Runtime Agent should create Application", func() { + err = cs.appComparator.Compare(cs.T(), expectedAppName, synchronizedCompassAppName) + cs.NoError(err) + + correctState = err == nil + }) + + cs.Run("Update app", func() { + if !correctState { + cs.T().Skip("App not in correct state") + } + + _ = cs.updateAndWait(applicationInterface, synchronizedCompassAppName, applicationID) + + err = cs.appComparator.Compare(cs.T(), updatedAppName, synchronizedCompassAppName) + cs.NoError(err) + + correctState = err == nil + }) + + // Clean up + cs.Run("Compass Runtime Agent should remove Application", func() { + err = cs.removeApplicationAndWaitForSync(applicationInterface, synchronizedCompassAppName, applicationID) + cs.NoError(err) + }) +} + +func (cs *CompassRuntimeAgentSuite) updateAndWait(appReader ApplicationReader, compassAppName, applicationID string) error { + t := cs.T() + t.Helper() + + exec := func() error { + _, err := cs.directorClient.UpdateApplication(applicationID, updatedDescription) + return err + } + + verify := func() bool { + app, err := appReader.Get(context.Background(), compassAppName, v1.GetOptions{}) + if err != nil { + t.Logf("Couldn't get updated: %v", err) + } + + return err == nil && app.Spec.Description == updatedDescription + } + + return executor.ExecuteAndWaitForCondition{ + RetryableExecuteFunc: exec, + ConditionMetFunc: verify, + Tick: checkAppExistsPeriod, + Timeout: appUpdateTimeout, + }.Do() +} + +func (cs *CompassRuntimeAgentSuite) assignApplicationToFormationAndWaitForSync(appReader ApplicationReader, compassAppName, applicationID string) error { + t := cs.T() + t.Helper() + + exec := func() error { + return cs.directorClient.AssignApplicationToFormation(applicationID, cs.formationName) + } + + verify := func() bool { + _, err := appReader.Get(context.Background(), compassAppName, v1.GetOptions{}) + if err != nil { + t.Logf("Failed to get app: %v", err) + } + + return err == nil + } + + return executor.ExecuteAndWaitForCondition{ + RetryableExecuteFunc: exec, + ConditionMetFunc: verify, + Tick: checkAppExistsPeriod, + Timeout: appCreationTimeout, + }.Do() +} + +func (cs *CompassRuntimeAgentSuite) removeApplicationAndWaitForSync(appReader ApplicationReader, compassAppName, applicationID string) error { + t := cs.T() + t.Helper() + + exec := func() error { + err := cs.directorClient.UnassignApplication(applicationID, cs.formationName) + if err != nil { + return err + } + + err = cs.directorClient.UnregisterApplication(applicationID) + return err + } + + verify := func() bool { + _, err := appReader.Get(context.Background(), compassAppName, v1.GetOptions{}) + if errors.IsNotFound(err) { + t.Logf("Application was successfully removed by Compass Runtime Agent: %v", err) + return true + } + + if err != nil { + t.Logf("Failed to check whether Application was removed by Compass Runtime Agent: %v", err) + } + + return false + } + + return executor.ExecuteAndWaitForCondition{ + RetryableExecuteFunc: exec, + ConditionMetFunc: verify, + Tick: checkAppExistsPeriod, + Timeout: appCreationTimeout, + }.Do() +} diff --git a/tests/test/compass-runtime-agent/testkit/applications/comparator.go b/tests/test/compass-runtime-agent/testkit/applications/comparator.go new file mode 100644 index 00000000..1a9ab03c --- /dev/null +++ b/tests/test/compass-runtime-agent/testkit/applications/comparator.go @@ -0,0 +1,125 @@ +package applications + +import ( + "context" + "errors" + "testing" + + "github.com/kyma-project/kyma/components/central-application-gateway/pkg/apis/applicationconnector/v1alpha1" + "github.com/stretchr/testify/assert" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +//go:generate mockery --name=ApplicationGetter +type ApplicationGetter interface { + Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha1.Application, error) +} + +func NewComparator(secretComparer Comparator, applicationGetter ApplicationGetter, expectedNamespace, actualNamespace string) (Comparator, error) { + return &comparator{ + secretComparer: secretComparer, + applicationGetter: applicationGetter, + expectedNamespace: expectedNamespace, + actualNamespace: actualNamespace, + }, nil +} + +type comparator struct { + secretComparer Comparator + applicationGetter ApplicationGetter + expectedNamespace string + actualNamespace string +} + +func (c comparator) Compare(t *testing.T, expected, actual string) error { + t.Helper() + + if actual == "" || expected == "" { + return errors.New("empty actual or expected application name") + } + + actualApp, err := c.applicationGetter.Get(context.Background(), actual, v1.GetOptions{}) + if err != nil { + return err + } + + expectedApp, err := c.applicationGetter.Get(context.Background(), expected, v1.GetOptions{}) + if err != nil { + return err + } + + c.compareSpec(t, expectedApp, actualApp) + return nil +} + +func (c comparator) compareSpec(t *testing.T, expected, actual *v1alpha1.Application) { + t.Helper() + a := assert.New(t) + + a.Equal(expected.Spec.Description, actual.Spec.Description, "Description is incorrect") + a.Equal(expected.Spec.SkipInstallation, actual.Spec.SkipInstallation, "SkipInstallation is incorrect") + + c.compareServices(t, expected.Spec.Services, actual.Spec.Services) + + a.NotNil(actual.Spec.Labels) + a.Equal(actual.Name, actual.Spec.Labels["connected-app"]) + + a.Equal(expected.Spec.Tenant, actual.Spec.Tenant, "Tenant is incorrect") + a.Equal(expected.Spec.Group, actual.Spec.Group, "Group is incorrect") + + a.Equal(expected.Spec.Tags, actual.Spec.Tags, "Tags is incorrect") + a.Equal(expected.Spec.DisplayName, actual.Spec.DisplayName, "DisplayName is incorrect") + a.Equal(expected.Spec.ProviderDisplayName, actual.Spec.ProviderDisplayName, "ProviderDisplayName is incorrect") + a.Equal(expected.Spec.LongDescription, actual.Spec.LongDescription, "LongDescription is incorrect") + a.Equal(expected.Spec.SkipVerify, actual.Spec.SkipVerify, "SkipVerify is incorrect") +} + +func (c comparator) compareServices(t *testing.T, expected, actual []v1alpha1.Service) { + t.Helper() + a := assert.New(t) + + a.Equal(len(expected), len(actual)) + + for i := 0; i < len(actual); i++ { + a.Equal(expected[i].Identifier, actual[i].Identifier) + a.Equal(expected[i].DisplayName, actual[i].DisplayName) + a.Equal(expected[i].Description, actual[i].Description) + + c.compareEntries(t, expected[i].Entries, actual[i].Entries) + + a.Equal(expected[i].AuthCreateParameterSchema, actual[i].AuthCreateParameterSchema) + } +} + +func (c comparator) compareEntries(t *testing.T, expected, actual []v1alpha1.Entry) { + t.Helper() + a := assert.New(t) + + a.Equal(len(expected), len(actual)) + + for i := 0; i < len(actual); i++ { + a.Equal(expected[i].Type, actual[i].Type) + a.Equal(expected[i].TargetUrl, actual[i].TargetUrl) + a.Equal(expected[i].SpecificationUrl, actual[i].SpecificationUrl) + a.Equal(expected[i].ApiType, actual[i].ApiType) + + c.compareCredentials(t, expected[i].Credentials, actual[i].Credentials) + + a.Equal(expected[i].RequestParametersSecretName, actual[i].RequestParametersSecretName) + a.Equal(expected[i].Name, actual[i].Name) + } +} + +func (c comparator) compareCredentials(t *testing.T, expected, actual v1alpha1.Credentials) { + t.Helper() + a := assert.New(t) + + a.Equal(expected.Type, actual.Type) + + err := c.secretComparer.Compare(t, expected.SecretName, actual.SecretName) + a.NoError(err) + + a.Equal(expected.AuthenticationUrl, actual.AuthenticationUrl) + + a.Equal(expected.CSRFInfo, actual.CSRFInfo) +} diff --git a/tests/test/compass-runtime-agent/testkit/applications/comparator_test.go b/tests/test/compass-runtime-agent/testkit/applications/comparator_test.go new file mode 100644 index 00000000..5583b291 --- /dev/null +++ b/tests/test/compass-runtime-agent/testkit/applications/comparator_test.go @@ -0,0 +1,195 @@ +package applications + +import ( + "errors" + "testing" + + "github.com/kyma-project/kyma/components/central-application-gateway/pkg/apis/applicationconnector/v1alpha1" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/kyma-project/kyma/tests/components/application-connector/test/compass-runtime-agent/testkit/applications/mocks" +) + +func TestApplicationCrdCompare(t *testing.T) { + + t.Run("should compare applications", func(t *testing.T) { + secretComparatorMock := &mocks.Comparator{} + applicationGetterMock := &mocks.ApplicationGetter{} + actualApp := getTestApp("actual", "actualNamespace", "actualSecret") + expectedApp := getTestApp("expected", "expectedNamespace", "expectedSecret") + + secretComparatorMock.On("Compare", mock.Anything, "expectedSecret", "actualSecret").Return(nil) + applicationGetterMock.On("Get", mock.Anything, "actual", v1.GetOptions{}).Return(actualApp, nil).Once() + applicationGetterMock.On("Get", mock.Anything, "expected", v1.GetOptions{}).Return(expectedApp, nil).Once() + + //when + applicationComparator, err := NewComparator(secretComparatorMock, applicationGetterMock, "expectedNamespace", "actualNamespace") + err = applicationComparator.Compare(t, "expected", "actual") + + //then + require.NoError(t, err) + secretComparatorMock.AssertExpectations(t) + applicationGetterMock.AssertExpectations(t) + }) + + t.Run("should return error when expected or actual application name is empty", func(t *testing.T) { + //given + secretComparatorMock := &mocks.Comparator{} + applicationGetterMock := &mocks.ApplicationGetter{} + + { + //when + applicationComparator, err := NewComparator(secretComparatorMock, applicationGetterMock, "expected", "actual") + err = applicationComparator.Compare(t, "expected", "") + + //then + require.Error(t, err) + } + + { + //when + applicationComparator, err := NewComparator(secretComparatorMock, applicationGetterMock, "expected", "actual") + err = applicationComparator.Compare(t, "", "actual") + + //then + require.Error(t, err) + } + + }) + + t.Run("should return error when failed to get actual application", func(t *testing.T) { + //given + secretComparatorMock := &mocks.Comparator{} + applicationGetterMock := &mocks.ApplicationGetter{} + actualApp := v1alpha1.Application{} + + applicationGetterMock.On("Get", mock.Anything, "actual", v1.GetOptions{}).Return(&actualApp, errors.New("failed to get actual app")).Once() + + //when + applicationComparator, err := NewComparator(secretComparatorMock, applicationGetterMock, "expected", "actual") + err = applicationComparator.Compare(t, "expected", "actual") + + //then + require.Error(t, err) + secretComparatorMock.AssertExpectations(t) + applicationGetterMock.AssertExpectations(t) + }) + + t.Run("should return error when failed to get expected application", func(t *testing.T) { + //given + secretComparatorMock := &mocks.Comparator{} + applicationGetterMock := &mocks.ApplicationGetter{} + expectedApp := v1alpha1.Application{} + actualApp := v1alpha1.Application{} + + applicationGetterMock.On("Get", mock.Anything, "actual", v1.GetOptions{}).Return(&actualApp, nil).Once() + applicationGetterMock.On("Get", mock.Anything, "expected", v1.GetOptions{}).Return(&expectedApp, errors.New("failed to get expected app")).Once() + + //when + applicationComparator, err := NewComparator(secretComparatorMock, applicationGetterMock, "expected", "actual") + err = applicationComparator.Compare(t, "expected", "actual") + + //then + require.Error(t, err) + secretComparatorMock.AssertExpectations(t) + applicationGetterMock.AssertExpectations(t) + }) +} + +func getTestApp(name, namespace, secretName string) *v1alpha1.Application { + //given + services := make([]v1alpha1.Service, 0, 0) + entries := make([]v1alpha1.Entry, 0, 0) + + credentials := v1alpha1.Credentials{ + Type: "OAuth", + SecretName: secretName, + AuthenticationUrl: "authURL", + CSRFInfo: &v1alpha1.CSRFInfo{TokenEndpointURL: "csrfTokenURL"}, + } + + entries = append(entries, v1alpha1.Entry{ + Type: "api", + TargetUrl: "targetURL", + SpecificationUrl: "specURL", + ApiType: "v1", + Credentials: credentials, + RequestParametersSecretName: "paramSecret", + Name: "test2", + ID: "t2", + CentralGatewayUrl: "centralURL", + AccessLabel: "", //ignore for now + GatewayUrl: "", + }) + + entries = append(entries, v1alpha1.Entry{ + Type: "api", + TargetUrl: "targetURL", + SpecificationUrl: "specURL", + ApiType: "v1", + Credentials: credentials, + RequestParametersSecretName: "paramSecret", + Name: "test1", + ID: "t1", + CentralGatewayUrl: "centralURL", + AccessLabel: "", + GatewayUrl: "", + }) + + services = append(services, v1alpha1.Service{ + ID: "serviceTest", + Identifier: "st1", + Name: "srvTest1", + DisplayName: "srvTest1", + Description: "srvTest1", + Entries: entries, + AuthCreateParameterSchema: nil, + Labels: nil, + LongDescription: "", + ProviderDisplayName: "", + Tags: nil, + }) + + services = append(services, v1alpha1.Service{ + ID: "serviceTest2", + Identifier: "st2", + Name: "srvTest2", + DisplayName: "srvTest2", + Description: "srvTest2", + Entries: entries, + AuthCreateParameterSchema: nil, + Labels: nil, + LongDescription: "", + ProviderDisplayName: "", + Tags: nil, + }) + + return &v1alpha1.Application{ + TypeMeta: v1.TypeMeta{}, + ObjectMeta: v1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: v1alpha1.ApplicationSpec{ + Description: "testapp", + SkipInstallation: false, + Services: services, + Labels: map[string]string{"connected-app": name}, + + Tenant: "test", + Group: "test", + CompassMetadata: &v1alpha1.CompassMetadata{ + ApplicationID: "compassID1", + Authentication: v1alpha1.Authentication{ClientIds: []string{"11", "22"}}, + }, + Tags: []string{"tag1", "tag2"}, + DisplayName: "applicationOneDisplay", + ProviderDisplayName: "applicationOneDisplay", + LongDescription: "applicationOne Test", + SkipVerify: true, + }, + } + +} diff --git a/tests/test/compass-runtime-agent/testkit/applications/mocks/ApplicationGetter.go b/tests/test/compass-runtime-agent/testkit/applications/mocks/ApplicationGetter.go new file mode 100644 index 00000000..82933f9a --- /dev/null +++ b/tests/test/compass-runtime-agent/testkit/applications/mocks/ApplicationGetter.go @@ -0,0 +1,55 @@ +// Code generated by mockery v2.15.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + v1alpha1 "github.com/kyma-project/kyma/components/central-application-gateway/pkg/apis/applicationconnector/v1alpha1" +) + +// ApplicationGetter is an autogenerated mock type for the ApplicationGetter type +type ApplicationGetter struct { + mock.Mock +} + +// Get provides a mock function with given fields: ctx, name, opts +func (_m *ApplicationGetter) Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha1.Application, error) { + ret := _m.Called(ctx, name, opts) + + var r0 *v1alpha1.Application + if rf, ok := ret.Get(0).(func(context.Context, string, v1.GetOptions) *v1alpha1.Application); ok { + r0 = rf(ctx, name, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1alpha1.Application) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string, v1.GetOptions) error); ok { + r1 = rf(ctx, name, opts) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type mockConstructorTestingTNewApplicationGetter interface { + mock.TestingT + Cleanup(func()) +} + +// NewApplicationGetter creates a new instance of ApplicationGetter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewApplicationGetter(t mockConstructorTestingTNewApplicationGetter) *ApplicationGetter { + mock := &ApplicationGetter{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/tests/test/compass-runtime-agent/testkit/applications/mocks/Comparator.go b/tests/test/compass-runtime-agent/testkit/applications/mocks/Comparator.go new file mode 100644 index 00000000..abccb412 --- /dev/null +++ b/tests/test/compass-runtime-agent/testkit/applications/mocks/Comparator.go @@ -0,0 +1,43 @@ +// Code generated by mockery v2.15.0. DO NOT EDIT. + +package mocks + +import ( + testing "testing" + + mock "github.com/stretchr/testify/mock" +) + +// Comparator is an autogenerated mock type for the Comparator type +type Comparator struct { + mock.Mock +} + +// Compare provides a mock function with given fields: test, expected, actual +func (_m *Comparator) Compare(test *testing.T, expected string, actual string) error { + ret := _m.Called(test, expected, actual) + + var r0 error + if rf, ok := ret.Get(0).(func(*testing.T, string, string) error); ok { + r0 = rf(test, expected, actual) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +type mockConstructorTestingTNewComparator interface { + mock.TestingT + Cleanup(func()) +} + +// NewComparator creates a new instance of Comparator. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewComparator(t mockConstructorTestingTNewComparator) *Comparator { + mock := &Comparator{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/tests/test/compass-runtime-agent/testkit/applications/secretcomparator.go b/tests/test/compass-runtime-agent/testkit/applications/secretcomparator.go new file mode 100644 index 00000000..5573754d --- /dev/null +++ b/tests/test/compass-runtime-agent/testkit/applications/secretcomparator.go @@ -0,0 +1,59 @@ +package applications + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +//go:generate mockery --name=Comparator +type Comparator interface { + Compare(test *testing.T, expected, actual string) error +} + +func NewSecretComparator(coreClientSet kubernetes.Interface, expectedNamespace, actualNamespace string) (Comparator, error) { + return &secretComparator{ + coreClientSet: coreClientSet, + expectedNamespace: expectedNamespace, + actualNamespace: actualNamespace, + }, nil +} + +type secretComparator struct { + coreClientSet kubernetes.Interface + expectedNamespace string + actualNamespace string +} + +func (c secretComparator) Compare(t *testing.T, expected, actual string) error { + t.Helper() + + if actual == "" && expected == "" { + return nil + } + + if actual == "" || expected == "" { + return errors.New("empty actual or expected secret name") + } + + expectedSecretRepo := c.coreClientSet.CoreV1().Secrets(c.expectedNamespace) + actualSecretRepo := c.coreClientSet.CoreV1().Secrets(c.actualNamespace) + + expectedSecret, err := expectedSecretRepo.Get(context.Background(), expected, metav1.GetOptions{}) + if err != nil { + return err + } + + actualSecret, err := actualSecretRepo.Get(context.Background(), actual, metav1.GetOptions{}) + if err != nil { + return err + } + + require.Equal(t, expectedSecret.Data, actualSecret.Data) + + return nil +} diff --git a/tests/test/compass-runtime-agent/testkit/applications/secretcomparator_test.go b/tests/test/compass-runtime-agent/testkit/applications/secretcomparator_test.go new file mode 100644 index 00000000..1241a785 --- /dev/null +++ b/tests/test/compass-runtime-agent/testkit/applications/secretcomparator_test.go @@ -0,0 +1,117 @@ +package applications + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + v1 "k8s.io/api/core/v1" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" + core "k8s.io/client-go/kubernetes/typed/core/v1" +) + +func TestCompare(t *testing.T) { + + t.Run("should return true if secrets are equal", func(t *testing.T) { + //given + coreV1 := fake.NewSimpleClientset() + secretComparator, err := NewSecretComparator(coreV1, "test", "kyma-system") + require.NoError(t, err) + createFakeCredentialsSecret(t, coreV1.CoreV1().Secrets("test"), "expected", "test") + createFakeCredentialsSecret(t, coreV1.CoreV1().Secrets("kyma-system"), "actual", "kyma-system") + + //when + err = secretComparator.Compare(t, "expected", "actual") + + // then + require.NoError(t, err) + }) + + t.Run("should return error if failed to read actual secret", func(t *testing.T) { + //given + coreV1 := fake.NewSimpleClientset() + secretComparator, err := NewSecretComparator(coreV1, "test", "kyma-system") + require.NoError(t, err) + createFakeCredentialsSecret(t, coreV1.CoreV1().Secrets("test"), "expected", "test") + + //when + err = secretComparator.Compare(t, "actual", "expected") + + // then + require.Error(t, err) + }) + + t.Run("should return error if failed to read expected secret", func(t *testing.T) { + //given + coreV1 := fake.NewSimpleClientset() + secretComparator, err := NewSecretComparator(coreV1, "test", "kyma-system") + require.NoError(t, err) + createFakeCredentialsSecret(t, coreV1.CoreV1().Secrets("kyma-system"), "actual", "kyma-system") + + //when + err = secretComparator.Compare(t, "actual", "expected") + + // then + require.Error(t, err) + }) + + t.Run("should return error if expected secret name is empty", func(t *testing.T) { + //given + secretComparator, err := NewSecretComparator(nil, "test", "kyma-system") + require.NoError(t, err) + + //when + err = secretComparator.Compare(t, "actual", "") + + // then + require.Error(t, err) + }) + + t.Run("should return error if actual secret name is empty", func(t *testing.T) { + //given + secretComparator, err := NewSecretComparator(nil, "test", "kyma-system") + require.NoError(t, err) + + //when + err = secretComparator.Compare(t, "", "expected") + + // then + require.Error(t, err) + }) + + t.Run("should return no error if actual and expected secret name is empty", func(t *testing.T) { + //given + secretComparator, err := NewSecretComparator(nil, "test", "kyma-system") + require.NoError(t, err) + + //when + err = secretComparator.Compare(t, "", "") + + // then + require.NoError(t, err) + }) +} + +func createFakeCredentialsSecret(t *testing.T, secrets core.SecretInterface, secretName, namespace string) { + + secret := &v1.Secret{ + ObjectMeta: meta.ObjectMeta{ + Name: secretName, + Namespace: namespace, + }, + TypeMeta: meta.TypeMeta{ + Kind: "Secret", + APIVersion: "v1", + }, + Data: map[string][]byte{ + "key1": []byte("val1"), + "key2": []byte("val2"), + "key3": []byte("val3"), + }, + } + + _, err := secrets.Create(context.Background(), secret, meta.CreateOptions{}) + + require.NoError(t, err) +} diff --git a/tests/test/compass-runtime-agent/testkit/director/directorclient.go b/tests/test/compass-runtime-agent/testkit/director/directorclient.go new file mode 100644 index 00000000..6aa33363 --- /dev/null +++ b/tests/test/compass-runtime-agent/testkit/director/directorclient.go @@ -0,0 +1,320 @@ +package director + +import ( + "fmt" + + "github.com/kyma-incubator/compass/components/director/pkg/graphql" + "github.com/kyma-incubator/compass/components/director/pkg/graphql/graphqlizer" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + + gql "github.com/kyma-project/kyma/tests/components/application-connector/test/compass-runtime-agent/testkit/graphql" + "github.com/kyma-project/kyma/tests/components/application-connector/test/compass-runtime-agent/testkit/oauth" + gcli "github.com/kyma-project/kyma/tests/components/application-connector/test/compass-runtime-agent/testkit/third_party/machinebox/graphql" +) + +const ( + AuthorizationHeader = "Authorization" + TenantHeader = "Tenant" +) + +//go:generate mockery --name=Client +type Client interface { + RegisterApplication(appName, displayName string) (string, error) + UnregisterApplication(id string) error + AssignApplicationToFormation(appId, formationName string) error + UnassignApplication(appId, formationName string) error + RegisterRuntime(runtimeName string) (string, error) + UnregisterRuntime(id string) error + RegisterFormation(formationName string) error + UnregisterFormation(formationName string) error + AssignRuntimeToFormation(runtimeId, formationName string) error + GetConnectionToken(runtimeID string) (string, string, error) + UpdateApplication(id, newDesc string) (string, error) +} + +type directorClient struct { + gqlClient gql.Client + queryProvider queryProvider + graphqlizer graphqlizer.Graphqlizer + token oauth.Token + oauthClient oauth.Client + tenant string +} + +func NewDirectorClient(gqlClient gql.Client, oauthClient oauth.Client, tenant string) Client { + + return &directorClient{ + gqlClient: gqlClient, + oauthClient: oauthClient, + queryProvider: queryProvider{}, + graphqlizer: graphqlizer.Graphqlizer{}, + token: oauth.Token{}, + tenant: tenant, + } +} + +func (cc *directorClient) getToken() error { + token, err := cc.oauthClient.GetAuthorizationToken() + if err != nil { + return err + } + + if token.EmptyOrExpired() { + return errors.New("Obtained empty or expired token") + } + + cc.token = token + return nil +} + +func (cc *directorClient) RegisterFormation(formationName string) error { + log.Infof("Registering Formation") + + queryFunc := func() string { return cc.queryProvider.createFormation(formationName) } + execFunc := getExecGraphQLFunc[graphql.Formation](cc) + operationDescription := "register Formation" + successfulLogMessage := fmt.Sprintf("Successfully registered Formation %s in Director for tenant %s", formationName, cc.tenant) + + return executeQuerySkipResponse(queryFunc, execFunc, operationDescription, successfulLogMessage) +} + +func (cc *directorClient) UnregisterFormation(formationName string) error { + log.Infof("Unregistering Formation") + queryFunc := func() string { return cc.queryProvider.deleteFormation(formationName) } + execFunc := getExecGraphQLFunc[graphql.Formation](cc) + operationDescription := "unregister Formation" + successfulLogMessage := fmt.Sprintf("Successfully unregistered Formation %s in Director for tenant %s", formationName, cc.tenant) + + return executeQuerySkipResponse(queryFunc, execFunc, operationDescription, successfulLogMessage) +} + +func (cc *directorClient) RegisterRuntime(runtimeName string) (string, error) { + log.Infof("Registering Runtime") + queryFunc := func() string { return cc.queryProvider.registerRuntimeMutation(runtimeName) } + execFunc := getExecGraphQLFunc[graphql.Runtime](cc) + operationDescription := "register Runtime" + successfulLogMessage := fmt.Sprintf("Successfully registered Runtime %s in Director for tenant %s", runtimeName, cc.tenant) + + response, err := executeQuery(queryFunc, execFunc, operationDescription, successfulLogMessage) + if err != nil { + return "", err + } + + return response.Result.ID, nil +} + +func (cc *directorClient) UnregisterRuntime(id string) error { + log.Infof("Unregistering Runtime") + + queryFunc := func() string { return cc.queryProvider.deleteRuntimeMutation(id) } + execFunc := getExecGraphQLFunc[graphql.Runtime](cc) + operationDescription := "unregister Runtime" + successfulLogMessage := fmt.Sprintf("Successfully unregistered Runtime %s in Director for tenant %s", id, cc.tenant) + + response, err := executeQuery(queryFunc, execFunc, operationDescription, successfulLogMessage) + if err != nil { + return err + } + + if response.Result.ID != id { + return fmt.Errorf("Failed to unregister runtime %s in Director: received unexpected RuntimeID.", id) + } + + return nil +} + +func (cc *directorClient) GetConnectionToken(runtimeId string) (string, string, error) { + log.Infof("Requesting one time token for Runtime from Director service") + + queryFunc := func() string { return cc.queryProvider.requestOneTimeTokenMutation(runtimeId) } + execFunc := getExecGraphQLFunc[graphql.OneTimeTokenForRuntimeExt](cc) + operationDescription := "register application" + successfulLogMessage := fmt.Sprintf("Received OneTimeToken for Runtime %s in Director for tenant %s", runtimeId, cc.tenant) + + response, err := executeQuery(queryFunc, execFunc, operationDescription, successfulLogMessage) + if err != nil { + return "", "", err + } + return response.Result.Token, response.Result.ConnectorURL, nil +} + +func (cc *directorClient) RegisterApplication(appName, displayName string) (string, error) { + log.Infof("Registering Application") + + queryFunc := func() string { return cc.queryProvider.registerApplicationFromTemplateMutation(appName, displayName) } + execFunc := getExecGraphQLFunc[graphql.Application](cc) + operationDescription := "register application" + successfulLogMessage := fmt.Sprintf("Successfully registered application %s in Director for tenant %s", appName, cc.tenant) + + result, err := executeQuery(queryFunc, execFunc, operationDescription, successfulLogMessage) + if err != nil { + return "", err + } + + id := result.Result.ID + _, err = cc.AddBundle(id) + return id, err +} + +func (cc *directorClient) AddBundle(appID string) (string, error) { + log.Infof("Adding Bundle to Application") + + queryFunc := func() string { return cc.queryProvider.addBundleMutation(appID) } + execFunc := getExecGraphQLFunc[graphql.Application](cc) + operationDescription := "add bundle" + successfulLogMessage := fmt.Sprintf("Successfully added bundle to application with ID %s in Director for tenant %s", appID, cc.tenant) + + result, err := executeQuery(queryFunc, execFunc, operationDescription, successfulLogMessage) + if err != nil { + return "", err + } + return result.Result.ID, err +} + +func (cc *directorClient) UpdateApplication(id, newDesc string) (string, error) { + log.Infof("Updating Application %s", id) + + queryFunc := func() string { + return cc.queryProvider. + updateApplicationMutation(id, newDesc) + } + execFunc := getExecGraphQLFunc[graphql.Application](cc) + operationDescription := "update application" + successfulLogMessage := fmt.Sprintf("Successfully updated application %s in Director for tenant %s", id, cc.tenant) + + result, err := executeQuery(queryFunc, execFunc, operationDescription, successfulLogMessage) + if err != nil { + return "", err + } + return result.Result.ID, err +} + +func (cc *directorClient) AssignApplicationToFormation(appId, formationName string) error { + log.Infof("Assigning Application to Formation") + + queryFunc := func() string { return cc.queryProvider.assignFormationForAppMutation(appId, formationName) } + execFunc := getExecGraphQLFunc[graphql.Formation](cc) + operationDescription := "assign Application to Formation" + successfulLogMessage := fmt.Sprintf("Successfully assigned application %s to Formation %s in Director for tenant %s", appId, formationName, cc.tenant) + + return executeQuerySkipResponse(queryFunc, execFunc, operationDescription, successfulLogMessage) +} + +func (cc *directorClient) UnassignApplication(appId, formationName string) error { + log.Infof("Unregistering Application from Formation") + + queryFunc := func() string { return cc.queryProvider.unassignFormation(appId, formationName) } + execFunc := getExecGraphQLFunc[graphql.Formation](cc) + operationDescription := "unregister formation" + successfulLogMessage := fmt.Sprintf("Successfully unassigned application %s from Formation %s in Director for tenant %s", appId, formationName, cc.tenant) + + return executeQuerySkipResponse(queryFunc, execFunc, operationDescription, successfulLogMessage) +} + +func (cc *directorClient) AssignRuntimeToFormation(runtimeId, formationName string) error { + log.Infof("Assigning Runtime to Formation") + + queryFunc := func() string { return cc.queryProvider.assignFormationForRuntimeMutation(runtimeId, formationName) } + execFunc := getExecGraphQLFunc[graphql.Formation](cc) + operationDescription := "assign Runtime to Formation" + successfulLogMessage := fmt.Sprintf("Successfully assigned runtime %s to Formation %s in Director for tenant %s", runtimeId, formationName, cc.tenant) + + return executeQuerySkipResponse(queryFunc, execFunc, operationDescription, successfulLogMessage) +} + +func (cc *directorClient) UnregisterApplication(appID string) error { + log.Infof("Unregistering Application") + + queryFunc := func() string { return cc.queryProvider.unregisterApplicationMutation(appID) } + execFunc := getExecGraphQLFunc[graphql.Application](cc) + operationDescription := "Unregistering Application" + successfulLogMessage := fmt.Sprintf("Successfully unregister application %s in Director for tenant %s", appID, cc.tenant) + + response, err := executeQuery(queryFunc, execFunc, operationDescription, successfulLogMessage) + if err != nil { + return err + } + + if response.Result.ID != appID { + return fmt.Errorf("Failed to unregister Application %s in Director: received unexpected applicationID.", appID) + } + + return nil +} + +func (cc *directorClient) executeDirectorGraphQLCall(directorQuery string, tenant string, response interface{}) error { + if cc.token.EmptyOrExpired() { + log.Infof("Refreshing token to access Director Service") + if err := cc.getToken(); err != nil { + return err + } + } + + req := gcli.NewRequest(directorQuery) + req.Header.Set(AuthorizationHeader, fmt.Sprintf("Bearer %s", cc.token.AccessToken)) + req.Header.Set(TenantHeader, tenant) + + if err := cc.gqlClient.Do(req, response); err != nil { + if egErr, ok := err.(gcli.ExtendedError); ok { + return errors.Wrap(egErr, "Failed to execute GraphQL request to Director") + } + return fmt.Errorf("Failed to execute GraphQL request to Director: %v", err) + } + + return nil +} + +type Response[T any] struct { + Result *T +} + +func executeQuerySkipResponse[T any](getQueryFunc func() string, executeQueryFunc func(string, *Response[T]) error, operationDescription, successfulLogMessage string) error { + _, err := executeQuery(getQueryFunc, executeQueryFunc, operationDescription, successfulLogMessage) + + return err +} + +func executeQuery[T any](getQueryFunc func() string, executeQueryFunc func(string, *Response[T]) error, operationDescription, successfulLogMessage string) (Response[T], error) { + query := getQueryFunc() + + var response Response[T] + err := executeQueryFunc(query, &response) + + if err != nil { + return Response[T]{}, errors.Wrap(err, fmt.Sprintf("Failed to %s in Director. Request failed", operationDescription)) + } + + // Nil check is necessary due to GraphQL client not checking response code + if response.Result == nil { + return Response[T]{}, errors.New(fmt.Sprintf("Failed to %s in Director: Received nil response.", operationDescription)) + } + + log.Infof(successfulLogMessage) + + return response, nil +} + +func getExecGraphQLFunc[T any](cc *directorClient) func(string, *Response[T]) error { + return func(query string, result *Response[T]) error { + if cc.token.EmptyOrExpired() { + log.Infof("Refreshing token to access Director Service") + if err := cc.getToken(); err != nil { + return err + } + } + + req := gcli.NewRequest(query) + req.Header.Set(AuthorizationHeader, fmt.Sprintf("Bearer %s", cc.token.AccessToken)) + req.Header.Set(TenantHeader, cc.tenant) + + if err := cc.gqlClient.Do(req, result); err != nil { + if egErr, ok := err.(gcli.ExtendedError); ok { + return errors.Wrap(egErr, "Failed to execute GraphQL request to Director") + } + return fmt.Errorf("Failed to execute GraphQL request to Director: %v", err) + } + + return nil + } +} diff --git a/tests/test/compass-runtime-agent/testkit/director/directorclient_test.go b/tests/test/compass-runtime-agent/testkit/director/directorclient_test.go new file mode 100644 index 00000000..baa5480c --- /dev/null +++ b/tests/test/compass-runtime-agent/testkit/director/directorclient_test.go @@ -0,0 +1,1142 @@ +package director + +import ( + "errors" + "fmt" + "github.com/kyma-incubator/compass/components/director/pkg/graphql" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + gcli "github.com/kyma-project/kyma/tests/components/application-connector/test/compass-runtime-agent/testkit/third_party/machinebox/graphql" + + gql "github.com/kyma-project/kyma/tests/components/application-connector/test/compass-runtime-agent/testkit/graphql" + "github.com/kyma-project/kyma/tests/components/application-connector/test/compass-runtime-agent/testkit/oauth" + oauthmocks "github.com/kyma-project/kyma/tests/components/application-connector/test/compass-runtime-agent/testkit/oauth/mocks" +) + +const ( + runtimeTestingID = "test-runtime-ID-12345" + runtimeTestingName = "Runtime Test name" + testAppName = "Test-application-123" + applicationTestingID = "test-application-ID-12345" + testAppScenario = "Testing-scenario" + testAppDisplayName = "Testing-app-display-name" + validTokenValue = "12345" + tenantValue = "3e64ebae-38b5-46a0-b1ed-9ccee153a0ae" + oneTimeToken = "54321" + connectorURL = "https://kyma.cx/connector/graphql" + + expectedRegisterApplicationQuery = `mutation { + result: registerApplicationFromTemplate(in: { + templateName: "SAP Commerce Cloud" + values: [ + { placeholder: "name", value: "Test-application-123" } + { placeholder: "display-name", value: "Testing-app-display-name" } + ] + }) { id } }` + + expectedAssignAppToFormationQuery = `mutation { + result: assignFormation( + objectID: "test-application-ID-12345" + objectType: APPLICATION + formation: { name: "Testing-scenario" } + ) { id } }` + + expectedAssignRuntimeToFormationQuery = `mutation { + result: assignFormation( + objectID: "test-runtime-ID-12345" + objectType: RUNTIME + formation: { name: "Testing-scenario" } + ) { id } }` + + expectedDeleteApplicationQuery = `mutation { + result: unregisterApplication(id: "test-application-ID-12345") { + id + } }` + + expectedRegisterRuntimeQuery = `mutation { + result: registerRuntime(in: { + name: "Runtime Test name" + }) { id } }` + + expectedDeleteRuntimeQuery = `mutation { + result: unregisterRuntime(id: "test-runtime-ID-12345") { + id + }}` + + expectedOneTimeTokenQuery = `mutation { + result: requestOneTimeTokenForRuntime(id: "test-runtime-ID-12345") { + token connectorURL + }}` + + expectedRegisterFormationQuery = `mutation { + result: createFormation(formation: { + name: "Testing-scenario" + }) { id } }` + + expectedDeleteFormationQuery = `mutation { + result: deleteFormation(formation: { + name: "Testing-scenario" + }) { id } }` +) + +var ( + futureExpirationTime = time.Now().Add(time.Duration(60) * time.Minute).Unix() + passedExpirationTime = time.Now().Add(time.Duration(60) * time.Minute * -1).Unix() +) + +func TestDirectorClient_RuntimeRegistering(t *testing.T) { + expectedRequest := gcli.NewRequest(expectedRegisterRuntimeQuery) + expectedRequest.Header.Set(AuthorizationHeader, fmt.Sprintf("Bearer %s", validTokenValue)) + expectedRequest.Header.Set(TenantHeader, tenantValue) + + t.Run("Should register runtime and return new runtime ID when the Director access token is valid", func(t *testing.T) { + // given + responseDescription := "runtime description" + expectedResponse := &graphql.Runtime{ + ID: runtimeTestingID, + Name: runtimeTestingName, + Description: &responseDescription, + } + + expectedID := runtimeTestingID + + gqlClient := newQueryAssertClient(t, expectedRequest, expectedResponse) + token := oauth.Token{ + AccessToken: validTokenValue, + Expiration: futureExpirationTime, + } + + mockedOAuthClient := &oauthmocks.Client{} + mockedOAuthClient.On("GetAuthorizationToken").Return(token, nil) + + configClient := NewDirectorClient(gqlClient, mockedOAuthClient, tenantValue) + + // when + receivedRuntimeID, err := configClient.RegisterRuntime(runtimeTestingName) + + // then + assert.NoError(t, err) + assert.Equal(t, expectedID, receivedRuntimeID) + }) + + t.Run("Should not register runtime and return an error when the Director access token is empty", func(t *testing.T) { + // given + token := oauth.Token{ + AccessToken: "", + Expiration: futureExpirationTime, + } + + mockedOAuthClient := &oauthmocks.Client{} + mockedOAuthClient.On("GetAuthorizationToken").Return(token, nil) + + configClient := NewDirectorClient(nil, mockedOAuthClient, tenantValue) + + // when + receivedRuntimeID, err := configClient.RegisterRuntime(runtimeTestingName) + + // then + assert.Error(t, err) + assert.Empty(t, receivedRuntimeID) + }) + + t.Run("Should not register runtime and return an error when the Director access token is expired", func(t *testing.T) { + // given + expiredToken := oauth.Token{ + AccessToken: validTokenValue, + Expiration: passedExpirationTime, + } + + mockedOAuthClient := &oauthmocks.Client{} + mockedOAuthClient.On("GetAuthorizationToken").Return(expiredToken, nil) + + configClient := NewDirectorClient(nil, mockedOAuthClient, tenantValue) + + // when + receivedRuntimeID, err := configClient.RegisterRuntime(runtimeTestingName) + + // then + assert.Error(t, err) + assert.Empty(t, receivedRuntimeID) + }) + + t.Run("Should not register Runtime and return error when the client fails to get an access token for Director", func(t *testing.T) { + // given + mockedOAuthClient := &oauthmocks.Client{} + mockedOAuthClient.On("GetAuthorizationToken").Return(oauth.Token{}, errors.New("Failed token error")) + + configClient := NewDirectorClient(nil, mockedOAuthClient, tenantValue) + + // when + receivedRuntimeID, err := configClient.RegisterRuntime(runtimeTestingName) + + // then + assert.Error(t, err) + assert.Empty(t, receivedRuntimeID) + }) + + t.Run("Should return error when the result of the call to Director service is nil", func(t *testing.T) { + // given + validToken := oauth.Token{ + AccessToken: validTokenValue, + Expiration: futureExpirationTime, + } + + mockedOAuthClient := &oauthmocks.Client{} + mockedOAuthClient.On("GetAuthorizationToken").Return(validToken, nil) + + gqlClient := newFailingQueryAssertClient[graphql.Runtime](t, expectedRequest) + + configClient := NewDirectorClient(gqlClient, mockedOAuthClient, tenantValue) + + // when + receivedRuntimeID, err := configClient.RegisterRuntime(runtimeTestingName) + + // then + assert.Error(t, err) + assert.Empty(t, receivedRuntimeID) + }) + + t.Run("Should return error when Director fails to register Runtime ", func(t *testing.T) { + // given + validToken := oauth.Token{ + AccessToken: validTokenValue, + Expiration: futureExpirationTime, + } + + mockedOAuthClient := &oauthmocks.Client{} + mockedOAuthClient.On("GetAuthorizationToken").Return(validToken, nil) + + gqlClient := newFailingQueryAssertClient[graphql.Runtime](t, expectedRequest) + configClient := NewDirectorClient(gqlClient, mockedOAuthClient, tenantValue) + + // when + receivedRuntimeID, err := configClient.RegisterRuntime(runtimeTestingName) + + // then + assert.Error(t, err) + assert.Empty(t, receivedRuntimeID) + }) +} + +func TestDirectorClient_RuntimeUnregistering(t *testing.T) { + expectedRequest := gcli.NewRequest(expectedDeleteRuntimeQuery) + expectedRequest.Header.Set(AuthorizationHeader, fmt.Sprintf("Bearer %s", validTokenValue)) + expectedRequest.Header.Set(TenantHeader, tenantValue) + + t.Run("Should unregister runtime of given ID and return no error when the Director access token is valid", func(t *testing.T) { + // given + responseDescription := "runtime description" + expectedResponse := &graphql.Runtime{ + ID: runtimeTestingID, + Name: runtimeTestingName, + Description: &responseDescription, + } + + gqlClient := newQueryAssertClient(t, expectedRequest, expectedResponse) + validToken := oauth.Token{ + AccessToken: validTokenValue, + Expiration: futureExpirationTime, + } + + mockedOAuthClient := &oauthmocks.Client{} + mockedOAuthClient.On("GetAuthorizationToken").Return(validToken, nil) + + configClient := NewDirectorClient(gqlClient, mockedOAuthClient, tenantValue) + + // when + err := configClient.UnregisterRuntime(runtimeTestingID) + + // then + assert.NoError(t, err) + }) + + t.Run("Should not unregister runtime and return an error when the Director access token is empty", func(t *testing.T) { + // given + emptyToken := oauth.Token{ + AccessToken: "", + Expiration: futureExpirationTime, + } + + mockedOAuthClient := &oauthmocks.Client{} + mockedOAuthClient.On("GetAuthorizationToken").Return(emptyToken, nil) + + configClient := NewDirectorClient(nil, mockedOAuthClient, tenantValue) + + // when + err := configClient.UnregisterRuntime(runtimeTestingID) + + // then + assert.Error(t, err) + }) + + t.Run("Should not unregister register runtime and return an error when the Director access token is expired", func(t *testing.T) { + // given + expiredToken := oauth.Token{ + AccessToken: validTokenValue, + Expiration: passedExpirationTime, + } + + mockedOAuthClient := &oauthmocks.Client{} + mockedOAuthClient.On("GetAuthorizationToken").Return(expiredToken, nil) + + configClient := NewDirectorClient(nil, mockedOAuthClient, tenantValue) + + // when + err := configClient.UnregisterRuntime(runtimeTestingID) + + // then + assert.Error(t, err) + }) + + t.Run("Should not unregister Runtime and return error when the client fails to get an access token for Director", func(t *testing.T) { + // given + mockedOAuthClient := &oauthmocks.Client{} + mockedOAuthClient.On("GetAuthorizationToken").Return(oauth.Token{}, errors.New("Failed token error")) + + configClient := NewDirectorClient(nil, mockedOAuthClient, tenantValue) + + // when + err := configClient.UnregisterRuntime(runtimeTestingID) + + // then + assert.Error(t, err) + }) + + t.Run("Should return error when the result of the call to Director service is nil", func(t *testing.T) { + // given + validToken := oauth.Token{ + AccessToken: validTokenValue, + Expiration: futureExpirationTime, + } + + mockedOAuthClient := &oauthmocks.Client{} + mockedOAuthClient.On("GetAuthorizationToken").Return(validToken, nil) + + // given + gqlClient := newFailingQueryAssertClient[graphql.Runtime](t, expectedRequest) + + configClient := NewDirectorClient(gqlClient, mockedOAuthClient, tenantValue) + + // when + err := configClient.UnregisterRuntime(runtimeTestingID) + + // then + assert.Error(t, err) + }) + + t.Run("Should return error when Director fails to delete Runtime", func(t *testing.T) { + // given + gqlClient := newFailingQueryAssertClient[graphql.Runtime](t, expectedRequest) + + validToken := oauth.Token{ + AccessToken: validTokenValue, + Expiration: futureExpirationTime, + } + + mockedOAuthClient := &oauthmocks.Client{} + mockedOAuthClient.On("GetAuthorizationToken").Return(validToken, nil) + + configClient := NewDirectorClient(gqlClient, mockedOAuthClient, tenantValue) + + // when + err := configClient.UnregisterRuntime(runtimeTestingID) + + // then + assert.Error(t, err) + }) + + // unusual and strange case + t.Run("Should return error when Director returns bad ID after Deleting", func(t *testing.T) { + // given + responseDescription := "runtime description" + expectedResponse := &graphql.Runtime{ + ID: "BadId", + Name: runtimeTestingName, + Description: &responseDescription, + } + + gqlClient := newQueryAssertClient(t, expectedRequest, expectedResponse) + + validToken := oauth.Token{ + AccessToken: validTokenValue, + Expiration: futureExpirationTime, + } + + mockedOAuthClient := &oauthmocks.Client{} + mockedOAuthClient.On("GetAuthorizationToken").Return(validToken, nil) + + configClient := NewDirectorClient(gqlClient, mockedOAuthClient, tenantValue) + + // when + err := configClient.UnregisterRuntime(runtimeTestingID) + + // then + assert.Error(t, err) + }) +} + +func TestDirectorClient_FormationRegistering(t *testing.T) { + expectedRequest := gcli.NewRequest(expectedRegisterFormationQuery) + expectedRequest.Header.Set(AuthorizationHeader, fmt.Sprintf("Bearer %s", validTokenValue)) + expectedRequest.Header.Set(TenantHeader, tenantValue) + + t.Run("Should register Formation and return no error when the Director access token is valid", func(t *testing.T) { + // given + expectedResponse := &graphql.Formation{ + Name: testAppScenario, + } + + gqlClient := newQueryAssertClient(t, expectedRequest, expectedResponse) + + token := oauth.Token{ + AccessToken: validTokenValue, + Expiration: futureExpirationTime, + } + + mockedOAuthClient := &oauthmocks.Client{} + mockedOAuthClient.On("GetAuthorizationToken").Return(token, nil) + + configClient := NewDirectorClient(gqlClient, mockedOAuthClient, tenantValue) + + // when + err := configClient.RegisterFormation(testAppScenario) + + // then + assert.NoError(t, err) + }) + + t.Run("Should not register Formation and return an error when the Director access token is empty", func(t *testing.T) { + // given + token := oauth.Token{ + AccessToken: "", + Expiration: futureExpirationTime, + } + + mockedOAuthClient := &oauthmocks.Client{} + mockedOAuthClient.On("GetAuthorizationToken").Return(token, nil) + + configClient := NewDirectorClient(nil, mockedOAuthClient, tenantValue) + + // when + err := configClient.RegisterFormation(testAppScenario) + + // then + assert.Error(t, err) + }) + + t.Run("Should not register Formation and return an error when the Director access token is expired", func(t *testing.T) { + // given + expiredToken := oauth.Token{ + AccessToken: validTokenValue, + Expiration: passedExpirationTime, + } + + mockedOAuthClient := &oauthmocks.Client{} + mockedOAuthClient.On("GetAuthorizationToken").Return(expiredToken, nil) + + configClient := NewDirectorClient(nil, mockedOAuthClient, tenantValue) + + // when + err := configClient.RegisterFormation(testAppScenario) + + // then + assert.Error(t, err) + }) + + t.Run("Should not register Formation and return error when the client fails to get an access token for Director", func(t *testing.T) { + // given + mockedOAuthClient := &oauthmocks.Client{} + mockedOAuthClient.On("GetAuthorizationToken").Return(oauth.Token{}, errors.New("Failed token error")) + + configClient := NewDirectorClient(nil, mockedOAuthClient, tenantValue) + + // when + err := configClient.RegisterFormation(testAppScenario) + + // then + assert.Error(t, err) + }) + + t.Run("Should return error when the result of the call to Director service is nil", func(t *testing.T) { + // given + validToken := oauth.Token{ + AccessToken: validTokenValue, + Expiration: futureExpirationTime, + } + + mockedOAuthClient := &oauthmocks.Client{} + mockedOAuthClient.On("GetAuthorizationToken").Return(validToken, nil) + + gqlClient := gql.NewQueryAssertClient(t, nil, []*gcli.Request{expectedRequest}, func(t *testing.T, r interface{}) { + cfg, ok := r.(*Response[graphql.Formation]) + require.True(t, ok) + assert.Empty(t, cfg.Result) + cfg.Result = nil + }) + + configClient := NewDirectorClient(gqlClient, mockedOAuthClient, tenantValue) + + // when + err := configClient.RegisterFormation(testAppScenario) + + // then + assert.Error(t, err) + }) + + t.Run("Should return error when Director fails to register Formation ", func(t *testing.T) { + // given + validToken := oauth.Token{ + AccessToken: validTokenValue, + Expiration: futureExpirationTime, + } + + mockedOAuthClient := &oauthmocks.Client{} + mockedOAuthClient.On("GetAuthorizationToken").Return(validToken, nil) + + gqlClient := newFailingQueryAssertClient[graphql.Formation](t, expectedRequest) + configClient := NewDirectorClient(gqlClient, mockedOAuthClient, tenantValue) + + // when + err := configClient.RegisterFormation(testAppScenario) + + // then + assert.Error(t, err) + }) +} + +func TestDirectorClient_FormationUnregistering(t *testing.T) { + expectedRequest := gcli.NewRequest(expectedDeleteFormationQuery) + expectedRequest.Header.Set(AuthorizationHeader, fmt.Sprintf("Bearer %s", validTokenValue)) + expectedRequest.Header.Set(TenantHeader, tenantValue) + + t.Run("Should unregister Formation of given name and return no error when the Director access token is valid", func(t *testing.T) { + // given + expectedResponse := &graphql.Formation{ + Name: testAppScenario, + } + + gqlClient := newQueryAssertClient(t, expectedRequest, expectedResponse) + + validToken := oauth.Token{ + AccessToken: validTokenValue, + Expiration: futureExpirationTime, + } + + mockedOAuthClient := &oauthmocks.Client{} + mockedOAuthClient.On("GetAuthorizationToken").Return(validToken, nil) + + configClient := NewDirectorClient(gqlClient, mockedOAuthClient, tenantValue) + + // when + err := configClient.UnregisterFormation(testAppScenario) + + // then + assert.NoError(t, err) + }) + + t.Run("Should not unregister Formation and return an error when the Director access token is empty", func(t *testing.T) { + // given + emptyToken := oauth.Token{ + AccessToken: "", + Expiration: futureExpirationTime, + } + + mockedOAuthClient := &oauthmocks.Client{} + mockedOAuthClient.On("GetAuthorizationToken").Return(emptyToken, nil) + + configClient := NewDirectorClient(nil, mockedOAuthClient, tenantValue) + + // when + err := configClient.UnregisterFormation(testAppScenario) + + // then + assert.Error(t, err) + }) + + t.Run("Should not unregister register Formation and return an error when the Director access token is expired", func(t *testing.T) { + // given + expiredToken := oauth.Token{ + AccessToken: validTokenValue, + Expiration: passedExpirationTime, + } + + mockedOAuthClient := &oauthmocks.Client{} + mockedOAuthClient.On("GetAuthorizationToken").Return(expiredToken, nil) + + configClient := NewDirectorClient(nil, mockedOAuthClient, tenantValue) + + // when + err := configClient.UnregisterFormation(testAppScenario) + + // then + assert.Error(t, err) + }) + + t.Run("Should not unregister Formation and return error when the client fails to get an access token for Director", func(t *testing.T) { + // given + mockedOAuthClient := &oauthmocks.Client{} + mockedOAuthClient.On("GetAuthorizationToken").Return(oauth.Token{}, errors.New("Failed token error")) + + configClient := NewDirectorClient(nil, mockedOAuthClient, tenantValue) + + // when + err := configClient.UnregisterFormation(testAppScenario) + + // then + assert.Error(t, err) + }) + + t.Run("Should return error when the result of the call to Director service is nil", func(t *testing.T) { + // given + validToken := oauth.Token{ + AccessToken: validTokenValue, + Expiration: futureExpirationTime, + } + + mockedOAuthClient := &oauthmocks.Client{} + mockedOAuthClient.On("GetAuthorizationToken").Return(validToken, nil) + + // given + gqlClient := newFailingQueryAssertClient[graphql.Formation](t, expectedRequest) + configClient := NewDirectorClient(gqlClient, mockedOAuthClient, tenantValue) + + // when + err := configClient.UnregisterFormation(testAppScenario) + // then + assert.Error(t, err) + }) + + t.Run("Should return error when Director fails to delete Formation", func(t *testing.T) { + // given + gqlClient := newFailingQueryAssertClient[graphql.Formation](t, expectedRequest) + + validToken := oauth.Token{ + AccessToken: validTokenValue, + Expiration: futureExpirationTime, + } + + mockedOAuthClient := &oauthmocks.Client{} + mockedOAuthClient.On("GetAuthorizationToken").Return(validToken, nil) + + configClient := NewDirectorClient(gqlClient, mockedOAuthClient, tenantValue) + + // when + err := configClient.UnregisterFormation(testAppScenario) + + // then + assert.Error(t, err) + }) +} + +func TestDirectorClient_ApplicationRegistering(t *testing.T) { + expectedRequest := gcli.NewRequest(expectedRegisterApplicationQuery) + expectedRequest.Header.Set(AuthorizationHeader, fmt.Sprintf("Bearer %s", validTokenValue)) + expectedRequest.Header.Set(TenantHeader, tenantValue) + + t.Run("Should register application and return new application ID when the Director access token is valid", func(t *testing.T) { + // given + expectedResponse := &graphql.Application{ + Name: testAppName, + BaseEntity: &graphql.BaseEntity{ + ID: applicationTestingID, + }, + } + expectedID := applicationTestingID + gqlClient := newQueryAssertClient(t, expectedRequest, expectedResponse) + + token := oauth.Token{ + AccessToken: validTokenValue, + Expiration: futureExpirationTime, + } + + mockedOAuthClient := &oauthmocks.Client{} + mockedOAuthClient.On("GetAuthorizationToken").Return(token, nil) + + configClient := NewDirectorClient(gqlClient, mockedOAuthClient, tenantValue) + + // when + receivedApplicationID, err := configClient.RegisterApplication(testAppName, testAppDisplayName) + + // then + assert.NoError(t, err) + assert.Equal(t, expectedID, receivedApplicationID) + }) + + t.Run("Should not register application and return an error when the Director access token is empty", func(t *testing.T) { + // given + token := oauth.Token{ + AccessToken: "", + Expiration: futureExpirationTime, + } + + mockedOAuthClient := &oauthmocks.Client{} + mockedOAuthClient.On("GetAuthorizationToken").Return(token, nil) + + configClient := NewDirectorClient(nil, mockedOAuthClient, tenantValue) + + // when + receivedApplicationID, err := configClient.RegisterApplication(testAppName, testAppDisplayName) + + // then + assert.Error(t, err) + assert.Empty(t, receivedApplicationID) + }) + + t.Run("Should not register Application and return an error when the Director access token is expired", func(t *testing.T) { + // given + expiredToken := oauth.Token{ + AccessToken: validTokenValue, + Expiration: passedExpirationTime, + } + + mockedOAuthClient := &oauthmocks.Client{} + mockedOAuthClient.On("GetAuthorizationToken").Return(expiredToken, nil) + + configClient := NewDirectorClient(nil, mockedOAuthClient, tenantValue) + + // when + receivedApplicationID, err := configClient.RegisterApplication(testAppName, testAppDisplayName) + + // then + assert.Error(t, err) + assert.Empty(t, receivedApplicationID) + }) + + t.Run("Should not register Application and return error when the client fails to get an access token for Director", func(t *testing.T) { + // given + mockedOAuthClient := &oauthmocks.Client{} + mockedOAuthClient.On("GetAuthorizationToken").Return(oauth.Token{}, errors.New("Failed token error")) + + configClient := NewDirectorClient(nil, mockedOAuthClient, tenantValue) + + // when + receivedApplicationID, err := configClient.RegisterApplication(testAppName, testAppDisplayName) + + // then + assert.Error(t, err) + assert.Empty(t, receivedApplicationID) + }) + + t.Run("Should return error when the result of the call to Director service is nil", func(t *testing.T) { + // given + validToken := oauth.Token{ + AccessToken: validTokenValue, + Expiration: futureExpirationTime, + } + + mockedOAuthClient := &oauthmocks.Client{} + mockedOAuthClient.On("GetAuthorizationToken").Return(validToken, nil) + + gqlClient := newFailingQueryAssertClient[graphql.Application](t, expectedRequest) + configClient := NewDirectorClient(gqlClient, mockedOAuthClient, tenantValue) + + // when + receivedApplicationID, err := configClient.RegisterApplication(testAppName, testAppDisplayName) + + // then + assert.Error(t, err) + assert.Empty(t, receivedApplicationID) + }) + + t.Run("Should return error when Director fails to register Runtime ", func(t *testing.T) { + // given + validToken := oauth.Token{ + AccessToken: validTokenValue, + Expiration: futureExpirationTime, + } + + mockedOAuthClient := &oauthmocks.Client{} + mockedOAuthClient.On("GetAuthorizationToken").Return(validToken, nil) + + gqlClient := newFailingQueryAssertClient[graphql.Application](t, expectedRequest) + configClient := NewDirectorClient(gqlClient, mockedOAuthClient, tenantValue) + + // when + receivedRuntimeID, err := configClient.RegisterApplication(testAppName, testAppDisplayName) + + // then + assert.Error(t, err) + assert.Empty(t, receivedRuntimeID) + }) +} + +func TestDirectorClient_ApplicationAssignToFormation(t *testing.T) { + expectedRequest := gcli.NewRequest(expectedAssignAppToFormationQuery) + expectedRequest.Header.Set(AuthorizationHeader, fmt.Sprintf("Bearer %s", validTokenValue)) + expectedRequest.Header.Set(TenantHeader, tenantValue) + + t.Run("Should assign application to formation and return new application ID when the Director access token is valid", func(t *testing.T) { + // given + expectedResponse := &graphql.Formation{ + Name: testAppScenario, + } + + gqlClient := newQueryAssertClient(t, expectedRequest, expectedResponse) + token := oauth.Token{ + AccessToken: validTokenValue, + Expiration: futureExpirationTime, + } + + mockedOAuthClient := &oauthmocks.Client{} + mockedOAuthClient.On("GetAuthorizationToken").Return(token, nil) + + configClient := NewDirectorClient(gqlClient, mockedOAuthClient, tenantValue) + + // when + err := configClient.AssignApplicationToFormation(applicationTestingID, testAppScenario) + + // then + assert.NoError(t, err) + }) + + t.Run("Should not assign application to formation and return an error when the Director access token is empty", func(t *testing.T) { + // given + token := oauth.Token{ + AccessToken: "", + Expiration: futureExpirationTime, + } + + mockedOAuthClient := &oauthmocks.Client{} + mockedOAuthClient.On("GetAuthorizationToken").Return(token, nil) + + configClient := NewDirectorClient(nil, mockedOAuthClient, tenantValue) + + // when + err := configClient.AssignApplicationToFormation(applicationTestingID, testAppScenario) + // then + assert.Error(t, err) + }) +} + +func TestDirectorClient_RuntimeAssignToFormation(t *testing.T) { + expectedRequest := gcli.NewRequest(expectedAssignRuntimeToFormationQuery) + expectedRequest.Header.Set(AuthorizationHeader, fmt.Sprintf("Bearer %s", validTokenValue)) + expectedRequest.Header.Set(TenantHeader, tenantValue) + + t.Run("Should assign application to formation and return new application ID when the Director access token is valid", func(t *testing.T) { + // given + expectedResponse := &graphql.Formation{ + Name: testAppScenario, + } + + gqlClient := newQueryAssertClient(t, expectedRequest, expectedResponse) + token := oauth.Token{ + AccessToken: validTokenValue, + Expiration: futureExpirationTime, + } + + mockedOAuthClient := &oauthmocks.Client{} + mockedOAuthClient.On("GetAuthorizationToken").Return(token, nil) + + configClient := NewDirectorClient(gqlClient, mockedOAuthClient, tenantValue) + + // when + err := configClient.AssignRuntimeToFormation(runtimeTestingID, testAppScenario) + + // then + assert.NoError(t, err) + }) + + t.Run("Should not assign application to formation and return an error when the Director access token is empty", func(t *testing.T) { + // given + token := oauth.Token{ + AccessToken: "", + Expiration: futureExpirationTime, + } + + mockedOAuthClient := &oauthmocks.Client{} + mockedOAuthClient.On("GetAuthorizationToken").Return(token, nil) + + configClient := NewDirectorClient(nil, mockedOAuthClient, tenantValue) + + // when + err := configClient.AssignRuntimeToFormation(runtimeTestingID, testAppScenario) + // then + assert.Error(t, err) + }) +} + +func TestDirectorClient_ApplicationUnregistering(t *testing.T) { + expectedRequest := gcli.NewRequest(expectedDeleteApplicationQuery) + expectedRequest.Header.Set(AuthorizationHeader, fmt.Sprintf("Bearer %s", validTokenValue)) + expectedRequest.Header.Set(TenantHeader, tenantValue) + + t.Run("Should unregister runtime of given ID and return no error when the Director access token is valid", func(t *testing.T) { + // given + expectedResponse := &graphql.Application{ + Name: testAppName, + BaseEntity: &graphql.BaseEntity{ + ID: applicationTestingID, + }, + } + + gqlClient := newQueryAssertClient(t, expectedRequest, expectedResponse) + validToken := oauth.Token{ + AccessToken: validTokenValue, + Expiration: futureExpirationTime, + } + + mockedOAuthClient := &oauthmocks.Client{} + mockedOAuthClient.On("GetAuthorizationToken").Return(validToken, nil) + + configClient := NewDirectorClient(gqlClient, mockedOAuthClient, tenantValue) + + // when + err := configClient.UnregisterApplication(applicationTestingID) + + // then + assert.NoError(t, err) + }) + + t.Run("Should not unregister runtime and return an error when the Director access token is empty", func(t *testing.T) { + // given + emptyToken := oauth.Token{ + AccessToken: "", + Expiration: futureExpirationTime, + } + + mockedOAuthClient := &oauthmocks.Client{} + mockedOAuthClient.On("GetAuthorizationToken").Return(emptyToken, nil) + + configClient := NewDirectorClient(nil, mockedOAuthClient, tenantValue) + + // when + err := configClient.UnregisterApplication(applicationTestingID) + + // then + assert.Error(t, err) + }) + + t.Run("Should not unregister register runtime and return an error when the Director access token is expired", func(t *testing.T) { + // given + expiredToken := oauth.Token{ + AccessToken: validTokenValue, + Expiration: passedExpirationTime, + } + + mockedOAuthClient := &oauthmocks.Client{} + mockedOAuthClient.On("GetAuthorizationToken").Return(expiredToken, nil) + + configClient := NewDirectorClient(nil, mockedOAuthClient, tenantValue) + + // when + err := configClient.UnregisterApplication(applicationTestingID) + + // then + assert.Error(t, err) + }) + + t.Run("Should not unregister Runtime and return error when the client fails to get an access token for Director", func(t *testing.T) { + // given + mockedOAuthClient := &oauthmocks.Client{} + mockedOAuthClient.On("GetAuthorizationToken").Return(oauth.Token{}, errors.New("Failed token error")) + + configClient := NewDirectorClient(nil, mockedOAuthClient, tenantValue) + + // when + err := configClient.UnregisterApplication(applicationTestingID) + + // then + assert.Error(t, err) + }) + + t.Run("Should return error when the result of the call to Director service is nil", func(t *testing.T) { + // given + validToken := oauth.Token{ + AccessToken: validTokenValue, + Expiration: futureExpirationTime, + } + + mockedOAuthClient := &oauthmocks.Client{} + mockedOAuthClient.On("GetAuthorizationToken").Return(validToken, nil) + + // given + gqlClient := newFailingQueryAssertClient[graphql.Application](t, expectedRequest) + + configClient := NewDirectorClient(gqlClient, mockedOAuthClient, tenantValue) + + // when + err := configClient.UnregisterApplication(applicationTestingID) + + // then + assert.Error(t, err) + }) + + t.Run("Should return error when Director fails to delete Runtime", func(t *testing.T) { + // given + gqlClient := newFailingQueryAssertClient[graphql.Application](t, expectedRequest) + + validToken := oauth.Token{ + AccessToken: validTokenValue, + Expiration: futureExpirationTime, + } + + mockedOAuthClient := &oauthmocks.Client{} + mockedOAuthClient.On("GetAuthorizationToken").Return(validToken, nil) + + configClient := NewDirectorClient(gqlClient, mockedOAuthClient, tenantValue) + + // when + err := configClient.UnregisterApplication(applicationTestingID) + + // then + assert.Error(t, err) + }) + + // unusual and strange case + t.Run("Should return error when Director returns bad ID after Deleting", func(t *testing.T) { + // given + expectedResponse := &graphql.Application{ + Name: testAppName, + BaseEntity: &graphql.BaseEntity{ + ID: "badID", + }, + } + + gqlClient := newQueryAssertClient(t, expectedRequest, expectedResponse) + + validToken := oauth.Token{ + AccessToken: validTokenValue, + Expiration: futureExpirationTime, + } + + mockedOAuthClient := &oauthmocks.Client{} + mockedOAuthClient.On("GetAuthorizationToken").Return(validToken, nil) + + configClient := NewDirectorClient(gqlClient, mockedOAuthClient, tenantValue) + + // when + err := configClient.UnregisterApplication(applicationTestingID) + + // then + assert.Error(t, err) + }) +} + +func TestDirectorClient_GetConnectionToken(t *testing.T) { + expectedRequest := gcli.NewRequest(expectedOneTimeTokenQuery) + expectedRequest.Header.Set(AuthorizationHeader, fmt.Sprintf("Bearer %s", validTokenValue)) + expectedRequest.Header.Set(TenantHeader, tenantValue) + + t.Run("Should return OneTimeToken when Oauth Token is valid", func(t *testing.T) { + // given + expectedResponse := &graphql.OneTimeTokenForRuntimeExt{ + OneTimeTokenForRuntime: graphql.OneTimeTokenForRuntime{ + TokenWithURL: graphql.TokenWithURL{ + Token: oneTimeToken, + ConnectorURL: connectorURL, + }, + }, + } + + gqlClient := newQueryAssertClient(t, expectedRequest, expectedResponse) + token := oauth.Token{ + AccessToken: validTokenValue, + Expiration: futureExpirationTime, + } + + mockedOAuthClient := &oauthmocks.Client{} + mockedOAuthClient.On("GetAuthorizationToken").Return(token, nil) + + configClient := NewDirectorClient(gqlClient, mockedOAuthClient, tenantValue) + + // when + receivedOneTimeToken, receivedConnectorURL, err := configClient.GetConnectionToken(runtimeTestingID) + + // then + require.NoError(t, err) + require.NotEmpty(t, receivedOneTimeToken) + assert.Equal(t, oneTimeToken, receivedOneTimeToken) + assert.Equal(t, connectorURL, receivedConnectorURL) + }) + + t.Run("Should return error when Oauth Token is empty", func(t *testing.T) { + // given + token := oauth.Token{ + AccessToken: "", + Expiration: futureExpirationTime, + } + + mockedOAuthClient := &oauthmocks.Client{} + mockedOAuthClient.On("GetAuthorizationToken").Return(token, nil) + + configClient := NewDirectorClient(nil, mockedOAuthClient, tenantValue) + + // when + receivedOneTimeToken, receivedConnectorURL, err := configClient.GetConnectionToken(runtimeTestingID) + + // then + require.Error(t, err) + require.Empty(t, receivedConnectorURL) + require.Empty(t, receivedOneTimeToken) + }) + + t.Run("Should return error when Oauth Token is expired", func(t *testing.T) { + // given + token := oauth.Token{ + AccessToken: validTokenValue, + Expiration: passedExpirationTime, + } + + mockedOAuthClient := &oauthmocks.Client{} + mockedOAuthClient.On("GetAuthorizationToken").Return(token, nil) + + configClient := NewDirectorClient(nil, mockedOAuthClient, tenantValue) + + // when + receivedOneTimeToken, receivedConnectorURL, err := configClient.GetConnectionToken(runtimeTestingID) + + // then + require.Error(t, err) + require.Empty(t, receivedConnectorURL) + require.Empty(t, receivedOneTimeToken) + }) + + t.Run("Should return error when Director call returns nil response", func(t *testing.T) { + // given + gqlClient := newQueryAssertClient[graphql.OneTimeTokenForRuntimeExt](t, expectedRequest, nil) + + token := oauth.Token{ + AccessToken: validTokenValue, + Expiration: futureExpirationTime, + } + + mockedOAuthClient := &oauthmocks.Client{} + mockedOAuthClient.On("GetAuthorizationToken").Return(token, nil) + + configClient := NewDirectorClient(gqlClient, mockedOAuthClient, tenantValue) + + // when + receivedOneTimeToken, receivedConnectorURL, err := configClient.GetConnectionToken(runtimeTestingID) + + // then + require.Error(t, err) + require.Empty(t, receivedConnectorURL) + require.Empty(t, receivedOneTimeToken) + }) +} + +func newQueryAssertClient[T any](t *testing.T, expectedRequest *gcli.Request, expectedResponse *T) gql.Client { + return gql.NewQueryAssertClient(t, nil, []*gcli.Request{expectedRequest}, func(t *testing.T, r interface{}) { + cfg, ok := r.(*Response[T]) + require.True(t, ok) + assert.Empty(t, cfg.Result) + + if expectedResponse != nil { + cfg.Result = expectedResponse + } + }) +} + +func newFailingQueryAssertClient[T any](t *testing.T, expectedRequest *gcli.Request) gql.Client { + return gql.NewQueryAssertClient(t, nil, []*gcli.Request{expectedRequest}, func(t *testing.T, r interface{}) { + cfg, ok := r.(*Response[T]) + require.True(t, ok) + assert.Empty(t, cfg.Result) + cfg.Result = nil + }) +} diff --git a/tests/test/compass-runtime-agent/testkit/director/mocks/Client.go b/tests/test/compass-runtime-agent/testkit/director/mocks/Client.go new file mode 100644 index 00000000..ccfa1a77 --- /dev/null +++ b/tests/test/compass-runtime-agent/testkit/director/mocks/Client.go @@ -0,0 +1,193 @@ +// Code generated by mockery v2.15.0. DO NOT EDIT. + +package mocks + +import mock "github.com/stretchr/testify/mock" + +// Client is an autogenerated mock type for the Client type +type Client struct { + mock.Mock +} + +// AssignApplicationToFormation provides a mock function with given fields: appId, formationName +func (_m *Client) AssignApplicationToFormation(appId string, formationName string) error { + ret := _m.Called(appId, formationName) + + var r0 error + if rf, ok := ret.Get(0).(func(string, string) error); ok { + r0 = rf(appId, formationName) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// AssignRuntimeToFormation provides a mock function with given fields: runtimeId, formationName +func (_m *Client) AssignRuntimeToFormation(runtimeId string, formationName string) error { + ret := _m.Called(runtimeId, formationName) + + var r0 error + if rf, ok := ret.Get(0).(func(string, string) error); ok { + r0 = rf(runtimeId, formationName) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetConnectionToken provides a mock function with given fields: runtimeID +func (_m *Client) GetConnectionToken(runtimeID string) (string, string, error) { + ret := _m.Called(runtimeID) + + var r0 string + if rf, ok := ret.Get(0).(func(string) string); ok { + r0 = rf(runtimeID) + } else { + r0 = ret.Get(0).(string) + } + + var r1 string + if rf, ok := ret.Get(1).(func(string) string); ok { + r1 = rf(runtimeID) + } else { + r1 = ret.Get(1).(string) + } + + var r2 error + if rf, ok := ret.Get(2).(func(string) error); ok { + r2 = rf(runtimeID) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// RegisterApplication provides a mock function with given fields: appName, displayName +func (_m *Client) RegisterApplication(appName string, displayName string) (string, error) { + ret := _m.Called(appName, displayName) + + var r0 string + if rf, ok := ret.Get(0).(func(string, string) string); ok { + r0 = rf(appName, displayName) + } else { + r0 = ret.Get(0).(string) + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, string) error); ok { + r1 = rf(appName, displayName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RegisterFormation provides a mock function with given fields: formationName +func (_m *Client) RegisterFormation(formationName string) error { + ret := _m.Called(formationName) + + var r0 error + if rf, ok := ret.Get(0).(func(string) error); ok { + r0 = rf(formationName) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RegisterRuntime provides a mock function with given fields: runtimeName +func (_m *Client) RegisterRuntime(runtimeName string) (string, error) { + ret := _m.Called(runtimeName) + + var r0 string + if rf, ok := ret.Get(0).(func(string) string); ok { + r0 = rf(runtimeName) + } else { + r0 = ret.Get(0).(string) + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(runtimeName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UnassignApplication provides a mock function with given fields: appId, formationName +func (_m *Client) UnassignApplication(appId string, formationName string) error { + ret := _m.Called(appId, formationName) + + var r0 error + if rf, ok := ret.Get(0).(func(string, string) error); ok { + r0 = rf(appId, formationName) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UnregisterApplication provides a mock function with given fields: id +func (_m *Client) UnregisterApplication(id string) error { + ret := _m.Called(id) + + var r0 error + if rf, ok := ret.Get(0).(func(string) error); ok { + r0 = rf(id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UnregisterFormation provides a mock function with given fields: formationName +func (_m *Client) UnregisterFormation(formationName string) error { + ret := _m.Called(formationName) + + var r0 error + if rf, ok := ret.Get(0).(func(string) error); ok { + r0 = rf(formationName) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UnregisterRuntime provides a mock function with given fields: id +func (_m *Client) UnregisterRuntime(id string) error { + ret := _m.Called(id) + + var r0 error + if rf, ok := ret.Get(0).(func(string) error); ok { + r0 = rf(id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +type mockConstructorTestingTNewClient interface { + mock.TestingT + Cleanup(func()) +} + +// NewClient creates a new instance of Client. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewClient(t mockConstructorTestingTNewClient) *Client { + mock := &Client{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/tests/test/compass-runtime-agent/testkit/director/mocks/DirectorClient.go b/tests/test/compass-runtime-agent/testkit/director/mocks/DirectorClient.go new file mode 100644 index 00000000..ee496d09 --- /dev/null +++ b/tests/test/compass-runtime-agent/testkit/director/mocks/DirectorClient.go @@ -0,0 +1,60 @@ +// Code generated by mockery v2.14.0. DO NOT EDIT. + +package mocks + +import mock "github.com/stretchr/testify/mock" + +// DirectorClient is an autogenerated mock type for the DirectorClient type +type DirectorClient struct { + mock.Mock +} + +// RegisterApplication provides a mock function with given fields: appName, scenario, tenant +func (_m *DirectorClient) RegisterApplication(appName string, scenario string, tenant string) (string, error) { + ret := _m.Called(appName, scenario, tenant) + + var r0 string + if rf, ok := ret.Get(0).(func(string, string, string) string); ok { + r0 = rf(appName, scenario, tenant) + } else { + r0 = ret.Get(0).(string) + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, string, string) error); ok { + r1 = rf(appName, scenario, tenant) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UnregisterApplication provides a mock function with given fields: id, tenant +func (_m *DirectorClient) UnregisterApplication(id string, tenant string) error { + ret := _m.Called(id, tenant) + + var r0 error + if rf, ok := ret.Get(0).(func(string, string) error); ok { + r0 = rf(id, tenant) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +type mockConstructorTestingTNewDirectorClient interface { + mock.TestingT + Cleanup(func()) +} + +// NewDirectorClient creates a new instance of DirectorClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewDirectorClient(t mockConstructorTestingTNewDirectorClient) *DirectorClient { + mock := &DirectorClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/tests/test/compass-runtime-agent/testkit/director/queryprovider.go b/tests/test/compass-runtime-agent/testkit/director/queryprovider.go new file mode 100644 index 00000000..45227199 --- /dev/null +++ b/tests/test/compass-runtime-agent/testkit/director/queryprovider.go @@ -0,0 +1,127 @@ +package director + +import "fmt" + +type queryProvider struct{} + +func (qp queryProvider) createFormation(formationName string) string { + return fmt.Sprintf(`mutation { + result: createFormation(formation: { + name: "%s" + }) { id } }`, formationName) +} + +func (qp queryProvider) deleteFormation(formationName string) string { + return fmt.Sprintf(`mutation { + result: deleteFormation(formation: { + name: "%s" + }) { id } }`, formationName) +} + +func (qp queryProvider) registerApplicationFromTemplateMutation(appName, displayName string) string { + return fmt.Sprintf(`mutation { + result: registerApplicationFromTemplate(in: { + templateName: "SAP Commerce Cloud" + values: [ + { placeholder: "name", value: "%s" } + { placeholder: "display-name", value: "%s" } + ] + }) { id } }`, appName, displayName) +} + +func (qp queryProvider) addBundleMutation(appID string) string { + return fmt.Sprintf(`mutation { + result: addBundle( + applicationID: "%s" + in: { + name: "bndl-app-1" + description: "Foo bar" + apiDefinitions: [ + { + name: "comments-v1" + description: "api for adding comments" + targetURL: "http://mywordpress.com/comments" + group: "comments" + spec: { + data: "{\"openapi\":\"3.0.2\"}" + type: OPEN_API + format: YAML + } + version: { + value: "v1" + deprecated: true + deprecatedSince: "v5" + forRemoval: false + } + } + ] + } + ) { id } }`, appID) +} + +func (qp queryProvider) updateApplicationMutation(id, description string) string { + return fmt.Sprintf(`mutation { + result: updateApplication( + id: "%s" + in: {description: "%s" + }) { id } }`, + id, description) +} + +func (qp queryProvider) assignFormationForAppMutation(applicationId, formationName string) string { + return fmt.Sprintf(`mutation { + result: assignFormation( + objectID: "%s" + objectType: APPLICATION + formation: { name: "%s" } + ) { id } }`, applicationId, formationName) +} + +func (qp queryProvider) unassignFormation(applicationId, formationName string) string { + return fmt.Sprintf(`mutation { + result: unassignFormation( + objectID: "%s" + objectType: APPLICATION + formation: { name: "%s" } + ) { + name + } +}`, applicationId, formationName) +} + +func (qp queryProvider) assignFormationForRuntimeMutation(runtimeId, formationName string) string { + return fmt.Sprintf(`mutation { + result: assignFormation( + objectID: "%s" + objectType: RUNTIME + formation: { name: "%s" } + ) { id } }`, runtimeId, formationName) +} + +func (qp queryProvider) unregisterApplicationMutation(applicationID string) string { + return fmt.Sprintf(`mutation { + result: unregisterApplication(id: "%s") { + id + } }`, applicationID) +} + +func (qp queryProvider) deleteRuntimeMutation(runtimeID string) string { + return fmt.Sprintf(`mutation { + result: unregisterRuntime(id: "%s") { + id + }}`, runtimeID) +} + +func (qp queryProvider) registerRuntimeMutation(runtimeName string) string { + return fmt.Sprintf(`mutation { + result: registerRuntime(in: { + name: "%s" + }) { id } }`, runtimeName) +} + +func (qp queryProvider) requestOneTimeTokenMutation(runtimeID string) string { + return fmt.Sprintf(`mutation { + result: requestOneTimeTokenForRuntime(id: "%s") { + token connectorURL + }}`, runtimeID) +} diff --git a/tests/test/compass-runtime-agent/testkit/executor/toolkit.go b/tests/test/compass-runtime-agent/testkit/executor/toolkit.go new file mode 100644 index 00000000..f132c9b5 --- /dev/null +++ b/tests/test/compass-runtime-agent/testkit/executor/toolkit.go @@ -0,0 +1,53 @@ +package executor + +import ( + "context" + "github.com/avast/retry-go" + "github.com/pkg/errors" + "time" +) + +type RetryableExecuteFunc func() error +type ConditionMet func() bool + +type ExecuteAndWaitForCondition struct { + RetryableExecuteFunc RetryableExecuteFunc + ConditionMetFunc ConditionMet + Tick time.Duration + Timeout time.Duration +} + +func (e ExecuteAndWaitForCondition) Do() error { + + err := retry.Do(func() error { + return e.RetryableExecuteFunc() + }) + + if err != nil { + return err + } + ctx, cancel := context.WithTimeout(context.Background(), e.Timeout) + defer cancel() + + ticker := time.NewTicker(e.Tick) + + for { + select { + case <-ticker.C: + { + res := e.ConditionMetFunc() + + if res { + ticker.Stop() + return nil + } + + } + case <-ctx.Done(): + { + ticker.Stop() + return errors.New("Condition not met") + } + } + } +} diff --git a/tests/test/compass-runtime-agent/testkit/executor/toolkit_test.go b/tests/test/compass-runtime-agent/testkit/executor/toolkit_test.go new file mode 100644 index 00000000..8a634276 --- /dev/null +++ b/tests/test/compass-runtime-agent/testkit/executor/toolkit_test.go @@ -0,0 +1,101 @@ +package executor + +import ( + "github.com/pkg/errors" + "github.com/stretchr/testify/require" + "testing" + "time" +) + +func TestToolkit(t *testing.T) { + t.Run("Should return no error when verify function returns true", func(t *testing.T) { + // given + executeAndWait := ExecuteAndWaitForCondition{ + RetryableExecuteFunc: func() error { + return nil + }, + ConditionMetFunc: func() bool { + return true + }, + Tick: 10 * time.Second, + Timeout: 1 * time.Minute, + } + + // when + err := executeAndWait.Do() + + //then + require.NoError(t, err) + }) + + t.Run("Retry when exec function fails", func(t *testing.T) { + // given + counter := 1 + + executeAndWait := ExecuteAndWaitForCondition{ + + RetryableExecuteFunc: func() error { + if counter < 3 { + counter++ + return errors.New("failed") + } + + return nil + }, + ConditionMetFunc: func() bool { + return true + }, + Tick: 10 * time.Second, + Timeout: 1 * time.Minute, + } + + // when + err := executeAndWait.Do() + + //then + require.NoError(t, err) + require.Greater(t, counter, 2) + }) + + t.Run("Return error when exec function constantly fails", func(t *testing.T) { + // given + executeAndWait := ExecuteAndWaitForCondition{ + + RetryableExecuteFunc: func() error { + return errors.New("call failed") + }, + ConditionMetFunc: func() bool { + return true + }, + Tick: 10 * time.Second, + Timeout: 1 * time.Minute, + } + + // when + err := executeAndWait.Do() + + //then + require.Error(t, err) + }) + + t.Run("Return error when verify function constantly returns false", func(t *testing.T) { + // given + executeAndWait := ExecuteAndWaitForCondition{ + + RetryableExecuteFunc: func() error { + return nil + }, + ConditionMetFunc: func() bool { + return false + }, + Tick: 10 * time.Second, + Timeout: 1 * time.Minute, + } + + // when + err := executeAndWait.Do() + + //then + require.Error(t, err) + }) +} diff --git a/tests/test/compass-runtime-agent/testkit/graphql/client.go b/tests/test/compass-runtime-agent/testkit/graphql/client.go new file mode 100644 index 00000000..888094ab --- /dev/null +++ b/tests/test/compass-runtime-agent/testkit/graphql/client.go @@ -0,0 +1,76 @@ +package graphql + +import ( + "context" + "crypto/tls" + "github.com/kyma-project/kyma/tests/components/application-connector/test/compass-runtime-agent/testkit/third_party/machinebox/graphql" + "net/http" + "time" + + "github.com/sirupsen/logrus" +) + +const ( + timeout = 30 * time.Second +) + +type ClientConstructor func(certificate *tls.Certificate, graphqlEndpoint string, enableLogging bool, insecureConfigFetch bool) (Client, error) + +//go:generate mockery --name=Client +type Client interface { + Do(req *graphql.Request, res interface{}) error +} + +type client struct { + gqlClient *graphql.Client + logs []string + logging bool +} + +func NewGraphQLClient(graphqlEndpoint string, enableLogging bool, insecureSkipVerify bool) Client { + httpClient := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: insecureSkipVerify}, + }, + } + + gqlClient := graphql.NewClient(graphqlEndpoint, graphql.WithHTTPClient(httpClient)) + + client := &client{ + gqlClient: gqlClient, + logging: enableLogging, + logs: []string{}, + } + + client.gqlClient.Log = client.addLog + + return client +} + +func (c *client) Do(req *graphql.Request, res interface{}) error { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + c.clearLogs() + err := c.gqlClient.Run(ctx, req, res) + if err != nil { + for _, l := range c.logs { + if l != "" { + logrus.Info(l) + } + } + } + return err +} + +func (c *client) addLog(log string) { + if !c.logging { + return + } + + c.logs = append(c.logs, log) +} + +func (c *client) clearLogs() { + c.logs = []string{} +} diff --git a/tests/test/compass-runtime-agent/testkit/graphql/gql_client_testkit.go b/tests/test/compass-runtime-agent/testkit/graphql/gql_client_testkit.go new file mode 100644 index 00000000..1bfe2a77 --- /dev/null +++ b/tests/test/compass-runtime-agent/testkit/graphql/gql_client_testkit.go @@ -0,0 +1,47 @@ +package graphql + +import ( + "errors" + "github.com/kyma-project/kyma/tests/components/application-connector/test/compass-runtime-agent/testkit/third_party/machinebox/graphql" + "testing" + + "github.com/stretchr/testify/assert" +) + +type QueryAssertClient struct { + t *testing.T + expectedRequests []*graphql.Request + err error + modifyResponseFunc ModifyResponseFunc +} + +type ModifyResponseFunc []func(t *testing.T, r interface{}) + +func (c *QueryAssertClient) Do(req *graphql.Request, res interface{}) error { + if len(c.expectedRequests) == 0 { + return errors.New("no more requests were expected") + } + + assert.Equal(c.t, c.expectedRequests[0], req) + if len(c.expectedRequests) > 1 { + c.expectedRequests = c.expectedRequests[1:] + } + + if len(c.modifyResponseFunc) > 0 { + c.modifyResponseFunc[0](c.t, res) + if len(c.modifyResponseFunc) > 1 { + c.modifyResponseFunc = c.modifyResponseFunc[1:] + } + } + + return c.err +} + +func NewQueryAssertClient(t *testing.T, err error, expectedReq []*graphql.Request, modifyResponseFunc ...func(t *testing.T, r interface{})) Client { + return &QueryAssertClient{ + t: t, + expectedRequests: expectedReq, + err: err, + modifyResponseFunc: modifyResponseFunc, + } +} diff --git a/tests/test/compass-runtime-agent/testkit/graphql/mocks/Client.go b/tests/test/compass-runtime-agent/testkit/graphql/mocks/Client.go new file mode 100644 index 00000000..e6a42c6c --- /dev/null +++ b/tests/test/compass-runtime-agent/testkit/graphql/mocks/Client.go @@ -0,0 +1,42 @@ +// Code generated by mockery v2.15.0. DO NOT EDIT. + +package mocks + +import ( + graphql "github.com/kyma-project/kyma/tests/components/application-connector/test/compass-runtime-agent/testkit/third_party/machinebox/graphql" + mock "github.com/stretchr/testify/mock" +) + +// Client is an autogenerated mock type for the Client type +type Client struct { + mock.Mock +} + +// Do provides a mock function with given fields: req, res +func (_m *Client) Do(req *graphql.Request, res interface{}) error { + ret := _m.Called(req, res) + + var r0 error + if rf, ok := ret.Get(0).(func(*graphql.Request, interface{}) error); ok { + r0 = rf(req, res) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +type mockConstructorTestingTNewClient interface { + mock.TestingT + Cleanup(func()) +} + +// NewClient creates a new instance of Client. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewClient(t mockConstructorTestingTNewClient) *Client { + mock := &Client{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/tests/test/compass-runtime-agent/testkit/init/certificatesecrets_test.go b/tests/test/compass-runtime-agent/testkit/init/certificatesecrets_test.go new file mode 100644 index 00000000..530e11af --- /dev/null +++ b/tests/test/compass-runtime-agent/testkit/init/certificatesecrets_test.go @@ -0,0 +1,78 @@ +package init + +import ( + "context" + "github.com/stretchr/testify/require" + v1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" + "testing" +) + +func TestCertificateSecrets(t *testing.T) { + t.Run("should return rollback function that will remove secrets", func(t *testing.T) { + // given + fakeKubernetesInterface := fake.NewSimpleClientset() + + // when + configurator := NewCertificateSecretConfigurator(fakeKubernetesInterface) + rollbackFunc, err := configurator.Do("newCaSecret", "newClientSetSecret") + + // then + require.NoError(t, err) + + // given + caCertSecret := createSecret("newCaSecret", IstioSystemNamespace) + clientCertSecret := createSecret("newClientSetSecret", CompassSystemNamespace) + + _, err = fakeKubernetesInterface.CoreV1().Secrets(IstioSystemNamespace).Create(context.TODO(), caCertSecret, meta.CreateOptions{}) + require.NoError(t, err) + + _, err = fakeKubernetesInterface.CoreV1().Secrets(CompassSystemNamespace).Create(context.TODO(), clientCertSecret, meta.CreateOptions{}) + require.NoError(t, err) + + // when + err = rollbackFunc() + require.NoError(t, err) + + // then + _, err = fakeKubernetesInterface.CoreV1().Secrets("test").Get(context.TODO(), "newCaSecret", meta.GetOptions{}) + require.Error(t, err) + require.True(t, k8serrors.IsNotFound(err)) + + _, err = fakeKubernetesInterface.CoreV1().Secrets("test").Get(context.TODO(), "newClientSetSecret", meta.GetOptions{}) + require.Error(t, err) + require.True(t, k8serrors.IsNotFound(err)) + }) + + t.Run("should not return error when rollback function tries to delete non-existent secrets", func(t *testing.T) { + // given + fakeKubernetesInterface := fake.NewSimpleClientset() + + // when + configurator := NewCertificateSecretConfigurator(fakeKubernetesInterface) + rollbackFunc, err := configurator.Do("newCaSecret", "newClientSetSecret") + + // then + require.NoError(t, err) + + // when + err = rollbackFunc() + require.NoError(t, err) + }) + // TODO: consider a case when rollback function fails +} + +func createSecret(name, namespace string) *v1.Secret { + return &v1.Secret{ + ObjectMeta: meta.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + TypeMeta: meta.TypeMeta{ + Kind: "Secret", + APIVersion: "v1", + }, + } +} diff --git a/tests/test/compass-runtime-agent/testkit/init/certificatessecrets.go b/tests/test/compass-runtime-agent/testkit/init/certificatessecrets.go new file mode 100644 index 00000000..3141ca9c --- /dev/null +++ b/tests/test/compass-runtime-agent/testkit/init/certificatessecrets.go @@ -0,0 +1,41 @@ +package init + +import ( + "github.com/hashicorp/go-multierror" + "github.com/kyma-project/kyma/tests/components/application-connector/test/compass-runtime-agent/testkit/init/types" + "k8s.io/client-go/kubernetes" +) + +type certificatesSecretsConfigurator struct { + kubernetesInterface kubernetes.Interface +} + +func NewCertificateSecretConfigurator(kubernetesInterface kubernetes.Interface) certificatesSecretsConfigurator { + return certificatesSecretsConfigurator{ + kubernetesInterface: kubernetesInterface, + } +} + +func (csc certificatesSecretsConfigurator) Do(newCASecretName, newClusterCertSecretName string) (types.RollbackFunc, error) { + // Original secrets created by Compass Runtime Agent are left intact so that they can be restored after the test. + // As part of the test preparation new secret names are passed to the Compass Runtime Agent Deployment. Rollback function needs to delete those. + return csc.getRollbackFunction(newCASecretName, newClusterCertSecretName), nil +} + +func (csc certificatesSecretsConfigurator) getRollbackFunction(caSecretName, clusterCertSecretName string) types.RollbackFunc { + return func() error { + var result *multierror.Error + + err := deleteSecretWithRetry(csc.kubernetesInterface, caSecretName, IstioSystemNamespace) + if err != nil { + multierror.Append(result, err) + } + + err = deleteSecretWithRetry(csc.kubernetesInterface, clusterCertSecretName, CompassSystemNamespace) + if err != nil { + multierror.Append(result, err) + } + + return result.ErrorOrNil() + } +} diff --git a/tests/test/compass-runtime-agent/testkit/init/compass.go b/tests/test/compass-runtime-agent/testkit/init/compass.go new file mode 100644 index 00000000..454b85bc --- /dev/null +++ b/tests/test/compass-runtime-agent/testkit/init/compass.go @@ -0,0 +1,53 @@ +package init + +import ( + "github.com/kyma-project/kyma/tests/components/application-connector/test/compass-runtime-agent/testkit/init/types" +) + +type compassconfigurator struct { + directorClient types.DirectorClient + tenant string +} + +func NewCompassConfigurator(directorClient types.DirectorClient, tenant string) compassconfigurator { + return compassconfigurator{ + directorClient: directorClient, + tenant: tenant, + } +} + +func (cc compassconfigurator) Do(runtimeName, formationName string) (types.CompassRuntimeAgentConfig, types.RollbackFunc, error) { + runtimeID, err := cc.directorClient.RegisterRuntime(runtimeName) + if err != nil { + return types.CompassRuntimeAgentConfig{}, nil, err + } + + unregisterRuntimeRollbackFunc := func() error { return cc.directorClient.UnregisterRuntime(runtimeID) } + + err = cc.directorClient.RegisterFormation(formationName) + if err != nil { + return types.CompassRuntimeAgentConfig{}, unregisterRuntimeRollbackFunc, err + } + + unregisterFormationRollbackFunc := func() error { return cc.directorClient.UnregisterFormation(formationName) } + rollBackFunc := newRollbackFunc(unregisterRuntimeRollbackFunc, unregisterFormationRollbackFunc) + + err = cc.directorClient.AssignRuntimeToFormation(runtimeID, formationName) + if err != nil { + return types.CompassRuntimeAgentConfig{}, rollBackFunc, err + } + + token, compassConnectorUrl, err := cc.directorClient.GetConnectionToken(runtimeID) + if err != nil { + return types.CompassRuntimeAgentConfig{}, rollBackFunc, err + } + + config := types.CompassRuntimeAgentConfig{ + ConnectorUrl: compassConnectorUrl, + RuntimeID: runtimeID, + Token: token, + Tenant: cc.tenant, + } + + return config, rollBackFunc, nil +} diff --git a/tests/test/compass-runtime-agent/testkit/init/compass_test.go b/tests/test/compass-runtime-agent/testkit/init/compass_test.go new file mode 100644 index 00000000..d47ca08d --- /dev/null +++ b/tests/test/compass-runtime-agent/testkit/init/compass_test.go @@ -0,0 +1,159 @@ +package init + +import ( + "github.com/kyma-project/kyma/tests/components/application-connector/test/compass-runtime-agent/testkit/init/types" + "github.com/kyma-project/kyma/tests/components/application-connector/test/compass-runtime-agent/testkit/init/types/mocks" + "github.com/pkg/errors" + "github.com/stretchr/testify/require" + "testing" +) + +func TestCompassConfigurator(t *testing.T) { + runtimeName := "runtime" + runtimeID := "runtimeID" + formationName := "formation" + connectionToken := "token" + connectorURL := "connector.com" + tenant := "tenant" + + t.Run("should register Runtime, Formation and get connection token", func(t *testing.T) { + // given + directorClientMock := &mocks.DirectorClient{} + directorClientMock.On("RegisterRuntime", runtimeName).Return(runtimeID, nil) + directorClientMock.On("RegisterFormation", formationName).Return(nil) + + directorClientMock.On("UnregisterRuntime", runtimeID).Return(nil) + directorClientMock.On("UnregisterFormation", formationName).Return(nil) + + directorClientMock.On("GetConnectionToken", runtimeID).Return(connectionToken, connectorURL, nil) + directorClientMock.On("AssignRuntimeToFormation", runtimeID, formationName).Return(nil) + + // when + compassConfigurator := NewCompassConfigurator(directorClientMock, tenant) + require.NotNil(t, compassConfigurator) + + compassRuntimeAgentConfig, rollbackFunc, err := compassConfigurator.Do(runtimeName, formationName) + + // then + require.NotNil(t, rollbackFunc) + require.NoError(t, err) + require.Equal(t, runtimeID, compassRuntimeAgentConfig.RuntimeID) + require.Equal(t, tenant, compassRuntimeAgentConfig.Tenant) + require.Equal(t, connectionToken, compassRuntimeAgentConfig.Token) + require.Equal(t, connectorURL, compassRuntimeAgentConfig.ConnectorUrl) + + // when + err = rollbackFunc() + + // then + require.NoError(t, err) + directorClientMock.AssertExpectations(t) + }) + + t.Run("should fail when failed to register Runtime", func(t *testing.T) { + // given + directorClientMock := &mocks.DirectorClient{} + directorClientMock.On("RegisterRuntime", runtimeName).Return(runtimeID, errors.New("some error")) + + // when + compassConfigurator := NewCompassConfigurator(directorClientMock, tenant) + require.NotNil(t, compassConfigurator) + + compassRuntimeAgentConfig, rollbackFunc, err := compassConfigurator.Do(runtimeName, formationName) + + // then + require.Equal(t, types.CompassRuntimeAgentConfig{}, compassRuntimeAgentConfig) + require.Nil(t, rollbackFunc) + require.Error(t, err) + directorClientMock.AssertExpectations(t) + }) + + t.Run("should fail when failed to register Formation", func(t *testing.T) { + // given + directorClientMock := &mocks.DirectorClient{} + directorClientMock.On("RegisterRuntime", runtimeName).Return(runtimeID, nil) + directorClientMock.On("RegisterFormation", formationName).Return(errors.New("some error")) + directorClientMock.On("UnregisterRuntime", runtimeID).Return(nil) + + // when + compassConfigurator := NewCompassConfigurator(directorClientMock, tenant) + require.NotNil(t, compassConfigurator) + + compassRuntimeAgentConfig, rollbackFunc, err := compassConfigurator.Do(runtimeName, formationName) + + // then + require.Equal(t, types.CompassRuntimeAgentConfig{}, compassRuntimeAgentConfig) + require.NotNil(t, rollbackFunc) + require.Error(t, err) + + // when + err = rollbackFunc() + + // then + require.NoError(t, err) + directorClientMock.AssertExpectations(t) + }) + + t.Run("should fail when failed to assign Runtime to Formation", func(t *testing.T) { + // given + directorClientMock := &mocks.DirectorClient{} + directorClientMock.On("RegisterRuntime", runtimeName).Return(runtimeID, nil) + directorClientMock.On("RegisterFormation", formationName).Return(nil) + + directorClientMock.On("UnregisterRuntime", runtimeID).Return(nil) + directorClientMock.On("UnregisterFormation", formationName).Return(nil) + + directorClientMock.On("AssignRuntimeToFormation", runtimeID, formationName).Return(errors.New("some error")) + + // when + compassConfigurator := NewCompassConfigurator(directorClientMock, tenant) + require.NotNil(t, compassConfigurator) + + compassRuntimeAgentConfig, rollbackFunc, err := compassConfigurator.Do(runtimeName, formationName) + + // then + require.NotNil(t, compassConfigurator) + require.Equal(t, types.CompassRuntimeAgentConfig{}, compassRuntimeAgentConfig) + require.NotNil(t, rollbackFunc) + require.Error(t, err) + + // when + err = rollbackFunc() + + // then + require.NoError(t, err) + directorClientMock.AssertExpectations(t) + }) + + t.Run("should fail when failed to get connection token", func(t *testing.T) { + // given + directorClientMock := &mocks.DirectorClient{} + directorClientMock.On("RegisterRuntime", runtimeName).Return(runtimeID, nil) + directorClientMock.On("RegisterFormation", formationName).Return(nil) + + directorClientMock.On("UnregisterRuntime", runtimeID).Return(nil) + directorClientMock.On("UnregisterFormation", formationName).Return(nil) + + directorClientMock.On("AssignRuntimeToFormation", runtimeID, formationName).Return(nil) + directorClientMock.On("GetConnectionToken", runtimeID).Return("", "", errors.New("some error")) + + // when + compassConfigurator := NewCompassConfigurator(directorClientMock, tenant) + require.NotNil(t, compassConfigurator) + + compassRuntimeAgentConfig, rollbackFunc, err := compassConfigurator.Do(runtimeName, formationName) + + // then + require.NotNil(t, compassConfigurator) + require.Equal(t, types.CompassRuntimeAgentConfig{}, compassRuntimeAgentConfig) + require.NotNil(t, rollbackFunc) + require.Error(t, err) + + // when + err = rollbackFunc() + + // then + require.NoError(t, err) + directorClientMock.AssertExpectations(t) + }) +} diff --git a/tests/test/compass-runtime-agent/testkit/init/compassconnection.go b/tests/test/compass-runtime-agent/testkit/init/compassconnection.go new file mode 100644 index 00000000..41d2885e --- /dev/null +++ b/tests/test/compass-runtime-agent/testkit/init/compassconnection.go @@ -0,0 +1,114 @@ +package init + +import ( + "context" + "github.com/avast/retry-go" + "github.com/kyma-project/kyma/components/compass-runtime-agent/pkg/apis/compass/v1alpha1" + "github.com/kyma-project/kyma/tests/components/application-connector/test/compass-runtime-agent/testkit/init/types" + "github.com/pkg/errors" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type compassConnectionCRConfiguration struct { + compassConnectionInterface CompassConnectionInterface +} + +const ( + ConnectionCRName = "compass-connection" + ConnectionBackupCRName = "compass-connection-backup" +) + +//go:generate mockery --name=CompassConnectionInterface +type CompassConnectionInterface interface { + Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha1.CompassConnection, error) + Create(ctx context.Context, compassConnection *v1alpha1.CompassConnection, opts v1.CreateOptions) (*v1alpha1.CompassConnection, error) + Update(ctx context.Context, compassConnection *v1alpha1.CompassConnection, opts v1.UpdateOptions) (*v1alpha1.CompassConnection, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error +} + +func NewCompassConnectionCRConfiguration(compassConnectionInterface CompassConnectionInterface) compassConnectionCRConfiguration { + return compassConnectionCRConfiguration{ + compassConnectionInterface: compassConnectionInterface, + } +} + +func (cc compassConnectionCRConfiguration) Do() (types.RollbackFunc, error) { + + backupRollbackFunc, err := cc.backup() + if err != nil { + return nil, err + } + + deleteRollbackFunc, err := cc.delete() + if err != nil { + return backupRollbackFunc, err + } + + return newRollbackFunc(deleteRollbackFunc, backupRollbackFunc), nil +} +func (cc compassConnectionCRConfiguration) backup() (types.RollbackFunc, error) { + compassConnectionCR, err := cc.compassConnectionInterface.Get(context.TODO(), ConnectionCRName, meta.GetOptions{}) + if err != nil { + return nil, errors.Wrap(err, "failed to get Compass Connection CR") + } + + compassConnectionCR.ResourceVersion = "" + + compassConnectionCRBackup := compassConnectionCR.DeepCopy() + compassConnectionCRBackup.ObjectMeta.Name = "compass-connection-backup" + _, err = cc.compassConnectionInterface.Create(context.TODO(), compassConnectionCRBackup, meta.CreateOptions{}) + if err != nil { + return nil, errors.Wrap(err, "failed to create Compass Connection CR") + } + + rollbackFunc := func() error { + return retry.Do(func() error { + err = cc.compassConnectionInterface.Delete(context.TODO(), "compass-connection-backup", meta.DeleteOptions{}) + if err != nil { + if k8serrors.IsNotFound(err) { + return nil + } + return errors.Wrap(err, "failed to delete Compass Connection CR") + } + + return nil + }) + } + + return rollbackFunc, nil +} + +func (cc compassConnectionCRConfiguration) delete() (types.RollbackFunc, error) { + err := cc.compassConnectionInterface.Delete(context.TODO(), ConnectionCRName, meta.DeleteOptions{}) + + if err != nil { + return nil, errors.Wrap(err, "failed to delete Compass Connection CR") + } + + rollbackFunc := func() error { + return retry.Do(func() error { + restoredCompassConnection, err := cc.compassConnectionInterface.Get(context.TODO(), ConnectionCRName, meta.GetOptions{}) + if err != nil { + return err + } + + compassConnectionCRBackup, err := cc.compassConnectionInterface.Get(context.TODO(), ConnectionBackupCRName, meta.GetOptions{}) + if err != nil { + return err + } + + restoredCompassConnection.Spec = compassConnectionCRBackup.Spec + restoredCompassConnection.Status = compassConnectionCRBackup.Status + + _, err = cc.compassConnectionInterface.Update(context.TODO(), restoredCompassConnection, meta.UpdateOptions{}) + if err != nil { + return errors.Wrap(err, "failed to update Compass Connection CR") + } + return err + }) + } + + return rollbackFunc, nil +} diff --git a/tests/test/compass-runtime-agent/testkit/init/compassconnection_test.go b/tests/test/compass-runtime-agent/testkit/init/compassconnection_test.go new file mode 100644 index 00000000..36ec6ee1 --- /dev/null +++ b/tests/test/compass-runtime-agent/testkit/init/compassconnection_test.go @@ -0,0 +1,139 @@ +package init + +import ( + "context" + "github.com/kyma-project/kyma/components/compass-runtime-agent/pkg/apis/compass/v1alpha1" + "github.com/kyma-project/kyma/components/compass-runtime-agent/pkg/client/clientset/versioned/fake" + "github.com/stretchr/testify/require" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + "testing" +) + +func TestCompassConnectionConfigurator(t *testing.T) { + t.Run("should delete CompassConnection CR and restore it when RollbackFunction is called", func(t *testing.T) { + // given + compassConnectionCRFake := fake.NewSimpleClientset().CompassV1alpha1().CompassConnections() + compassConnection := &v1alpha1.CompassConnection{ + ObjectMeta: meta.ObjectMeta{ + Name: ConnectionCRName, + }, + TypeMeta: meta.TypeMeta{ + Kind: "CompassConnection", + APIVersion: "v1alpha", + }, + } + + _, err := compassConnectionCRFake.Create(context.TODO(), compassConnection, meta.CreateOptions{}) + require.NoError(t, err) + + // when + configurator := NewCompassConnectionCRConfiguration(compassConnectionCRFake) + rollbackFunc, err := configurator.Do() + + // then + require.NoError(t, err) + _, err = compassConnectionCRFake.Get(context.TODO(), "compass-connection", meta.GetOptions{}) + require.Error(t, err) + require.True(t, k8serrors.IsNotFound(err)) + + _, err = compassConnectionCRFake.Get(context.TODO(), "compass-connection-backup", meta.GetOptions{}) + require.NoError(t, err) + + _, err = compassConnectionCRFake.Create(context.TODO(), compassConnection, meta.CreateOptions{}) + require.NoError(t, err) + // when + err = rollbackFunc() + + // then + require.NoError(t, err) + _, err = compassConnectionCRFake.Get(context.TODO(), "compass-connection", meta.GetOptions{}) + require.NoError(t, err) + }) + + t.Run("should fail when CompassConnection CR doesn't exist", func(t *testing.T) { + // given + compassConnectionCRFake := fake.NewSimpleClientset().CompassV1alpha1().CompassConnections() + + // when + configurator := NewCompassConnectionCRConfiguration(compassConnectionCRFake) + _, err := configurator.Do() + + // then + require.Error(t, err) + }) + + t.Run("should fail when CompassConnection CR backup already exist", func(t *testing.T) { + // given + compassConnectionCRFake := fake.NewSimpleClientset().CompassV1alpha1().CompassConnections() + compassConnection := &v1alpha1.CompassConnection{ + ObjectMeta: meta.ObjectMeta{ + Name: ConnectionCRName, + }, + TypeMeta: meta.TypeMeta{ + Kind: "CompassConnection", + APIVersion: "v1alpha", + }, + } + + compassConnectionBackup := &v1alpha1.CompassConnection{ + ObjectMeta: meta.ObjectMeta{ + Name: ConnectionBackupCRName, + }, + TypeMeta: meta.TypeMeta{ + Kind: "CompassConnection", + APIVersion: "v1alpha", + }, + } + + _, err := compassConnectionCRFake.Create(context.TODO(), compassConnection, meta.CreateOptions{}) + require.NoError(t, err) + + _, err = compassConnectionCRFake.Create(context.TODO(), compassConnectionBackup, meta.CreateOptions{}) + require.NoError(t, err) + + // when + configurator := NewCompassConnectionCRConfiguration(compassConnectionCRFake) + rollbackFunc, err := configurator.Do() + + // then + require.Nil(t, rollbackFunc) + require.Error(t, err) + }) + + t.Run("rollback function should fail when CompassConnection CR backup doesn't exist", func(t *testing.T) { + // given + compassConnectionCRFake := fake.NewSimpleClientset().CompassV1alpha1().CompassConnections() + compassConnection := &v1alpha1.CompassConnection{ + ObjectMeta: meta.ObjectMeta{ + Name: ConnectionCRName, + }, + TypeMeta: meta.TypeMeta{ + Kind: "CompassConnection", + APIVersion: "v1alpha", + }, + } + + _, err := compassConnectionCRFake.Create(context.TODO(), compassConnection, meta.CreateOptions{}) + require.NoError(t, err) + + // when + configurator := NewCompassConnectionCRConfiguration(compassConnectionCRFake) + rollbackFunc, err := configurator.Do() + + // then + require.NoError(t, err) + + // when + _, err = compassConnectionCRFake.Create(context.TODO(), compassConnection, meta.CreateOptions{}) + require.NoError(t, err) + + err = compassConnectionCRFake.Delete(context.TODO(), ConnectionBackupCRName, meta.DeleteOptions{}) + require.NoError(t, err) + + err = rollbackFunc() + + // then + require.Error(t, err) + }) +} diff --git a/tests/test/compass-runtime-agent/testkit/init/configurationsecret.go b/tests/test/compass-runtime-agent/testkit/init/configurationsecret.go new file mode 100644 index 00000000..eaa6b04a --- /dev/null +++ b/tests/test/compass-runtime-agent/testkit/init/configurationsecret.go @@ -0,0 +1,83 @@ +package init + +import ( + "context" + "github.com/avast/retry-go" + "github.com/kyma-project/kyma/tests/components/application-connector/test/compass-runtime-agent/testkit/init/types" + "github.com/pkg/errors" + v1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "time" +) + +const ( + connectorURLConfigKey = "CONNECTOR_URL" + tokenConfigKey = "TOKEN" + runtimeIdConfigKey = "RUNTIME_ID" + tenantConfigKey = "TENANT" +) + +type configurationSecretConfigurator struct { + kubernetesInterface kubernetes.Interface +} + +func NewConfigurationSecretConfigurator(kubernetesInterface kubernetes.Interface) configurationSecretConfigurator { + return configurationSecretConfigurator{ + kubernetesInterface: kubernetesInterface, + } +} + +func (s configurationSecretConfigurator) Do(newConfigSecretName string, config types.CompassRuntimeAgentConfig) (types.RollbackFunc, error) { + + secret := v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: newConfigSecretName, + Namespace: CompassSystemNamespace, + }, + Data: map[string][]byte{ + connectorURLConfigKey: []byte(config.ConnectorUrl), + tokenConfigKey: []byte(config.Token), + runtimeIdConfigKey: []byte(config.RuntimeID), + tenantConfigKey: []byte(config.Tenant), + }, + } + + err := retry.Do(func() error { + _, err := s.kubernetesInterface.CoreV1().Secrets(CompassSystemNamespace).Create(context.Background(), &secret, metav1.CreateOptions{}) + if err != nil { + if k8serrors.IsAlreadyExists(err) { + return retry.Unrecoverable(err) + } + return errors.Wrap(err, "failed to create configuration secret") + } + + return nil + }, retry.Attempts(RetryAttempts), retry.Delay(RetrySeconds*time.Second)) + + if err != nil { + return nil, err + } + + return s.newRollbackSecretFunc(newConfigSecretName, CompassSystemNamespace), nil +} + +func (s configurationSecretConfigurator) newRollbackSecretFunc(name, namespace string) types.RollbackFunc { + return func() error { + return deleteSecretWithRetry(s.kubernetesInterface, name, namespace) + } +} + +func deleteSecretWithRetry(kubernetesInterface kubernetes.Interface, name, namespace string) error { + return retry.Do(func() error { + err := kubernetesInterface.CoreV1().Secrets(namespace).Delete(context.Background(), name, metav1.DeleteOptions{}) + if err != nil { + if k8serrors.IsNotFound(err) { + return nil + } + } + + return errors.Wrap(err, "failed to delete secret") + }, retry.Attempts(RetryAttempts), retry.Delay(RetrySeconds*time.Second)) +} diff --git a/tests/test/compass-runtime-agent/testkit/init/configurationsecret_test.go b/tests/test/compass-runtime-agent/testkit/init/configurationsecret_test.go new file mode 100644 index 00000000..b3fd06fb --- /dev/null +++ b/tests/test/compass-runtime-agent/testkit/init/configurationsecret_test.go @@ -0,0 +1,73 @@ +package init + +import ( + "context" + "github.com/kyma-project/kyma/tests/components/application-connector/test/compass-runtime-agent/testkit/init/types" + "github.com/stretchr/testify/require" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" + "testing" +) + +func TestConfigurationSecret(t *testing.T) { + t.Run("should create configuration secret", func(t *testing.T) { + // given + fakeKubernetesInterface := fake.NewSimpleClientset() + secretConfigurator := NewConfigurationSecretConfigurator(fakeKubernetesInterface) + connectorURL := "www.example.com" + runtimeID := "runtimeID" + token := "token" + tenant := "tenant" + + config := types.CompassRuntimeAgentConfig{ + ConnectorUrl: connectorURL, + RuntimeID: runtimeID, + Token: token, + Tenant: tenant, + } + secretName := "config" + + // when + rollbackFunc, err := secretConfigurator.Do(secretName, config) + require.NotNil(t, rollbackFunc) + require.NoError(t, err) + + // then + secret, err := fakeKubernetesInterface.CoreV1().Secrets(CompassSystemNamespace).Get(context.TODO(), secretName, meta.GetOptions{}) + require.NoError(t, err) + + require.Equal(t, connectorURL, string(secret.Data[connectorURLConfigKey])) + require.Equal(t, token, string(secret.Data[tokenConfigKey])) + require.Equal(t, runtimeID, string(secret.Data[runtimeIdConfigKey])) + require.Equal(t, tenant, string(secret.Data[tenantConfigKey])) + + // when + err = rollbackFunc() + require.NoError(t, err) + + _, err = fakeKubernetesInterface.CoreV1().Secrets(CompassSystemNamespace).Get(context.TODO(), secretName, meta.GetOptions{}) + require.Error(t, err) + require.True(t, k8serrors.IsNotFound(err)) + }) + + t.Run("should return error when failed to create secret", func(t *testing.T) { + // given + fakeKubernetesInterface := fake.NewSimpleClientset() + secretConfigurator := NewConfigurationSecretConfigurator(fakeKubernetesInterface) + + config := types.CompassRuntimeAgentConfig{} + secretName := "config" + + // when + secret := createSecret(secretName, CompassSystemNamespace) + _, err := fakeKubernetesInterface.CoreV1().Secrets(CompassSystemNamespace).Create(context.Background(), secret, meta.CreateOptions{}) + require.NoError(t, err) + + rollbackFunc, err := secretConfigurator.Do(secretName, config) + + // then + require.Nil(t, rollbackFunc) + require.Error(t, err) + }) +} diff --git a/tests/test/compass-runtime-agent/testkit/init/deployment.go b/tests/test/compass-runtime-agent/testkit/init/deployment.go new file mode 100644 index 00000000..ffe11af0 --- /dev/null +++ b/tests/test/compass-runtime-agent/testkit/init/deployment.go @@ -0,0 +1,160 @@ +package init + +import ( + "context" + "fmt" + "github.com/avast/retry-go" + "github.com/kyma-project/kyma/tests/components/application-connector/test/compass-runtime-agent/testkit/init/types" + "github.com/pkg/errors" + v12 "k8s.io/api/apps/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + v13 "k8s.io/client-go/kubernetes/typed/apps/v1" + "time" +) + +const ( + CRAContainerNumber = 0 + ConfigurationSecretEnvName = "APP_AGENT_CONFIGURATION_SECRET" + CASecretEnvName = "APP_CA_CERTIFICATES_SECRET" + ClusterCertSecretEnvName = "APP_CLUSTER_CERTIFICATES_SECRET" + ControllerSyncPeriodEnvTime = "APP_CONTROLLER_SYNC_PERIOD" +) + +type deploymentConfiguration struct { + kubernetesInterface kubernetes.Interface + deploymentName string + namespaceName string +} + +func NewDeploymentConfiguration(kubernetesInterface kubernetes.Interface, deploymentName, namespaceName string) deploymentConfiguration { + return deploymentConfiguration{ + kubernetesInterface: kubernetesInterface, + deploymentName: deploymentName, + namespaceName: namespaceName, + } +} + +func (dc deploymentConfiguration) Do(newCANamespacedSecretName, newClusterNamespacedCertSecretName, newConfigNamespacedSecretName, newControllerSyncPeriodTime string) (types.RollbackFunc, error) { + deploymentInterface := dc.kubernetesInterface.AppsV1().Deployments(dc.namespaceName) + + deployment, err := retryGetDeployment(dc.deploymentName, deploymentInterface) + if err != nil { + return nil, err + } + + if len(deployment.Spec.Template.Spec.Containers) < 1 { + return nil, fmt.Errorf("no containers found in %s/%s deployment", "kyma-system", dc.deploymentName) + } + + previousConfigSecretNamespacedName, found := replaceEnvValue(deployment, ConfigurationSecretEnvName, newConfigNamespacedSecretName) + if !found { + return nil, fmt.Errorf("environment variable '%s' not found in %s deployment", ConfigurationSecretEnvName, dc.deploymentName) + } + + previousCASecretNamespacedName, found := replaceEnvValue(deployment, CASecretEnvName, newCANamespacedSecretName) + if !found { + return nil, fmt.Errorf("environment variable '%s' not found in %s deployment", CASecretEnvName, dc.deploymentName) + } + + previousCertSecretNamespacedName, found := replaceEnvValue(deployment, ClusterCertSecretEnvName, newClusterNamespacedCertSecretName) + if !found { + return nil, fmt.Errorf("environment variable '%s' not found in %s deployment", ClusterCertSecretEnvName, dc.deploymentName) + } + + previousControllerSyncPeriodTime, found := replaceEnvValue(deployment, ControllerSyncPeriodEnvTime, newControllerSyncPeriodTime) + if !found { + return nil, fmt.Errorf("environment variable '%s' not found in %s deployment", ControllerSyncPeriodEnvTime, dc.deploymentName) + } + + err = retryUpdateDeployment(deployment, deploymentInterface) + if err != nil { + return nil, err + } + rollbackDeploymentFunc := newRollbackDeploymentFunc(dc.deploymentName, previousConfigSecretNamespacedName, previousCASecretNamespacedName, previousCertSecretNamespacedName, previousControllerSyncPeriodTime, deploymentInterface) + + err = waitForRollout(dc.deploymentName, deploymentInterface) + + return rollbackDeploymentFunc, err +} + +func newRollbackDeploymentFunc(name, previousConfigSecretNamespacedName, previousCASecretNamespacedName, previousCertSecretNamespacedName, previousControllerSyncPeriodTime string, deploymentInterface v13.DeploymentInterface) types.RollbackFunc { + return func() error { + deployment, err := retryGetDeployment(name, deploymentInterface) + if err != nil { + return err + } + + _, found := replaceEnvValue(deployment, ConfigurationSecretEnvName, previousConfigSecretNamespacedName) + if !found { + return fmt.Errorf("environment variable '%s' not found in %s deployment", ConfigurationSecretEnvName, name) + } + + _, found = replaceEnvValue(deployment, CASecretEnvName, previousCASecretNamespacedName) + if !found { + return fmt.Errorf("environment variable '%s' not found in %s deployment", CASecretEnvName, name) + } + + _, found = replaceEnvValue(deployment, ClusterCertSecretEnvName, previousCertSecretNamespacedName) + if !found { + return fmt.Errorf("environment variable '%s' not found in %s deployment", ClusterCertSecretEnvName, name) + } + + _, found = replaceEnvValue(deployment, ControllerSyncPeriodEnvTime, previousControllerSyncPeriodTime) + if !found { + return fmt.Errorf("environment variable '%s' not found in %s deployment", ControllerSyncPeriodEnvTime, name) + } + + return retryUpdateDeployment(deployment, deploymentInterface) + } +} + +func replaceEnvValue(deployment *v12.Deployment, name, newValue string) (string, bool) { + envs := deployment.Spec.Template.Spec.Containers[CRAContainerNumber].Env + for i := range envs { + if envs[i].Name == name { + previousValue := envs[i].Value + envs[i].Value = newValue + deployment.Spec.Template.Spec.Containers[CRAContainerNumber].Env = envs + + return previousValue, true + } + } + + return "", false +} + +func retryGetDeployment(name string, deploymentInterface v13.DeploymentInterface) (*v12.Deployment, error) { + var deployment *v12.Deployment + + err := retry.Do(func() error { + var err error + deployment, err = deploymentInterface.Get(context.TODO(), name, v1.GetOptions{}) + if err != nil { + return errors.Wrap(err, "failed to get Compass Runtime Agent deployment") + } + return nil + }, retry.Attempts(RetryAttempts), retry.Delay(RetrySeconds*time.Second)) + + return deployment, err +} + +func retryUpdateDeployment(deployment *v12.Deployment, deploymentInterface v13.DeploymentInterface) error { + return retry.Do(func() error { + _, err := deploymentInterface.Update(context.TODO(), deployment, v1.UpdateOptions{}) + return errors.Wrap(err, "failed to update Compass Runtime Agent deployment") + }, retry.Attempts(RetryAttempts), retry.Delay(RetrySeconds*time.Second)) +} + +func waitForRollout(name string, deploymentInterface v13.DeploymentInterface) error { + return retry.Do(func() error { + deployment, err := deploymentInterface.Get(context.TODO(), name, v1.GetOptions{}) + if err != nil { + return errors.Wrap(err, "failed to get Compass Runtime Agent deployment") + } + if deployment.Status.AvailableReplicas == 0 || deployment.Status.UnavailableReplicas != 0 { + return fmt.Errorf("deployment %s is not yet ready", name) + } + return nil + }, retry.Attempts(RetryAttempts), retry.Delay(RetrySeconds*time.Second)) +} diff --git a/tests/test/compass-runtime-agent/testkit/init/init.go b/tests/test/compass-runtime-agent/testkit/init/init.go new file mode 100644 index 00000000..68b397c8 --- /dev/null +++ b/tests/test/compass-runtime-agent/testkit/init/init.go @@ -0,0 +1,140 @@ +package init + +import ( + "fmt" + "github.com/hashicorp/go-multierror" + "github.com/kyma-project/kyma/tests/components/application-connector/test/compass-runtime-agent/testkit/init/types" + log "github.com/sirupsen/logrus" +) + +const ( + CompassSystemNamespace = "kyma-system" + IstioSystemNamespace = "istio-system" + CompassRuntimeAgentDeployment = "compass-runtime-agent" + NewCompassRuntimeConfigName = "test-compass-runtime-agent-config" + NewCACertSecretName = "ca-cert-test" + NewClientCertSecretName = "client-cert-test" + NewControllerSyncPeriodTime = "15s" + RetryAttempts = 6 + RetrySeconds = 5 +) + +type CompassRuntimeAgentConfigurator interface { + Do(runtimeName, formationName string) (types.RollbackFunc, error) +} + +type Configurator interface { + Configure(runtimeName, formationName string) (types.RollbackFunc, error) +} + +type compassRuntimeAgentConfigurator struct { + compassConfigurator types.CompassConfigurator + certificateSecretConfigurator types.CertificateSecretConfigurator + configurationSecretConfigurator types.ConfigurationSecretConfigurator + compassConnectionConfigurator types.CompassConnectionConfigurator + deploymentConfigurator types.DeploymentConfigurator + testNamespace string +} + +func NewCompassRuntimeAgentConfigurator(compassConfigurator types.CompassConfigurator, + certificateSecretConfigurator types.CertificateSecretConfigurator, + configurationSecretConfigurator types.ConfigurationSecretConfigurator, + compassConnectionConfigurator types.CompassConnectionConfigurator, + deploymentConfigurator types.DeploymentConfigurator, + testNamespace string) CompassRuntimeAgentConfigurator { + return compassRuntimeAgentConfigurator{ + compassConfigurator: compassConfigurator, + certificateSecretConfigurator: certificateSecretConfigurator, + configurationSecretConfigurator: configurationSecretConfigurator, + compassConnectionConfigurator: compassConnectionConfigurator, + deploymentConfigurator: deploymentConfigurator, + testNamespace: testNamespace, + } +} + +func (crc compassRuntimeAgentConfigurator) Do(runtimeName, formationName string) (types.RollbackFunc, error) { + log.Info("Configuring Compass") + compassRuntimeAgentConfig, compassConfiguratorRollbackFunc, err := crc.compassConfigurator.Do(runtimeName, formationName) + if err != nil { + return nil, crc.rollbackOnError(err, + compassConfiguratorRollbackFunc) + } + + log.Info("Configuring certificate secrets") + certificateSecretsRollbackFunc, err := crc.certificateSecretConfigurator.Do(NewCACertSecretName, NewClientCertSecretName) + if err != nil { + return nil, crc.rollbackOnError(err, + compassConfiguratorRollbackFunc, + certificateSecretsRollbackFunc) + } + + log.Info("Preparing Compass Runtime Agent configuration secret") + configurationSecretRollbackFunc, err := crc.configurationSecretConfigurator.Do(NewCompassRuntimeConfigName, compassRuntimeAgentConfig) + if err != nil { + return nil, crc.rollbackOnError(err, + compassConfiguratorRollbackFunc, + certificateSecretsRollbackFunc, + configurationSecretRollbackFunc) + } + + newCACertNamespacedSecretName := fmt.Sprintf("%s/%s", IstioSystemNamespace, NewCACertSecretName) + newClientCertNamespacedSecretName := fmt.Sprintf("%s/%s", CompassSystemNamespace, NewClientCertSecretName) + newCompassRuntimeNamespacedSecretConfigName := fmt.Sprintf("%s/%s", CompassSystemNamespace, NewCompassRuntimeConfigName) + newControllerSyncPeriodTime := fmt.Sprintf("%s", NewControllerSyncPeriodTime) + + log.Info("Preparing Compass Runtime Agent configuration secret") + deploymentRollbackFunc, err := crc.deploymentConfigurator.Do(newCACertNamespacedSecretName, + newClientCertNamespacedSecretName, + newCompassRuntimeNamespacedSecretConfigName, newControllerSyncPeriodTime) + if err != nil { + return nil, crc.rollbackOnError(err, + compassConfiguratorRollbackFunc, + certificateSecretsRollbackFunc, + configurationSecretRollbackFunc, + deploymentRollbackFunc) + } + + compassConnectionRollbackFunc, err := crc.compassConnectionConfigurator.Do() + if err != nil { + return nil, crc.rollbackOnError(err, + compassConfiguratorRollbackFunc, + certificateSecretsRollbackFunc, + configurationSecretRollbackFunc, + deploymentRollbackFunc, + compassConnectionRollbackFunc) + } + + return newRollbackFunc(compassConfiguratorRollbackFunc, + certificateSecretsRollbackFunc, + configurationSecretRollbackFunc, + deploymentRollbackFunc, + compassConnectionRollbackFunc), nil +} + +func (crc compassRuntimeAgentConfigurator) rollbackOnError(initialErr error, rollbackFunctions ...types.RollbackFunc) error { + var result *multierror.Error + result = multierror.Append(result, initialErr) + + err := newRollbackFunc(rollbackFunctions...)() + if err != nil { + result = multierror.Append(result, err) + } + + return result.ErrorOrNil() +} + +func newRollbackFunc(rollbackFunctions ...types.RollbackFunc) types.RollbackFunc { + var result *multierror.Error + + return func() error { + for _, f := range rollbackFunctions { + if f != nil { + if err := f(); err != nil { + result = multierror.Append(result, err) + } + } + } + + return result.ErrorOrNil() + } +} diff --git a/tests/test/compass-runtime-agent/testkit/init/init_test.go b/tests/test/compass-runtime-agent/testkit/init/init_test.go new file mode 100644 index 00000000..9078c01e --- /dev/null +++ b/tests/test/compass-runtime-agent/testkit/init/init_test.go @@ -0,0 +1,232 @@ +package init + +import ( + "fmt" + "github.com/kyma-project/kyma/tests/components/application-connector/test/compass-runtime-agent/testkit/init/types" + "github.com/kyma-project/kyma/tests/components/application-connector/test/compass-runtime-agent/testkit/init/types/mocks" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" +) + +func TestCompassRuntimeAgentInit(t *testing.T) { + runtimeName := "newRuntime" + runtimeID := "runtimeID" + token := "token" + connectorURL := "www.someurl.com" + tenant := "tenant" + formationName := "newFormation" + + t.Run("should succeed and return rollback function", func(t *testing.T) { + // given + compassConfiguratorMock := &mocks.CompassConfigurator{} + certificateSecretConfiguratorMock := &mocks.CertificateSecretConfigurator{} + configurationSecretConfiguratorMock := &mocks.ConfigurationSecretConfigurator{} + compassConnectionConfiguratorMock := &mocks.CompassConnectionConfigurator{} + deploymentConfiguratorMock := &mocks.DeploymentConfigurator{} + + compassConfiguratorRollbackFunc := RollbackFuncTest{} + certificateSecretsRollbackFunc := RollbackFuncTest{} + configurationSecretRollbackFunc := RollbackFuncTest{} + compassConnectionRollbackFunc := RollbackFuncTest{} + deploymentRollbackFunc := RollbackFuncTest{} + + config := types.CompassRuntimeAgentConfig{ + ConnectorUrl: connectorURL, + RuntimeID: runtimeID, + Token: token, + Tenant: tenant, + } + + compassConfiguratorMock.On("Do", runtimeName, formationName).Return(config, compassConfiguratorRollbackFunc.Func(), nil) + certificateSecretConfiguratorMock.On("Do", NewCACertSecretName, NewClientCertSecretName).Return(certificateSecretsRollbackFunc.Func(), nil) + configurationSecretConfiguratorMock.On("Do", NewCompassRuntimeConfigName, config).Return(configurationSecretRollbackFunc.Func(), nil) + compassConnectionConfiguratorMock.On("Do").Return(compassConnectionRollbackFunc.Func(), nil) + deploymentConfiguratorMock.On("Do", + fmt.Sprintf("%s/%s", IstioSystemNamespace, NewCACertSecretName), + fmt.Sprintf("%s/%s", CompassSystemNamespace, NewClientCertSecretName), + fmt.Sprintf("%s/%s", CompassSystemNamespace, NewCompassRuntimeConfigName), + fmt.Sprintf("%s", NewControllerSyncPeriodTime)). + Return(deploymentRollbackFunc.Func(), nil) + + configurator := NewCompassRuntimeAgentConfigurator(compassConfiguratorMock, certificateSecretConfiguratorMock, configurationSecretConfiguratorMock, compassConnectionConfiguratorMock, deploymentConfiguratorMock, "tenant") + + // when + rollbackFunc, err := configurator.Do(runtimeName, formationName) + + // then + require.NoError(t, err) + certificateSecretConfiguratorMock.AssertExpectations(t) + compassConnectionConfiguratorMock.AssertExpectations(t) + deploymentConfiguratorMock.AssertExpectations(t) + + //when + err = rollbackFunc() + + // then + require.NoError(t, err) + require.True(t, compassConfiguratorRollbackFunc.invoked) + require.True(t, certificateSecretsRollbackFunc.invoked) + require.True(t, configurationSecretRollbackFunc.invoked) + require.True(t, compassConnectionRollbackFunc.invoked) + require.True(t, deploymentRollbackFunc.invoked) + }) + + t.Run("should fail if failed to register runtime", func(t *testing.T) { + // given + compassConfiguratorMock := &mocks.CompassConfigurator{} + compassConfiguratorRollbackFunc := RollbackFuncTest{} + + compassConfiguratorMock.On("Do", runtimeName, formationName).Return(types.CompassRuntimeAgentConfig{}, compassConfiguratorRollbackFunc.Func(), errors.New("some error")) + + configurator := NewCompassRuntimeAgentConfigurator(compassConfiguratorMock, nil, nil, nil, nil, "tenant") + + // when + rollbackFunc, err := configurator.Do(runtimeName, formationName) + + // then + require.Error(t, err) + require.Nil(t, rollbackFunc) + assert.True(t, compassConfiguratorRollbackFunc.invoked) + }) + + t.Run("should fail if failed to create configuration secret", func(t *testing.T) { + // given + compassConfiguratorMock := &mocks.CompassConfigurator{} + certificateSecretConfiguratorMock := &mocks.CertificateSecretConfigurator{} + configurationSecretConfiguratorMock := &mocks.ConfigurationSecretConfigurator{} + + compassConfiguratorRollbackFunc := RollbackFuncTest{} + certificateSecretsRollbackFunc := RollbackFuncTest{} + + config := types.CompassRuntimeAgentConfig{ + ConnectorUrl: connectorURL, + RuntimeID: runtimeID, + Token: token, + Tenant: tenant, + } + + compassConfiguratorMock.On("Do", runtimeName, formationName).Return(config, compassConfiguratorRollbackFunc.Func(), nil) + certificateSecretConfiguratorMock.On("Do", NewCACertSecretName, NewClientCertSecretName).Return(certificateSecretsRollbackFunc.Func(), nil) + configurationSecretConfiguratorMock.On("Do", NewCompassRuntimeConfigName, config).Return(nil, errors.New("some error")) + + configurator := NewCompassRuntimeAgentConfigurator(compassConfiguratorMock, certificateSecretConfiguratorMock, configurationSecretConfiguratorMock, nil, nil, "tenant") + + // when + rollbackFunc, err := configurator.Do(runtimeName, formationName) + + // then + require.Error(t, err) + require.Nil(t, rollbackFunc) + compassConfiguratorMock.AssertExpectations(t) + certificateSecretConfiguratorMock.AssertExpectations(t) + certificateSecretConfiguratorMock.AssertExpectations(t) + require.True(t, compassConfiguratorRollbackFunc.invoked) + require.True(t, certificateSecretsRollbackFunc.invoked) + }) + + t.Run("should fail if failed to modify deployment", func(t *testing.T) { + // given + compassConfiguratorMock := &mocks.CompassConfigurator{} + certificateSecretConfiguratorMock := &mocks.CertificateSecretConfigurator{} + configurationSecretConfiguratorMock := &mocks.ConfigurationSecretConfigurator{} + deploymentConfiguratorMock := &mocks.DeploymentConfigurator{} + + compassConfiguratorRollbackFunc := RollbackFuncTest{} + certificateSecretsRollbackFunc := RollbackFuncTest{} + configurationSecretRollbackFunc := RollbackFuncTest{} + + config := types.CompassRuntimeAgentConfig{ + ConnectorUrl: connectorURL, + RuntimeID: runtimeID, + Token: token, + Tenant: tenant, + } + + compassConfiguratorMock.On("Do", runtimeName, formationName).Return(config, compassConfiguratorRollbackFunc.Func(), nil) + certificateSecretConfiguratorMock.On("Do", NewCACertSecretName, NewClientCertSecretName).Return(certificateSecretsRollbackFunc.Func(), nil) + configurationSecretConfiguratorMock.On("Do", NewCompassRuntimeConfigName, config).Return(configurationSecretRollbackFunc.Func(), nil) + deploymentConfiguratorMock.On("Do", + fmt.Sprintf("%s/%s", IstioSystemNamespace, NewCACertSecretName), + fmt.Sprintf("%s/%s", CompassSystemNamespace, NewClientCertSecretName), + fmt.Sprintf("%s/%s", CompassSystemNamespace, NewCompassRuntimeConfigName), + fmt.Sprintf("%s", NewControllerSyncPeriodTime)). + Return(nil, errors.New("some error")) + + configurator := NewCompassRuntimeAgentConfigurator(compassConfiguratorMock, certificateSecretConfiguratorMock, configurationSecretConfiguratorMock, nil, deploymentConfiguratorMock, "tenant") + + // when + rollbackFunc, err := configurator.Do(runtimeName, formationName) + + // then + require.Error(t, err) + require.Nil(t, rollbackFunc) + certificateSecretConfiguratorMock.AssertExpectations(t) + deploymentConfiguratorMock.AssertExpectations(t) + require.True(t, compassConfiguratorRollbackFunc.invoked) + require.True(t, certificateSecretsRollbackFunc.invoked) + require.True(t, configurationSecretRollbackFunc.invoked) + }) + + t.Run("should fail if failed to configure Compass Connection CR", func(t *testing.T) { + // given + compassConfiguratorMock := &mocks.CompassConfigurator{} + certificateSecretConfiguratorMock := &mocks.CertificateSecretConfigurator{} + configurationSecretConfiguratorMock := &mocks.ConfigurationSecretConfigurator{} + compassConnectionConfiguratorMock := &mocks.CompassConnectionConfigurator{} + deploymentConfiguratorMock := &mocks.DeploymentConfigurator{} + + compassConfiguratorRollbackFunc := RollbackFuncTest{} + certificateSecretsRollbackFunc := RollbackFuncTest{} + configurationSecretRollbackFunc := RollbackFuncTest{} + compassConnectionRollbackFunc := RollbackFuncTest{} + deploymentRollbackFunc := RollbackFuncTest{} + + config := types.CompassRuntimeAgentConfig{ + ConnectorUrl: connectorURL, + RuntimeID: runtimeID, + Token: token, + Tenant: tenant, + } + + compassConfiguratorMock.On("Do", runtimeName, formationName).Return(config, compassConfiguratorRollbackFunc.Func(), nil) + certificateSecretConfiguratorMock.On("Do", NewCACertSecretName, NewClientCertSecretName).Return(certificateSecretsRollbackFunc.Func(), nil) + configurationSecretConfiguratorMock.On("Do", NewCompassRuntimeConfigName, config).Return(configurationSecretRollbackFunc.Func(), nil) + compassConnectionConfiguratorMock.On("Do").Return(compassConnectionRollbackFunc.Func(), errors.New("some error")) + deploymentConfiguratorMock.On("Do", + fmt.Sprintf("%s/%s", IstioSystemNamespace, NewCACertSecretName), + fmt.Sprintf("%s/%s", CompassSystemNamespace, NewClientCertSecretName), + fmt.Sprintf("%s/%s", CompassSystemNamespace, NewCompassRuntimeConfigName), + fmt.Sprintf("%s", NewControllerSyncPeriodTime)). + Return(deploymentRollbackFunc.Func(), nil) + + configurator := NewCompassRuntimeAgentConfigurator(compassConfiguratorMock, certificateSecretConfiguratorMock, configurationSecretConfiguratorMock, compassConnectionConfiguratorMock, deploymentConfiguratorMock, "tenant") + + // when + rollbackFunc, err := configurator.Do(runtimeName, formationName) + + // then + require.Error(t, err) + require.Nil(t, rollbackFunc) + certificateSecretConfiguratorMock.AssertExpectations(t) + compassConnectionConfiguratorMock.AssertExpectations(t) + deploymentConfiguratorMock.AssertExpectations(t) + require.True(t, compassConfiguratorRollbackFunc.invoked) + require.True(t, certificateSecretsRollbackFunc.invoked) + require.True(t, configurationSecretRollbackFunc.invoked) + //require.True(t, compassConnectionRollbackFunc.invoked) + require.True(t, deploymentRollbackFunc.invoked) + }) +} + +type RollbackFuncTest struct { + invoked bool +} + +func (rfc *RollbackFuncTest) Func() types.RollbackFunc { + return func() error { + rfc.invoked = true + return nil + } +} diff --git a/tests/test/compass-runtime-agent/testkit/init/types/mocks/CertificateSecretConfigurator.go b/tests/test/compass-runtime-agent/testkit/init/types/mocks/CertificateSecretConfigurator.go new file mode 100644 index 00000000..3130678b --- /dev/null +++ b/tests/test/compass-runtime-agent/testkit/init/types/mocks/CertificateSecretConfigurator.go @@ -0,0 +1,51 @@ +// Code generated by mockery v2.15.0. DO NOT EDIT. + +package mocks + +import ( + types "github.com/kyma-project/kyma/tests/components/application-connector/test/compass-runtime-agent/testkit/init/types" + mock "github.com/stretchr/testify/mock" +) + +// CertificateSecretConfigurator is an autogenerated mock type for the CertificateSecretConfigurator type +type CertificateSecretConfigurator struct { + mock.Mock +} + +// Do provides a mock function with given fields: caSecretName, clusterCertSecretName +func (_m *CertificateSecretConfigurator) Do(caSecretName string, clusterCertSecretName string) (types.RollbackFunc, error) { + ret := _m.Called(caSecretName, clusterCertSecretName) + + var r0 types.RollbackFunc + if rf, ok := ret.Get(0).(func(string, string) types.RollbackFunc); ok { + r0 = rf(caSecretName, clusterCertSecretName) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(types.RollbackFunc) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, string) error); ok { + r1 = rf(caSecretName, clusterCertSecretName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type mockConstructorTestingTNewCertificateSecretConfigurator interface { + mock.TestingT + Cleanup(func()) +} + +// NewCertificateSecretConfigurator creates a new instance of CertificateSecretConfigurator. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewCertificateSecretConfigurator(t mockConstructorTestingTNewCertificateSecretConfigurator) *CertificateSecretConfigurator { + mock := &CertificateSecretConfigurator{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/tests/test/compass-runtime-agent/testkit/init/types/mocks/CompassConfigurator.go b/tests/test/compass-runtime-agent/testkit/init/types/mocks/CompassConfigurator.go new file mode 100644 index 00000000..aa11389a --- /dev/null +++ b/tests/test/compass-runtime-agent/testkit/init/types/mocks/CompassConfigurator.go @@ -0,0 +1,58 @@ +// Code generated by mockery v2.15.0. DO NOT EDIT. + +package mocks + +import ( + types "github.com/kyma-project/kyma/tests/components/application-connector/test/compass-runtime-agent/testkit/init/types" + mock "github.com/stretchr/testify/mock" +) + +// CompassConfigurator is an autogenerated mock type for the CompassConfigurator type +type CompassConfigurator struct { + mock.Mock +} + +// Do provides a mock function with given fields: runtimeName, formationName +func (_m *CompassConfigurator) Do(runtimeName string, formationName string) (types.CompassRuntimeAgentConfig, types.RollbackFunc, error) { + ret := _m.Called(runtimeName, formationName) + + var r0 types.CompassRuntimeAgentConfig + if rf, ok := ret.Get(0).(func(string, string) types.CompassRuntimeAgentConfig); ok { + r0 = rf(runtimeName, formationName) + } else { + r0 = ret.Get(0).(types.CompassRuntimeAgentConfig) + } + + var r1 types.RollbackFunc + if rf, ok := ret.Get(1).(func(string, string) types.RollbackFunc); ok { + r1 = rf(runtimeName, formationName) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(types.RollbackFunc) + } + } + + var r2 error + if rf, ok := ret.Get(2).(func(string, string) error); ok { + r2 = rf(runtimeName, formationName) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +type mockConstructorTestingTNewCompassConfigurator interface { + mock.TestingT + Cleanup(func()) +} + +// NewCompassConfigurator creates a new instance of CompassConfigurator. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewCompassConfigurator(t mockConstructorTestingTNewCompassConfigurator) *CompassConfigurator { + mock := &CompassConfigurator{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/tests/test/compass-runtime-agent/testkit/init/types/mocks/CompassConnectionConfigurator.go b/tests/test/compass-runtime-agent/testkit/init/types/mocks/CompassConnectionConfigurator.go new file mode 100644 index 00000000..cea552f1 --- /dev/null +++ b/tests/test/compass-runtime-agent/testkit/init/types/mocks/CompassConnectionConfigurator.go @@ -0,0 +1,51 @@ +// Code generated by mockery v2.15.0. DO NOT EDIT. + +package mocks + +import ( + types "github.com/kyma-project/kyma/tests/components/application-connector/test/compass-runtime-agent/testkit/init/types" + mock "github.com/stretchr/testify/mock" +) + +// CompassConnectionConfigurator is an autogenerated mock type for the CompassConnectionConfigurator type +type CompassConnectionConfigurator struct { + mock.Mock +} + +// Do provides a mock function with given fields: +func (_m *CompassConnectionConfigurator) Do() (types.RollbackFunc, error) { + ret := _m.Called() + + var r0 types.RollbackFunc + if rf, ok := ret.Get(0).(func() types.RollbackFunc); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(types.RollbackFunc) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type mockConstructorTestingTNewCompassConnectionConfigurator interface { + mock.TestingT + Cleanup(func()) +} + +// NewCompassConnectionConfigurator creates a new instance of CompassConnectionConfigurator. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewCompassConnectionConfigurator(t mockConstructorTestingTNewCompassConnectionConfigurator) *CompassConnectionConfigurator { + mock := &CompassConnectionConfigurator{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/tests/test/compass-runtime-agent/testkit/init/types/mocks/ConfigurationSecretConfigurator.go b/tests/test/compass-runtime-agent/testkit/init/types/mocks/ConfigurationSecretConfigurator.go new file mode 100644 index 00000000..33ae8a41 --- /dev/null +++ b/tests/test/compass-runtime-agent/testkit/init/types/mocks/ConfigurationSecretConfigurator.go @@ -0,0 +1,51 @@ +// Code generated by mockery v2.15.0. DO NOT EDIT. + +package mocks + +import ( + types "github.com/kyma-project/kyma/tests/components/application-connector/test/compass-runtime-agent/testkit/init/types" + mock "github.com/stretchr/testify/mock" +) + +// ConfigurationSecretConfigurator is an autogenerated mock type for the ConfigurationSecretConfigurator type +type ConfigurationSecretConfigurator struct { + mock.Mock +} + +// Do provides a mock function with given fields: configurationSecretName, config +func (_m *ConfigurationSecretConfigurator) Do(configurationSecretName string, config types.CompassRuntimeAgentConfig) (types.RollbackFunc, error) { + ret := _m.Called(configurationSecretName, config) + + var r0 types.RollbackFunc + if rf, ok := ret.Get(0).(func(string, types.CompassRuntimeAgentConfig) types.RollbackFunc); ok { + r0 = rf(configurationSecretName, config) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(types.RollbackFunc) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, types.CompassRuntimeAgentConfig) error); ok { + r1 = rf(configurationSecretName, config) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type mockConstructorTestingTNewConfigurationSecretConfigurator interface { + mock.TestingT + Cleanup(func()) +} + +// NewConfigurationSecretConfigurator creates a new instance of ConfigurationSecretConfigurator. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewConfigurationSecretConfigurator(t mockConstructorTestingTNewConfigurationSecretConfigurator) *ConfigurationSecretConfigurator { + mock := &ConfigurationSecretConfigurator{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/tests/test/compass-runtime-agent/testkit/init/types/mocks/DeploymentConfigurator.go b/tests/test/compass-runtime-agent/testkit/init/types/mocks/DeploymentConfigurator.go new file mode 100644 index 00000000..f49a52b2 --- /dev/null +++ b/tests/test/compass-runtime-agent/testkit/init/types/mocks/DeploymentConfigurator.go @@ -0,0 +1,54 @@ +// Code generated by mockery v2.22.1. DO NOT EDIT. + +package mocks + +import ( + types "github.com/kyma-project/kyma/tests/components/application-connector/test/compass-runtime-agent/testkit/init/types" + mock "github.com/stretchr/testify/mock" +) + +// DeploymentConfigurator is an autogenerated mock type for the DeploymentConfigurator type +type DeploymentConfigurator struct { + mock.Mock +} + +// Do provides a mock function with given fields: caSecretName, clusterCertSecretName, runtimeAgentConfigSecretName, controllerSyncPeriodTime +func (_m *DeploymentConfigurator) Do(caSecretName string, clusterCertSecretName string, runtimeAgentConfigSecretName string, controllerSyncPeriodTime string) (types.RollbackFunc, error) { + ret := _m.Called(caSecretName, clusterCertSecretName, runtimeAgentConfigSecretName, controllerSyncPeriodTime) + + var r0 types.RollbackFunc + var r1 error + if rf, ok := ret.Get(0).(func(string, string, string, string) (types.RollbackFunc, error)); ok { + return rf(caSecretName, clusterCertSecretName, runtimeAgentConfigSecretName, controllerSyncPeriodTime) + } + if rf, ok := ret.Get(0).(func(string, string, string, string) types.RollbackFunc); ok { + r0 = rf(caSecretName, clusterCertSecretName, runtimeAgentConfigSecretName, controllerSyncPeriodTime) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(types.RollbackFunc) + } + } + + if rf, ok := ret.Get(1).(func(string, string, string, string) error); ok { + r1 = rf(caSecretName, clusterCertSecretName, runtimeAgentConfigSecretName, controllerSyncPeriodTime) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type mockConstructorTestingTNewDeploymentConfigurator interface { + mock.TestingT + Cleanup(func()) +} + +// NewDeploymentConfigurator creates a new instance of DeploymentConfigurator. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewDeploymentConfigurator(t mockConstructorTestingTNewDeploymentConfigurator) *DeploymentConfigurator { + mock := &DeploymentConfigurator{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/tests/test/compass-runtime-agent/testkit/init/types/mocks/DirectorClient.go b/tests/test/compass-runtime-agent/testkit/init/types/mocks/DirectorClient.go new file mode 100644 index 00000000..da03b2b6 --- /dev/null +++ b/tests/test/compass-runtime-agent/testkit/init/types/mocks/DirectorClient.go @@ -0,0 +1,130 @@ +// Code generated by mockery v2.15.0. DO NOT EDIT. + +package mocks + +import mock "github.com/stretchr/testify/mock" + +// DirectorClient is an autogenerated mock type for the DirectorClient type +type DirectorClient struct { + mock.Mock +} + +// AssignRuntimeToFormation provides a mock function with given fields: runtimeId, formationName +func (_m *DirectorClient) AssignRuntimeToFormation(runtimeId string, formationName string) error { + ret := _m.Called(runtimeId, formationName) + + var r0 error + if rf, ok := ret.Get(0).(func(string, string) error); ok { + r0 = rf(runtimeId, formationName) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetConnectionToken provides a mock function with given fields: runtimeID +func (_m *DirectorClient) GetConnectionToken(runtimeID string) (string, string, error) { + ret := _m.Called(runtimeID) + + var r0 string + if rf, ok := ret.Get(0).(func(string) string); ok { + r0 = rf(runtimeID) + } else { + r0 = ret.Get(0).(string) + } + + var r1 string + if rf, ok := ret.Get(1).(func(string) string); ok { + r1 = rf(runtimeID) + } else { + r1 = ret.Get(1).(string) + } + + var r2 error + if rf, ok := ret.Get(2).(func(string) error); ok { + r2 = rf(runtimeID) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// RegisterFormation provides a mock function with given fields: formationName +func (_m *DirectorClient) RegisterFormation(formationName string) error { + ret := _m.Called(formationName) + + var r0 error + if rf, ok := ret.Get(0).(func(string) error); ok { + r0 = rf(formationName) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RegisterRuntime provides a mock function with given fields: runtimeName +func (_m *DirectorClient) RegisterRuntime(runtimeName string) (string, error) { + ret := _m.Called(runtimeName) + + var r0 string + if rf, ok := ret.Get(0).(func(string) string); ok { + r0 = rf(runtimeName) + } else { + r0 = ret.Get(0).(string) + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(runtimeName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UnregisterFormation provides a mock function with given fields: formationName +func (_m *DirectorClient) UnregisterFormation(formationName string) error { + ret := _m.Called(formationName) + + var r0 error + if rf, ok := ret.Get(0).(func(string) error); ok { + r0 = rf(formationName) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UnregisterRuntime provides a mock function with given fields: id +func (_m *DirectorClient) UnregisterRuntime(id string) error { + ret := _m.Called(id) + + var r0 error + if rf, ok := ret.Get(0).(func(string) error); ok { + r0 = rf(id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +type mockConstructorTestingTNewDirectorClient interface { + mock.TestingT + Cleanup(func()) +} + +// NewDirectorClient creates a new instance of DirectorClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewDirectorClient(t mockConstructorTestingTNewDirectorClient) *DirectorClient { + mock := &DirectorClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/tests/test/compass-runtime-agent/testkit/init/types/types.go b/tests/test/compass-runtime-agent/testkit/init/types/types.go new file mode 100644 index 00000000..c657ad61 --- /dev/null +++ b/tests/test/compass-runtime-agent/testkit/init/types/types.go @@ -0,0 +1,45 @@ +package types + +type CompassRuntimeAgentConfig struct { + ConnectorUrl string + RuntimeID string + Token string + Tenant string +} + +type RollbackFunc func() error + +//go:generate mockery --name=DirectorClient +type DirectorClient interface { + RegisterRuntime(runtimeName string) (string, error) + RegisterFormation(formationName string) error + AssignRuntimeToFormation(runtimeId, formationName string) error + UnregisterRuntime(id string) error + UnregisterFormation(formationName string) error + GetConnectionToken(runtimeID string) (string, string, error) +} + +//go:generate mockery --name=CompassConfigurator +type CompassConfigurator interface { + Do(runtimeName, formationName string) (CompassRuntimeAgentConfig, RollbackFunc, error) +} + +//go:generate mockery --name=DeploymentConfigurator +type DeploymentConfigurator interface { + Do(caSecretName, clusterCertSecretName, runtimeAgentConfigSecretName, controllerSyncPeriodTime string) (RollbackFunc, error) +} + +//go:generate mockery --name=CertificateSecretConfigurator +type CertificateSecretConfigurator interface { + Do(caSecretName, clusterCertSecretName string) (RollbackFunc, error) +} + +//go:generate mockery --name=ConfigurationSecretConfigurator +type ConfigurationSecretConfigurator interface { + Do(configurationSecretName string, config CompassRuntimeAgentConfig) (RollbackFunc, error) +} + +//go:generate mockery --name=CompassConnectionConfigurator +type CompassConnectionConfigurator interface { + Do() (RollbackFunc, error) +} diff --git a/tests/test/compass-runtime-agent/testkit/oauth/client.go b/tests/test/compass-runtime-agent/testkit/oauth/client.go new file mode 100644 index 00000000..51d0e8f3 --- /dev/null +++ b/tests/test/compass-runtime-agent/testkit/oauth/client.go @@ -0,0 +1,119 @@ +package oauth + +import ( + "context" + "encoding/json" + "fmt" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + "io/ioutil" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/client-go/kubernetes/typed/core/v1" + "net/http" + "net/http/httputil" + "net/url" + "strings" + "time" +) + +//go:generate mockery --name=Client +type Client interface { + GetAuthorizationToken() (Token, error) +} + +type oauthClient struct { + httpClient *http.Client + secretsClient v1.SecretInterface + secretName string +} + +func NewOauthClient(client *http.Client, secrets v1.SecretInterface, secretName string) (Client, error) { + + _, err := secrets.Get(context.Background(), secretName, metav1.GetOptions{}) + + if err != nil { + return nil, fmt.Errorf("Cound not access oauthCredential secret %s", secretName) + } + + return &oauthClient{ + httpClient: client, + secretsClient: secrets, + secretName: secretName, + }, nil +} + +func (c *oauthClient) GetAuthorizationToken() (Token, error) { + credentials, err := c.getCredentials() + + if err != nil { + return Token{}, err + } + + return c.getAuthorizationToken(credentials) +} + +func (c *oauthClient) getCredentials() (credentials, error) { + secret, err := c.secretsClient.Get(context.Background(), c.secretName, metav1.GetOptions{}) + + if err != nil { + return credentials{}, err + } + + return credentials{ + clientID: string(secret.Data[clientIDKey]), + clientSecret: string(secret.Data[clientSecretKey]), + tokensEndpoint: string(secret.Data[tokensEndpointKey]), + }, nil +} + +func (c *oauthClient) getAuthorizationToken(credentials credentials) (Token, error) { + log.Infof("Getting authorisation token for credentials to access Director from endpoint: %s", credentials.tokensEndpoint) + + form := url.Values{} + form.Add(grantTypeFieldName, credentialsGrantType) + form.Add(scopeFieldName, scopes) + + request, err := http.NewRequest(http.MethodPost, credentials.tokensEndpoint, strings.NewReader(form.Encode())) + if err != nil { + log.Errorf("Failed to create authorisation token request") + return Token{}, errors.Wrap(err, "Failed to create authorisation token request") + } + + now := time.Now().Unix() + + request.SetBasicAuth(credentials.clientID, credentials.clientSecret) + request.Header.Set(contentTypeHeader, contentTypeApplicationURLEncoded) + + response, err := c.httpClient.Do(request) + if err != nil { + return Token{}, errors.Wrap(err, "Failed to execute http call") + } + + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + dump, err := httputil.DumpResponse(response, true) + if err != nil { + dump = []byte("failed to dump response body") + } + return Token{}, fmt.Errorf("Get token call returned unexpected status: %s. Response dump: %s", response.Status, string(dump)) + } + + body, err := ioutil.ReadAll(response.Body) + if err != nil { + return Token{}, fmt.Errorf("Failed to read token response body from '%s': %s", credentials.tokensEndpoint, err.Error()) + } + + tokenResponse := Token{} + + err = json.Unmarshal(body, &tokenResponse) + if err != nil { + return Token{}, fmt.Errorf("failed to unmarshal token response body: %s", err.Error()) + } + + log.Infof("Successfully unmarshal response oauth token for accessing Director") + + tokenResponse.Expiration += now + + return tokenResponse, nil +} diff --git a/tests/test/compass-runtime-agent/testkit/oauth/client_test.go b/tests/test/compass-runtime-agent/testkit/oauth/client_test.go new file mode 100644 index 00000000..5bfdb5f2 --- /dev/null +++ b/tests/test/compass-runtime-agent/testkit/oauth/client_test.go @@ -0,0 +1,109 @@ +package oauth + +import ( + "bytes" + "context" + "encoding/json" + "io/ioutil" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + v1 "k8s.io/api/core/v1" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" + core "k8s.io/client-go/kubernetes/typed/core/v1" +) + +const ( + namespace = "test" + secretName = "oauth-compass-credentials" +) + +func TestOauthClient_GetAuthorizationToken(t *testing.T) { + t.Run("Should return oauth token", func(t *testing.T) { + //given + credentials := credentials{ + clientID: "12345", + clientSecret: "some dark and scary secret", + tokensEndpoint: "http://hydra:4445", + } + + token := Token{ + AccessToken: "12345", + Expiration: 1234, + } + + client := NewTestClient(func(req *http.Request) *http.Response { + username, secret, ok := req.BasicAuth() + + if ok && username == credentials.clientID && secret == credentials.clientSecret { + jsonToken, err := json.Marshal(&token) + + require.NoError(t, err) + + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(jsonToken)), + } + } + return &http.Response{ + StatusCode: http.StatusForbidden, + } + }) + + coreV1 := fake.NewSimpleClientset() + secrets := coreV1.CoreV1().Secrets(namespace) + + createFakeCredentialsSecret(t, secrets, credentials) + + oauthClient, err := NewOauthClient(client, secrets, secretName) + require.NoError(t, err) + + //when + responseToken, err := oauthClient.GetAuthorizationToken() + require.NoError(t, err) + token.Expiration += time.Now().Unix() + + //then + assert.Equal(t, token.AccessToken, responseToken.AccessToken) + assert.Equal(t, token.Expiration, responseToken.Expiration) + }) +} + +func NewTestClient(fn RoundTripFunc) *http.Client { + return &http.Client{ + Transport: fn, + } +} + +type RoundTripFunc func(req *http.Request) *http.Response + +func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { + return f(req), nil +} + +func createFakeCredentialsSecret(t *testing.T, secrets core.SecretInterface, credentials credentials) { + + secret := &v1.Secret{ + ObjectMeta: meta.ObjectMeta{ + Name: secretName, + Namespace: namespace, + }, + TypeMeta: meta.TypeMeta{ + Kind: "Secret", + APIVersion: "v1", + }, + Data: map[string][]byte{ + clientIDKey: []byte(credentials.clientID), + clientSecretKey: []byte(credentials.clientSecret), + tokensEndpointKey: []byte(credentials.tokensEndpoint), + }, + } + + _, err := secrets.Create(context.Background(), secret, meta.CreateOptions{}) + + require.NoError(t, err) +} diff --git a/tests/test/compass-runtime-agent/testkit/oauth/mocks/Client.go b/tests/test/compass-runtime-agent/testkit/oauth/mocks/Client.go new file mode 100644 index 00000000..3a357903 --- /dev/null +++ b/tests/test/compass-runtime-agent/testkit/oauth/mocks/Client.go @@ -0,0 +1,49 @@ +// Code generated by mockery v2.15.0. DO NOT EDIT. + +package mocks + +import ( + oauth "github.com/kyma-project/kyma/tests/components/application-connector/test/compass-runtime-agent/testkit/oauth" + mock "github.com/stretchr/testify/mock" +) + +// Client is an autogenerated mock type for the Client type +type Client struct { + mock.Mock +} + +// GetAuthorizationToken provides a mock function with given fields: +func (_m *Client) GetAuthorizationToken() (oauth.Token, error) { + ret := _m.Called() + + var r0 oauth.Token + if rf, ok := ret.Get(0).(func() oauth.Token); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(oauth.Token) + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type mockConstructorTestingTNewClient interface { + mock.TestingT + Cleanup(func()) +} + +// NewClient creates a new instance of Client. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewClient(t mockConstructorTestingTNewClient) *Client { + mock := &Client{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/tests/test/compass-runtime-agent/testkit/oauth/types.go b/tests/test/compass-runtime-agent/testkit/oauth/types.go new file mode 100644 index 00000000..86fc07c2 --- /dev/null +++ b/tests/test/compass-runtime-agent/testkit/oauth/types.go @@ -0,0 +1,38 @@ +package oauth + +import "time" + +const ( + contentTypeHeader = "Content-Type" + contentTypeApplicationURLEncoded = "application/x-www-form-urlencoded" + + grantTypeFieldName = "grant_type" + credentialsGrantType = "client_credentials" + + scopeFieldName = "scope" + scopes = "application:read application:write formation:write runtime:read runtime:write" + + clientIDKey = "client_id" + clientSecretKey = "client_secret" + tokensEndpointKey = "tokens_endpoint" +) + +type Token struct { + AccessToken string `json:"access_token"` + Expiration int64 `json:"expires_in"` +} + +type credentials struct { + clientID string + clientSecret string + tokensEndpoint string +} + +func (token Token) EmptyOrExpired() bool { + if token.AccessToken == "" { + return true + } + + expiration := time.Unix(token.Expiration, 0) + return time.Now().After(expiration) +} diff --git a/tests/test/compass-runtime-agent/testkit/oauth/types_test.go b/tests/test/compass-runtime-agent/testkit/oauth/types_test.go new file mode 100644 index 00000000..e1f8b0f7 --- /dev/null +++ b/tests/test/compass-runtime-agent/testkit/oauth/types_test.go @@ -0,0 +1,53 @@ +package oauth + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestToken_EmptyOrExpired(t *testing.T) { + t.Run("Should return true when token is empty", func(t *testing.T) { + //given + token := Token{} + + //when + empty := token.EmptyOrExpired() + + //then + assert.True(t, empty) + }) + + t.Run("Should return true when expired", func(t *testing.T) { + //given + time2000 := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC).Unix() + + token := Token{ + AccessToken: "token", + Expiration: time2000, + } + + //when + expired := token.EmptyOrExpired() + + //then + assert.True(t, expired) + }) + + t.Run("Should return false when not empty or expired", func(t *testing.T) { + //given + time3000 := time.Date(3000, 1, 1, 0, 0, 0, 0, time.UTC).Unix() + + token := Token{ + AccessToken: "token", + Expiration: time3000, + } + + //when + notExpired := token.EmptyOrExpired() + + //then + assert.False(t, notExpired) + }) +} diff --git a/tests/test/compass-runtime-agent/testkit/random/randomstring.go b/tests/test/compass-runtime-agent/testkit/random/randomstring.go new file mode 100644 index 00000000..7fbb0075 --- /dev/null +++ b/tests/test/compass-runtime-agent/testkit/random/randomstring.go @@ -0,0 +1,25 @@ +package random + +import ( + "math/rand" + "strings" + "time" +) + +const charset = "abcdefghijklmnopqrstuvwxyz" + + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + +var seededRand *rand.Rand = rand.New( + rand.NewSource(time.Now().UnixNano())) + +func StringWithCharset(length int, charset string) string { + b := make([]byte, length) + for i := range b { + b[i] = charset[seededRand.Intn(len(charset))] + } + return string(b) +} + +func RandomString(length int) string { + return strings.ToLower(StringWithCharset(length, charset)) +} diff --git a/tests/test/compass-runtime-agent/testkit/third_party/machinebox/graphql/LICENSE b/tests/test/compass-runtime-agent/testkit/third_party/machinebox/graphql/LICENSE new file mode 100644 index 00000000..9b0dfaa8 --- /dev/null +++ b/tests/test/compass-runtime-agent/testkit/third_party/machinebox/graphql/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + 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 2017 Machine Box, Ltd. + + 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. diff --git a/tests/test/compass-runtime-agent/testkit/third_party/machinebox/graphql/README.md b/tests/test/compass-runtime-agent/testkit/third_party/machinebox/graphql/README.md new file mode 100644 index 00000000..fabd7c8e --- /dev/null +++ b/tests/test/compass-runtime-agent/testkit/third_party/machinebox/graphql/README.md @@ -0,0 +1,67 @@ +# graphql [![GoDoc](https://godoc.org/github.com/machinebox/graphql?status.png)](http://godoc.org/github.com/machinebox/graphql) [![Build Status](https://travis-ci.org/machinebox/graphql.svg?branch=master)](https://travis-ci.org/machinebox/graphql) [![Go Report Card](https://goreportcard.com/badge/github.com/machinebox/graphql)](https://goreportcard.com/report/github.com/machinebox/graphql) + +Low-level GraphQL client for Go. + +* Simple, familiar API +* Respects `context.Context` timeouts and cancellation +* Build and execute any kind of GraphQL request +* Use strong Go types for response data +* Use variables and upload files +* Simple error handling + +## Installation +Make sure you have a working Go environment. To install graphql, simply run: + +``` +$ go get github.com/machinebox/graphql +``` + +## Usage + +```go +import "context" + +// create a client (safe to share across requests) +client := graphql.NewClient("https://machinebox.io/graphql") + +// make a request +req := graphql.NewRequest(` + query ($key: String!) { + items (id:$key) { + field1 + field2 + field3 + } + } +`) + +// set any variables +req.Var("key", "value") + +// set header fields +req.Header.Set("Cache-Control", "no-cache") + +// define a Context for the request +ctx := context.Background() + +// run it and capture the response +var respData ResponseStruct +if err := client.Run(ctx, req, &respData); err != nil { + log.Fatal(err) +} +``` + +### File Support via Multipart Form Data + +By default, the package will send a JSON body. To enable the sending of files, you can opt to +use multipart form data instead using the `UseMultipartForm` option when you create your `Client`: + +``` +client := graphql.NewClient("https://machinebox.io/graphql", graphql.UseMultipartForm()) +``` + +For more information, [read the godoc package documentation](http://godoc.org/github.com/machinebox/graphql). + +## Thanks + +Thanks to [Chris Broadfoot](https://github.com/broady) for design help. diff --git a/tests/test/compass-runtime-agent/testkit/third_party/machinebox/graphql/graphql.go b/tests/test/compass-runtime-agent/testkit/third_party/machinebox/graphql/graphql.go new file mode 100644 index 00000000..dc005dad --- /dev/null +++ b/tests/test/compass-runtime-agent/testkit/third_party/machinebox/graphql/graphql.go @@ -0,0 +1,354 @@ +// Package graphql provides a low level GraphQL client. +// +// // create a client (safe to share across requests) +// client := graphql.NewClient("https://machinebox.io/graphql") +// +// // make a request +// req := graphql.NewRequest(` +// query ($key: String!) { +// items (id:$key) { +// field1 +// field2 +// field3 +// } +// } +// `) +// +// // set any variables +// req.Var("key", "value") +// +// // run it and capture the response +// var respData ResponseStruct +// if err := client.Run(ctx, req, &respData); err != nil { +// log.Fatal(err) +// } +// +// Specify client +// +// To specify your own http.Client, use the WithHTTPClient option: +// httpclient := &http.Client{} +// client := graphql.NewClient("https://machinebox.io/graphql", graphql.WithHTTPClient(httpclient)) +package graphql + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "mime/multipart" + "net/http" + "strings" + "unicode/utf8" + + "github.com/pkg/errors" +) + +// Client is a client for interacting with a GraphQL API. +type Client struct { + endpoint string + httpClient *http.Client + useMultipartForm bool + + // closeReq will close the request body immediately allowing for reuse of client + closeReq bool + + // Log is called with various debug information. + // To log to standard out, use: + // client.Log = func(s string) { log.Println(s) } + Log func(s string) +} + +// NewClient makes a new Client capable of making GraphQL requests. +func NewClient(endpoint string, opts ...ClientOption) *Client { + c := &Client{ + endpoint: endpoint, + Log: func(string) {}, + } + for _, optionFunc := range opts { + optionFunc(c) + } + if c.httpClient == nil { + c.httpClient = http.DefaultClient + } + return c +} + +func (c *Client) logf(format string, args ...interface{}) { + c.Log(fmt.Sprintf(format, args...)) +} + +// Run executes the query and unmarshals the response from the data field +// into the response object. +// Pass in a nil response object to skip response parsing. +// If the request fails or the server returns an error, the first error +// will be returned. +func (c *Client) Run(ctx context.Context, req *Request, resp interface{}) error { + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + if len(req.files) > 0 && !c.useMultipartForm { + return errors.New("cannot send files with PostFields option") + } + if c.useMultipartForm { + return c.runWithPostFields(ctx, req, resp) + } + return c.runWithJSON(ctx, req, resp) +} + +func (c *Client) runWithJSON(ctx context.Context, req *Request, resp interface{}) error { + var requestBody bytes.Buffer + requestBodyObj := struct { + Query string `json:"query"` + Variables map[string]interface{} `json:"variables"` + }{ + Query: req.q, + Variables: req.vars, + } + if err := json.NewEncoder(&requestBody).Encode(requestBodyObj); err != nil { + return errors.Wrap(err, "encode body") + } + c.logf(">> variables: %v", req.vars) + c.logf(">> query: %s", req.q) + gr := &graphResponse{ + Data: resp, + } + r, err := http.NewRequest(http.MethodPost, c.endpoint, &requestBody) + if err != nil { + return err + } + r.Close = c.closeReq + r.Header.Set("Content-Type", "application/json; charset=utf-8") + r.Header.Set("Accept", "application/json; charset=utf-8") + for key, values := range req.Header { + for _, value := range values { + r.Header.Add(key, value) + } + } + c.logf(">> headers: %v", hideHeaders(r.Header)) + r = r.WithContext(ctx) + res, err := c.httpClient.Do(r) + if err != nil { + return err + } + defer res.Body.Close() + var buf bytes.Buffer + if _, err := io.Copy(&buf, res.Body); err != nil { + return errors.Wrap(err, "reading body") + } + c.logf("<< %s", buf.String()) + if err := json.NewDecoder(&buf).Decode(&gr); err != nil { + if res.StatusCode != http.StatusOK { + return fmt.Errorf("graphql: server returned a non-200 status code: %v", res.StatusCode) + } + return errors.Wrap(err, "decoding response") + } + if len(gr.Errors) > 0 { + // return first error + return gr.Errors[0] + } + return nil +} + +func (c *Client) runWithPostFields(ctx context.Context, req *Request, resp interface{}) error { + var requestBody bytes.Buffer + writer := multipart.NewWriter(&requestBody) + if err := writer.WriteField("query", req.q); err != nil { + return errors.Wrap(err, "write query field") + } + var variablesBuf bytes.Buffer + if len(req.vars) > 0 { + variablesField, err := writer.CreateFormField("variables") + if err != nil { + return errors.Wrap(err, "create variables field") + } + if err := json.NewEncoder(io.MultiWriter(variablesField, &variablesBuf)).Encode(req.vars); err != nil { + return errors.Wrap(err, "encode variables") + } + } + for i := range req.files { + part, err := writer.CreateFormFile(req.files[i].Field, req.files[i].Name) + if err != nil { + return errors.Wrap(err, "create form file") + } + if _, err := io.Copy(part, req.files[i].R); err != nil { + return errors.Wrap(err, "preparing file") + } + } + if err := writer.Close(); err != nil { + return errors.Wrap(err, "close writer") + } + c.logf(">> variables: %s", variablesBuf.String()) + c.logf(">> files: %d", len(req.files)) + c.logf(">> query: %s", req.q) + gr := &graphResponse{ + Data: resp, + } + r, err := http.NewRequest(http.MethodPost, c.endpoint, &requestBody) + if err != nil { + return err + } + r.Close = c.closeReq + r.Header.Set("Content-Type", writer.FormDataContentType()) + r.Header.Set("Accept", "application/json; charset=utf-8") + for key, values := range req.Header { + for _, value := range values { + r.Header.Add(key, value) + } + } + c.logf(">> headers: %v", hideHeaders(r.Header)) + r = r.WithContext(ctx) + res, err := c.httpClient.Do(r) + if err != nil { + return err + } + defer res.Body.Close() + var buf bytes.Buffer + if _, err := io.Copy(&buf, res.Body); err != nil { + return errors.Wrap(err, "reading body") + } + c.logf("<< %s", buf.String()) + if err := json.NewDecoder(&buf).Decode(&gr); err != nil { + if res.StatusCode != http.StatusOK { + return fmt.Errorf("graphql: server returned a non-200 status code: %v", res.StatusCode) + } + return errors.Wrap(err, "decoding response") + } + if len(gr.Errors) > 0 { + // return first error + return gr.Errors[0] + } + return nil +} + +// WithHTTPClient specifies the underlying http.Client to use when +// making requests. +// NewClient(endpoint, WithHTTPClient(specificHTTPClient)) +func WithHTTPClient(httpclient *http.Client) ClientOption { + return func(client *Client) { + client.httpClient = httpclient + } +} + +// UseMultipartForm uses multipart/form-data and activates support for +// files. +func UseMultipartForm() ClientOption { + return func(client *Client) { + client.useMultipartForm = true + } +} + +// ImmediatelyCloseReqBody will close the req body immediately after each request body is ready +func ImmediatelyCloseReqBody() ClientOption { + return func(client *Client) { + client.closeReq = true + } +} + +// ClientOption are functions that are passed into NewClient to +// modify the behaviour of the Client. +type ClientOption func(*Client) + +type ExtendedError interface { + Error() string + Extensions() map[string]interface{} +} + +type graphErr struct { + Message string `json:"message,omitempty"` + ErrorExtensions map[string]interface{} `json:"extensions,omitempty"` +} + +func (e graphErr) Error() string { + return "graphql: " + e.Message +} + +func (e graphErr) Extensions() map[string]interface{} { + return e.ErrorExtensions +} + +type graphResponse struct { + Data interface{} + Errors []graphErr +} + +// Request is a GraphQL request. +type Request struct { + q string + vars map[string]interface{} + files []File + + // Header represent any request headers that will be set + // when the request is made. + Header http.Header +} + +// NewRequest makes a new Request with the specified string. +func NewRequest(q string) *Request { + req := &Request{ + q: q, + Header: make(map[string][]string), + } + return req +} + +// Var sets a variable. +func (req *Request) Var(key string, value interface{}) { + if req.vars == nil { + req.vars = make(map[string]interface{}) + } + req.vars[key] = value +} + +// Vars gets the variables for this Request. +func (req *Request) Vars() map[string]interface{} { + return req.vars +} + +// Files gets the files in this request. +func (req *Request) Files() []File { + return req.files +} + +// Query gets the query string of this request. +func (req *Request) Query() string { + return req.q +} + +// File sets a file to upload. +// Files are only supported with a Client that was created with +// the UseMultipartForm option. +func (req *Request) File(fieldname, filename string, r io.Reader) { + req.files = append(req.files, File{ + Field: fieldname, + Name: filename, + R: r, + }) +} + +// File represents a file to upload. +type File struct { + Field string + Name string + R io.Reader +} + +// hideHeaders creates a copy of headers +// with specified fields censored (eg 'password' -> '********') +// Additionally by default censors: +// - Authorization +func hideHeaders(headers http.Header, toHide ...string) http.Header { + toHide = append(toHide, "Authorization") + hs := headers.Clone() + for _, h := range toHide { + v, ok := hs[h] + if ok { + for i := range v { + hs[h][i] = strings.Repeat("*", utf8.RuneCountInString(v[i])) + } + } + } + return hs +} diff --git a/tests/test/compass-runtime-agent/testkit/third_party/machinebox/graphql/graphql_json_test.go b/tests/test/compass-runtime-agent/testkit/third_party/machinebox/graphql/graphql_json_test.go new file mode 100644 index 00000000..f2a9b23e --- /dev/null +++ b/tests/test/compass-runtime-agent/testkit/third_party/machinebox/graphql/graphql_json_test.go @@ -0,0 +1,233 @@ +package graphql + +import ( + "bytes" + "context" + "io" + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/matryer/is" +) + +func TestDoJSON(t *testing.T) { + is := is.New(t) + var calls int + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + calls++ + is.Equal(r.Method, http.MethodPost) + b, err := ioutil.ReadAll(r.Body) + is.NoErr(err) + is.Equal(string(b), `{"query":"query {}","variables":null}`+"\n") + io.WriteString(w, `{ + "data": { + "something": "yes" + } + }`) + })) + defer srv.Close() + + ctx := context.Background() + client := NewClient(srv.URL) + + ctx, cancel := context.WithTimeout(ctx, 1*time.Second) + defer cancel() + var responseData map[string]interface{} + err := client.Run(ctx, &Request{q: "query {}"}, &responseData) + is.NoErr(err) + is.Equal(calls, 1) // calls + is.Equal(responseData["something"], "yes") +} + +func TestDoJSONServerError(t *testing.T) { + is := is.New(t) + var calls int + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + calls++ + is.Equal(r.Method, http.MethodPost) + b, err := ioutil.ReadAll(r.Body) + is.NoErr(err) + is.Equal(string(b), `{"query":"query {}","variables":null}`+"\n") + w.WriteHeader(http.StatusInternalServerError) + io.WriteString(w, `Internal Server Error`) + })) + defer srv.Close() + + ctx := context.Background() + client := NewClient(srv.URL) + + ctx, cancel := context.WithTimeout(ctx, 1*time.Second) + defer cancel() + var responseData map[string]interface{} + err := client.Run(ctx, &Request{q: "query {}"}, &responseData) + is.Equal(calls, 1) // calls + is.Equal(err.Error(), "graphql: server returned a non-200 status code: 500") +} + +func TestDoJSONBadRequestErr(t *testing.T) { + is := is.New(t) + var calls int + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + calls++ + is.Equal(r.Method, http.MethodPost) + b, err := ioutil.ReadAll(r.Body) + is.NoErr(err) + is.Equal(string(b), `{"query":"query {}","variables":null}`+"\n") + w.WriteHeader(http.StatusBadRequest) + io.WriteString(w, `{ + "errors": [{ + "message": "miscellaneous message as to why the the request was bad" + }] + }`) + })) + defer srv.Close() + + ctx := context.Background() + client := NewClient(srv.URL) + + ctx, cancel := context.WithTimeout(ctx, 1*time.Second) + defer cancel() + var responseData map[string]interface{} + err := client.Run(ctx, &Request{q: "query {}"}, &responseData) + is.Equal(calls, 1) // calls + is.Equal(err.Error(), "graphql: miscellaneous message as to why the the request was bad") +} + +func TestDoJSONErrWithExtensions(t *testing.T) { + is := is.New(t) + var calls int + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + calls++ + is.Equal(r.Method, http.MethodPost) + b, err := ioutil.ReadAll(r.Body) + is.NoErr(err) + is.Equal(string(b), `{"query":"query {}","variables":null}`+"\n") + w.WriteHeader(http.StatusOK) + io.WriteString(w, `{ + "errors": [{ + "message": "miscellaneous message as to why the the request was bad", + "extensions": { + "code": "400" + } + }] + }`) + })) + defer srv.Close() + + ctx := context.Background() + client := NewClient(srv.URL) + + ctx, cancel := context.WithTimeout(ctx, 1*time.Second) + defer cancel() + var responseData map[string]interface{} + err := client.Run(ctx, &Request{q: "query {}"}, &responseData) + is.Equal(calls, 1) // calls + is.Equal(err.Error(), "graphql: miscellaneous message as to why the the request was bad") + is.Equal(err.(ExtendedError).Extensions()["code"], "400") +} + +func TestQueryJSON(t *testing.T) { + is := is.New(t) + + var calls int + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + calls++ + b, err := ioutil.ReadAll(r.Body) + is.NoErr(err) + is.Equal(string(b), `{"query":"query {}","variables":{"username":"matryer"}}`+"\n") + _, err = io.WriteString(w, `{"data":{"value":"some data"}}`) + is.NoErr(err) + })) + defer srv.Close() + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + + client := NewClient(srv.URL) + + req := NewRequest("query {}") + req.Var("username", "matryer") + + // check variables + is.True(req != nil) + is.Equal(req.vars["username"], "matryer") + + var resp struct { + Value string + } + err := client.Run(ctx, req, &resp) + is.NoErr(err) + is.Equal(calls, 1) + + is.Equal(resp.Value, "some data") +} + +func TestHeader(t *testing.T) { + is := is.New(t) + + var calls int + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + calls++ + is.Equal(r.Header.Get("X-Custom-Header"), "123") + + _, err := io.WriteString(w, `{"data":{"value":"some data"}}`) + is.NoErr(err) + })) + defer srv.Close() + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + + client := NewClient(srv.URL) + + req := NewRequest("query {}") + req.Header.Set("X-Custom-Header", "123") + + var resp struct { + Value string + } + err := client.Run(ctx, req, &resp) + is.NoErr(err) + is.Equal(calls, 1) + + is.Equal(resp.Value, "some data") +} + +func TestHideAuthInJSON(t *testing.T) { + is := is.New(t) + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + io.WriteString(w, `{ + "data": { + "something": "yes" + } + }`) + })) + defer srv.Close() + + ctx := context.Background() + client := NewClient(srv.URL) + + var cout bytes.Buffer + client.Log = func(s string) { + _, err := cout.WriteString(s) + is.NoErr(err) + } + + ctx, cancel := context.WithTimeout(ctx, 1*time.Second) + defer cancel() + var responseData map[string]interface{} + + header := make(http.Header) + header["Authorization"] = []string{"some secret key", "another secret key"} + req := Request{ + q: "query {}", + Header: header, + } + + err := client.Run(ctx, &req, &responseData) + is.NoErr(err) + is.Equal(responseData["something"], "yes") + is.True(!strings.Contains(cout.String(), "secret key")) +} diff --git a/tests/test/compass-runtime-agent/testkit/third_party/machinebox/graphql/graphql_multipart_test.go b/tests/test/compass-runtime-agent/testkit/third_party/machinebox/graphql/graphql_multipart_test.go new file mode 100644 index 00000000..4bf3fb8c --- /dev/null +++ b/tests/test/compass-runtime-agent/testkit/third_party/machinebox/graphql/graphql_multipart_test.go @@ -0,0 +1,302 @@ +package graphql + +import ( + "bytes" + "context" + "io" + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/matryer/is" +) + +func TestWithClient(t *testing.T) { + is := is.New(t) + var calls int + testClient := &http.Client{ + Transport: roundTripperFunc(func(req *http.Request) (*http.Response, error) { + calls++ + resp := &http.Response{ + Body: ioutil.NopCloser(strings.NewReader(`{"data":{"key":"value"}}`)), + } + return resp, nil + }), + } + + ctx := context.Background() + client := NewClient("", WithHTTPClient(testClient), UseMultipartForm()) + + req := NewRequest(``) + client.Run(ctx, req, nil) + + is.Equal(calls, 1) // calls +} + +func TestDoUseMultipartForm(t *testing.T) { + is := is.New(t) + var calls int + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + calls++ + is.Equal(r.Method, http.MethodPost) + query := r.FormValue("query") + is.Equal(query, `query {}`) + io.WriteString(w, `{ + "data": { + "something": "yes" + } + }`) + })) + defer srv.Close() + + ctx := context.Background() + client := NewClient(srv.URL, UseMultipartForm()) + + ctx, cancel := context.WithTimeout(ctx, 1*time.Second) + defer cancel() + var responseData map[string]interface{} + err := client.Run(ctx, &Request{q: "query {}"}, &responseData) + is.NoErr(err) + is.Equal(calls, 1) // calls + is.Equal(responseData["something"], "yes") +} + +func TestImmediatelyCloseReqBody(t *testing.T) { + is := is.New(t) + var calls int + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + calls++ + is.Equal(r.Method, http.MethodPost) + query := r.FormValue("query") + is.Equal(query, `query {}`) + io.WriteString(w, `{ + "data": { + "something": "yes" + } + }`) + })) + defer srv.Close() + + ctx := context.Background() + client := NewClient(srv.URL, ImmediatelyCloseReqBody(), UseMultipartForm()) + + ctx, cancel := context.WithTimeout(ctx, 1*time.Second) + defer cancel() + var responseData map[string]interface{} + err := client.Run(ctx, &Request{q: "query {}"}, &responseData) + is.NoErr(err) + is.Equal(calls, 1) // calls + is.Equal(responseData["something"], "yes") +} + +func TestDoErr(t *testing.T) { + is := is.New(t) + var calls int + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + calls++ + is.Equal(r.Method, http.MethodPost) + query := r.FormValue("query") + is.Equal(query, `query {}`) + io.WriteString(w, `{ + "errors": [{ + "message": "Something went wrong" + }] + }`) + })) + defer srv.Close() + + ctx := context.Background() + client := NewClient(srv.URL, UseMultipartForm()) + + ctx, cancel := context.WithTimeout(ctx, 1*time.Second) + defer cancel() + var responseData map[string]interface{} + err := client.Run(ctx, &Request{q: "query {}"}, &responseData) + is.True(err != nil) + is.Equal(err.Error(), "graphql: Something went wrong") +} + +func TestDoServerErr(t *testing.T) { + is := is.New(t) + var calls int + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + calls++ + is.Equal(r.Method, http.MethodPost) + query := r.FormValue("query") + is.Equal(query, `query {}`) + w.WriteHeader(http.StatusInternalServerError) + io.WriteString(w, `Internal Server Error`) + })) + defer srv.Close() + + ctx := context.Background() + client := NewClient(srv.URL, UseMultipartForm()) + + ctx, cancel := context.WithTimeout(ctx, 1*time.Second) + defer cancel() + var responseData map[string]interface{} + err := client.Run(ctx, &Request{q: "query {}"}, &responseData) + is.Equal(err.Error(), "graphql: server returned a non-200 status code: 500") +} + +func TestDoBadRequestErr(t *testing.T) { + is := is.New(t) + var calls int + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + calls++ + is.Equal(r.Method, http.MethodPost) + query := r.FormValue("query") + is.Equal(query, `query {}`) + w.WriteHeader(http.StatusBadRequest) + io.WriteString(w, `{ + "errors": [{ + "message": "miscellaneous message as to why the the request was bad" + }] + }`) + })) + defer srv.Close() + + ctx := context.Background() + client := NewClient(srv.URL, UseMultipartForm()) + + ctx, cancel := context.WithTimeout(ctx, 1*time.Second) + defer cancel() + var responseData map[string]interface{} + err := client.Run(ctx, &Request{q: "query {}"}, &responseData) + is.Equal(err.Error(), "graphql: miscellaneous message as to why the the request was bad") +} + +func TestDoNoResponse(t *testing.T) { + is := is.New(t) + var calls int + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + calls++ + is.Equal(r.Method, http.MethodPost) + query := r.FormValue("query") + is.Equal(query, `query {}`) + io.WriteString(w, `{ + "data": { + "something": "yes" + } + }`) + })) + defer srv.Close() + + ctx := context.Background() + client := NewClient(srv.URL, UseMultipartForm()) + + ctx, cancel := context.WithTimeout(ctx, 1*time.Second) + defer cancel() + err := client.Run(ctx, &Request{q: "query {}"}, nil) + is.NoErr(err) + is.Equal(calls, 1) // calls +} + +func TestQuery(t *testing.T) { + is := is.New(t) + + var calls int + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + calls++ + query := r.FormValue("query") + is.Equal(query, "query {}") + is.Equal(r.FormValue("variables"), `{"username":"matryer"}`+"\n") + _, err := io.WriteString(w, `{"data":{"value":"some data"}}`) + is.NoErr(err) + })) + defer srv.Close() + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + + client := NewClient(srv.URL, UseMultipartForm()) + + req := NewRequest("query {}") + req.Var("username", "matryer") + + // check variables + is.True(req != nil) + is.Equal(req.vars["username"], "matryer") + + var resp struct { + Value string + } + err := client.Run(ctx, req, &resp) + is.NoErr(err) + is.Equal(calls, 1) + + is.Equal(resp.Value, "some data") +} + +func TestFile(t *testing.T) { + is := is.New(t) + + var calls int + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + calls++ + file, header, err := r.FormFile("file") + is.NoErr(err) + defer file.Close() + is.Equal(header.Filename, "filename.txt") + + b, err := ioutil.ReadAll(file) + is.NoErr(err) + is.Equal(string(b), `This is a file`) + + _, err = io.WriteString(w, `{"data":{"value":"some data"}}`) + is.NoErr(err) + })) + defer srv.Close() + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + client := NewClient(srv.URL, UseMultipartForm()) + f := strings.NewReader(`This is a file`) + req := NewRequest("query {}") + req.File("file", "filename.txt", f) + err := client.Run(ctx, req, nil) + is.NoErr(err) +} + +type roundTripperFunc func(req *http.Request) (*http.Response, error) + +func (fn roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) { + return fn(req) +} + +func TestHideAuthInMultipartForm(t *testing.T) { + is := is.New(t) + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + io.WriteString(w, `{ + "data": { + "something": "yes" + } + }`) + })) + defer srv.Close() + + ctx := context.Background() + client := NewClient(srv.URL, UseMultipartForm()) + + var cout bytes.Buffer + client.Log = func(s string) { + _, err := cout.WriteString(s) + is.NoErr(err) + } + + header := make(http.Header) + header["Authorization"] = []string{"some secret key", "another secret key"} + req := Request{ + q: "query {}", + Header: header, + } + + ctx, cancel := context.WithTimeout(ctx, 1*time.Second) + defer cancel() + var responseData map[string]interface{} + err := client.Run(ctx, &req, &responseData) + is.NoErr(err) + is.Equal(responseData["something"], "yes") + is.True(!strings.Contains(cout.String(), "secret key")) +} diff --git a/tests/tools/external-api-mock-app/config.go b/tests/tools/external-api-mock-app/config.go new file mode 100644 index 00000000..19927178 --- /dev/null +++ b/tests/tools/external-api-mock-app/config.go @@ -0,0 +1,54 @@ +package main + +import ( + "fmt" +) + +type mTLS struct { + caCertPath string + serverCertPath string + serverKeyPath string + port int +} + +type Config struct { + LogLevel string + Port int + BasicAuthUser string + BasicAuthPassword string + OAuthClientID string + OAuthClientSecret string + RequestHeaders map[string][]string + RequestQueryParameters map[string][]string + mTLS mTLS + mTLSExpiredCerts mTLS +} + +func NewConfig() *Config { + return &Config{ + LogLevel: "info", + Port: 8080, + BasicAuthUser: "user", + BasicAuthPassword: "passwd", + OAuthClientID: "clientID", + OAuthClientSecret: "clientSecret", + RequestHeaders: map[string][]string{"Hkey1": {"Hval1"}, "Hkey2": {"Hval21", "Hval22"}}, + RequestQueryParameters: map[string][]string{"Qkey1": {"Qval1"}, "Qkey2": {"Qval21", "Qval22"}}, + mTLS: mTLS{ + port: 8090, + caCertPath: "/etc/secret-volume/ca.crt", + serverCertPath: "/etc/secret-volume/server.crt", + serverKeyPath: "/etc/secret-volume/server.key", + }, + mTLSExpiredCerts: mTLS{ + port: 8091, + caCertPath: "/etc/expired-server-cert-volume/ca.crt", + serverCertPath: "/etc/expired-server-cert-volume/server.crt", + serverKeyPath: "/etc/expired-server-cert-volume/server.key", + }, + } +} + +func (c *Config) String() string { + return fmt.Sprintf("LogLevel: %s", c.LogLevel) +} diff --git a/tests/tools/external-api-mock-app/server.go b/tests/tools/external-api-mock-app/server.go new file mode 100644 index 00000000..b469380a --- /dev/null +++ b/tests/tools/external-api-mock-app/server.go @@ -0,0 +1,79 @@ +package main + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "github.com/kyma-project/kyma/tests/components/application-connector/internal/testkit/test-api" + log "github.com/sirupsen/logrus" + "io/ioutil" + "net/http" + "os" + "sync" +) + +func main() { + cfg := NewConfig() + logLevel, err := log.ParseLevel(cfg.LogLevel) + if err != nil { + log.Warnf("Invalid log level: '%s', defaulting to 'info'", cfg.LogLevel) + logLevel = log.InfoLevel + } + log.SetLevel(logLevel) + + log.Infof("Starting mock application") + log.Infof("Config: %s", cfg.String()) + + wg := sync.WaitGroup{} + wg.Add(3) + + basicAuthCredentials := test_api.BasicAuthCredentials{User: cfg.BasicAuthUser, Password: cfg.BasicAuthPassword} + oAuthCredentials := test_api.OAuthCredentials{ClientID: cfg.OAuthClientID, ClientSecret: cfg.OAuthClientSecret} + expectedRequestParameters := test_api.ExpectedRequestParameters{Headers: cfg.RequestHeaders, QueryParameters: cfg.RequestQueryParameters} + oauthTokens := make(map[string]test_api.OAuthToken) + csrfTokens := make(test_api.CSRFTokens) + + go func() { + address := fmt.Sprintf(":%d", cfg.Port) + router := test_api.SetupRoutes(os.Stdout, basicAuthCredentials, oAuthCredentials, expectedRequestParameters, oauthTokens, csrfTokens) + log.Fatal(http.ListenAndServe(address, router)) + }() + + go func() { + address := fmt.Sprintf(":%d", cfg.mTLS.port) + router := test_api.SetupMTLSRoutes(os.Stdout, oAuthCredentials, oauthTokens, csrfTokens) + mtlsServer := newMTLSServer(cfg.mTLS.caCertPath, address, router) + log.Fatal(mtlsServer.ListenAndServeTLS(cfg.mTLS.serverCertPath, cfg.mTLS.serverKeyPath)) + }() + + go func() { + address := fmt.Sprintf(":%d", cfg.mTLSExpiredCerts.port) + router := test_api.SetupMTLSRoutes(os.Stdout, oAuthCredentials, oauthTokens, csrfTokens) + mtlsServer := newMTLSServer(cfg.mTLSExpiredCerts.caCertPath, address, router) + log.Fatal(mtlsServer.ListenAndServeTLS(cfg.mTLSExpiredCerts.serverCertPath, cfg.mTLSExpiredCerts.serverKeyPath)) + }() + + wg.Wait() +} + +func newMTLSServer(caCertPath, address string, handler http.Handler) *http.Server { + // Create a CA certificate pool and add cert.pem to it + caCert, err := ioutil.ReadFile(caCertPath) + if err != nil { + log.Fatal(err) + } + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM(caCert) + + // Create the TLS Config with the CA pool and enable Client certificate validation + tlsConfig := &tls.Config{ + ClientCAs: caCertPool, + ClientAuth: tls.RequireAndVerifyClientCert, + } + + return &http.Server{ + Addr: address, + Handler: handler, + TLSConfig: tlsConfig, + } +}