From 493cbd91d7f318780cf4ca2eee04f313b3caa0c3 Mon Sep 17 00:00:00 2001 From: m00g3n Date: Tue, 12 Mar 2024 16:58:14 +0100 Subject: [PATCH 01/11] Add app-gateway k3d integration test --- .../kyma-integration-k3d-agent-tests.yml | 37 + .../kyma-integration-k3d-app-gateway.yml | 36 + .github/workflows/run-tests.yaml | 3 - .gitignore | 2 +- pkg/crypto/sha256/utils_test.go | 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 | 38 + .../Makefile.test-application-conn-validator | 59 + tests/Makefile.test-application-gateway | 80 ++ tests/Makefile.test-compass-runtime-agent | 53 + 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 | 126 ++ ...applications.applicationconnector.crd.yaml | 183 +++ .../hack/ci/deps/compass-connection.crd.yaml | 144 +++ .../charts/compass-runtime-agent/.helmignore | 21 + .../charts/compass-runtime-agent/Chart.yaml | 6 + .../templates/_helpers.tpl | 18 + .../templates/cluster-role-binding.yaml | 48 + .../templates/deployment.yaml | 115 ++ .../templates/priority-class.yaml | 7 + .../templates/resources-role-binding.yaml | 40 + .../templates/role-binding.yaml | 76 ++ .../templates/service-account.yaml | 11 + .../templates/service.yaml | 19 + .../templates/skr-configmap.yaml | 7 + .../charts/compass-runtime-agent/values.yaml | 89 ++ 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 | 19 + .../resources/charts/gateway-test/Chart.yaml | 24 + .../gateway-test/charts/mock-app/Chart.yaml | 24 + .../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/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 | 120 ++ .../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 ++ 174 files changed, 11891 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/kyma-integration-k3d-agent-tests.yml 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/applications.applicationconnector.crd.yaml create mode 100644 tests/hack/ci/deps/compass-connection.crd.yaml create mode 100644 tests/hack/ci/resources/charts/compass-runtime-agent/.helmignore create mode 100644 tests/hack/ci/resources/charts/compass-runtime-agent/Chart.yaml create mode 100644 tests/hack/ci/resources/charts/compass-runtime-agent/templates/_helpers.tpl create mode 100644 tests/hack/ci/resources/charts/compass-runtime-agent/templates/cluster-role-binding.yaml create mode 100644 tests/hack/ci/resources/charts/compass-runtime-agent/templates/deployment.yaml create mode 100644 tests/hack/ci/resources/charts/compass-runtime-agent/templates/priority-class.yaml create mode 100644 tests/hack/ci/resources/charts/compass-runtime-agent/templates/resources-role-binding.yaml create mode 100644 tests/hack/ci/resources/charts/compass-runtime-agent/templates/role-binding.yaml create mode 100644 tests/hack/ci/resources/charts/compass-runtime-agent/templates/service-account.yaml create mode 100644 tests/hack/ci/resources/charts/compass-runtime-agent/templates/service.yaml create mode 100644 tests/hack/ci/resources/charts/compass-runtime-agent/templates/skr-configmap.yaml create mode 100644 tests/hack/ci/resources/charts/compass-runtime-agent/values.yaml 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/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/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-agent-tests.yml b/.github/workflows/kyma-integration-k3d-agent-tests.yml new file mode 100644 index 00000000..fe9d333a --- /dev/null +++ b/.github/workflows/kyma-integration-k3d-agent-tests.yml @@ -0,0 +1,37 @@ +name: Run compass-rt-agemt 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: Set up cache + uses: actions/cache@v3 + with: + path: | + ~/.cache/go-build + ~/go/pkg/mod + /home/runner/work/application-connector-manager/application-connector-manager/bin + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + - name: Run unit tests + run: make -C tests/hack/ci k3d-agent-tests + - name: Archive test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: app-gateway-test-results + path: compass-runtime-agent-test.log + 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..bb9886fb --- /dev/null +++ b/.github/workflows/kyma-integration-k3d-app-gateway.yml @@ -0,0 +1,36 @@ +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: Set up cache + uses: actions/cache@v3 + with: + path: | + ~/.cache/go-build + ~/go/pkg/mod + /home/runner/work/application-connector-manager/application-connector-manager/bin + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + - name: Run unit tests + run: make -C tests/hack/ci k3d-gateway-tests + - name: Archive test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: app-gateway-test-results + path: application-gateway-test.log 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/pkg/crypto/sha256/utils_test.go b/pkg/crypto/sha256/utils_test.go index caa42092..b85e825d 100644 --- a/pkg/crypto/sha256/utils_test.go +++ b/pkg/crypto/sha256/utils_test.go @@ -54,7 +54,7 @@ func Test_calculateSHA256(t *testing.T) { u.SetGroupVersionKind(schema.GroupVersionKind{ Kind: "CustomResourceDefinition", Group: "apiextensions.k8s.io", - Version: "v1", + Version: "1.0.0", }) return u 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..92ed1713 --- /dev/null +++ b/tests/Makefile @@ -0,0 +1,38 @@ +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)" + +REGISTRY_PORT ?= 5001 + +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) + +.PHONY: image-compass-runtime-agent-test +image-compass-runtime-agent-test: + @echo "::group::image-compass-runtime-agent-test" + @docker build -t $(COMPASS_TEST_IMAGE) -f ${PWD}/tests/Dockerfile.compass-runtime-agent . + @echo "::endgroup::" + +.PHONY: compass-runtime-agent-test-image +compass-runtime-agent-test-image: \ + image-compass-runtime-agent-test \ + publish-compass-runtime-agent-test 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..29ca21d0 --- /dev/null +++ b/tests/Makefile.test-application-gateway @@ -0,0 +1,80 @@ +# -*- 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" +TEST_LOG = ${PWD}/application-gateway-test.log +MAKEFILE_NAME=Makefile.test-application-gateway + +.PHONY: test +test: test-gateway + +.PHONY: clean +clean: clean-gateway-test + +.PHONY: test-gateway +test-gateway: disable-sidecar-for-mtls-test generate-certs create-resources + @echo "::group::test-gateway" + kubectl wait --for=condition=complete --timeout=$(TEST_TIMEOUT) -n $(NAMESPACE) job/application-gateway-test + kubectl -n $(NAMESPACE) -l batch.kubernetes.io/job-name=application-gateway-test logs 2>&1 > $(TEST_LOG) + @echo "::endgroup::" + +.PHONY: create-resources +create-resources: + @echo "::group::create-test-namespace" + kubectl create namespace $(NAMESPACE) --dry-run=client -o yaml | kubectl apply -f - + kubectl label namespace $(NAMESPACE) istio-injection=enabled --overwrite + @echo "::endgroup::" + @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::" + +.PHONY: clean-gateway-test +clean-gateway-test: + @echo "::group::clean-gateway-test" + helm template ${PWD}/tests/resources/charts/gateway-test --set namespace=$(NAMESPACE) \ + | kubectl delete -f - + kubectl delete ns $(NAMESPACE) --ignore-not-found + @echo "::endgroup::" + +.PHONY: disable-sidecar-for-mtls-test +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::" + +.PHONY: enable-sidecar-after-mtls-test +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::" + +.PHONY: generate-certs +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..f887c3a1 --- /dev/null +++ b/tests/Makefile.test-compass-runtime-agent @@ -0,0 +1,53 @@ +# -*- 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 + +TEST_TIMEOUT = "3m" +CLUSTER_NAME ?= kyma +REGISTRY_PORT ?= 5001 +REGISTRY_NAME ?= ${CLUSTER_NAME}-registry +TEST_LOG = ${PWD}/compass-runtime-agent-test.log + +.PHONY: test +test: test-compass-runtime-agent clean-compass-runtime-agent-test + +.PHONY: clean +clean: clean-compass-runtime-agent-test + +.PHONY: test-compass-runtime-agent +test-compass-runtime-agent: install-compass-runtime-agent-test + @echo "::group::test-compass-runtime-agent" + kubectl wait --for=condition=complete --timeout=$(TEST_TIMEOUT) -n $(NAMESPACE) job/compass-runtime-agent-test; \ + kubectl -n $(NAMESPACE) -l batch.kubernetes.io/job-name=compass-runtime-agent-test logs 2>&1 > $(TEST_LOG) + @echo "::endgroup::" + +.PHONY: create-resources +create-resources: + @echo "::group::create-test-namespace" + kubectl create namespace $(NAMESPACE) --dry-run=client -o yaml | kubectl apply -f - + kubectl label namespace $(NAMESPACE) istio-injection=enabled --overwrite + @echo "::endgroup::" + +.PHONY: install-compass-runtime-agent-test +install-compass-runtime-agent-test: create-resources + @echo "::group::install-compass-runtime-agent-test" + @echo "::add-mask::$(TOKENS_ENDPOINT)" + @echo "::add-mask::$(DIRECTOR_URL)" + @echo "::add-mask::$(COMPASS_CLIENT_ID)" + @echo "::add-mask::$(COMPASS_CLIENT_SECRET)" + @helm template ${PWD}/tests/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 "::endgroup::" + +.PHONY: clean-compass-runtime-agent-test +clean-compass-runtime-agent-test: + helm template ${PWD}/tests/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..cdcea9ae --- /dev/null +++ b/tests/hack/ci/Makefile @@ -0,0 +1,126 @@ +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: application-connector-module-image +application-connector-module-image: + @echo "::group::application-connector-module-image" + @make -C ${PROJECT_ROOT}/hack/common module-image + @echo "::endgroup::" + +.PHONY: application-connector-deploy +application-connector-deploy: + @echo "::group::application-connector-deploy" + @make -C ${PROJECT_ROOT}/hack/common deploy \ + IMG=k3d-${REGISTRY_NAME}:${REGISTRY_PORT}/${MANAGER_IMAGE_NAME}:${MANAGER_IMAGE_TAG} + @echo "::endgroup::" + +.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 + @echo "::group::apply-appcon" + @make -C ${PROJECT_ROOT}/hack/common apply-appcon + @echo "::endgroup::" + +.PHONY: apply-appcon-crd +apply-appcon-crd: ## Apply the application-connector CRD + @echo "::group::apply-appcon-crd" + kubectl apply -f ${PROJECT_ROOT}/tests/hack/ci/deps/applications.applicationconnector.crd.yaml + @echo "::endgroup::" + +.PHONY: apply-compass-connection-crd +apply-compass-connection-crd: ## Apply the compass-connection CRD + @echo "::group::apply-compas-connection-crd" + kubectl apply -f ${PROJECT_ROOT}/tests/hack/ci/deps/compass-connection.crd.yaml + @echo "::endgroup::" + +.PHONY: gateway-tests +gateway-tests: + @echo "::group::gateway-tests" + @make -f ${PROJECT_ROOT}/tests/Makefile.test-application-gateway test + @echo "::endgroup::" + +.PHONY: k3d-gateway-tests +k3d-gateway-tests: create-k3d install-istio install-application-connector apply-appcon-crd 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 + +.PHONY: k3d-agent-tests +k3d-agent-tests: create-k3d \ + install-istio \ + install-application-connector \ + apply-compass-connection-crd \ + apply-appcon-crd \ + compass-runtime-agent-test-image \ + install-compass-runtime-agent \ + agent-tests + +.PHONY: agent-tests +agent-tests: + @echo "::group::agent-tests" + @make -f ${PROJECT_ROOT}/tests/Makefile.test-compass-runtime-agent test + @echo "::endgroup::" + +.PHONY: compass-runtime-agent-test-image +compass-runtime-agent-test-image: + @echo "::group::compass-runtime-agent-test-image" + @make -C ${PROJECT_ROOT}/tests compass-runtime-agent-test-image \ + DOCKER_PUSH_REPOSITORY=localhost:${REGISTRY_PORT} \ + DOCKER_TAG=002 + @echo "::endgroup::" + +.PHONY: install-compass-runtime-agent +install-compass-runtime-agent: compass-runtime-agent-test-image + @echo "::group::install-compass-runtime-agent" + @helm template ${PROJECT_ROOT}/tests/hack/ci/resources/charts/compass-runtime-agent \ + -n kyma-system \ + | kubectl apply -f - + @echo "::endgroup::" 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/compass-connection.crd.yaml b/tests/hack/ci/deps/compass-connection.crd.yaml new file mode 100644 index 00000000..aae2958a --- /dev/null +++ b/tests/hack/ci/deps/compass-connection.crd.yaml @@ -0,0 +1,144 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + "helm.sh/resource-policy": keep + name: compassconnections.compass.kyma-project.io +spec: + group: compass.kyma-project.io + names: + kind: CompassConnection + listKind: CompassConnectionList + plural: compassconnections + singular: compassconnection + scope: Cluster + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + 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: + managementInfo: + properties: + connectorUrl: + description: 'URL used for maintaining the secure connection.' + type: string + directorUrl: + description: 'URL used for fetching Applications.' + type: string + required: + - connectorUrl + - directorUrl + type: object + refreshCredentialsNow: + description: 'If set to `true`, ignores certificate expiration date and refreshes in the next round.' + type: boolean + resyncNow: + description: 'If set to `true`, ignores `APP_MINIMAL_COMPASS_SYNC_TIME` and syncs in the next round.' + type: boolean + required: + - managementInfo + type: object + status: + properties: + connectionState: + type: string + connectionStatus: + description: 'Represents the status of the connection to + Compass.' + properties: + certificateStatus: + description: 'Specifies the certificate issue and expiration dates.' + properties: + acquired: + description: 'Specifies when the certificate was acquired.' + format: date-time + nullable: true + type: string + notAfter: + description: 'Specifies when the certificate stops being valid.' + format: date-time + nullable: true + type: string + notBefore: + description: 'Specifies when the certificate becomes valid.' + format: date-time + nullable: true + type: string + type: object + error: + type: string + established: + description: 'Specifies when the connection was established.' + format: date-time + nullable: true + type: string + lastSuccess: + description: 'Specifies the date of the last successful synchronization with the Connector.' + format: date-time + nullable: true + type: string + lastSync: + description: 'Specifies the date of the last synchronization attempt.' + format: date-time + nullable: true + type: string + renewed: + description: 'Specifies the date of the last certificate renewal.' + format: date-time + nullable: true + type: string + required: + - certificateStatus + type: object + synchronizationStatus: + description: 'Provides the status of the synchronization with the Director.' + nullable: true + properties: + error: + type: string + lastAttempt: + description: 'Specifies the date of the last synchronization attempt with the Director.' + format: date-time + nullable: true + type: string + lastSuccessfulApplication: + description: 'Specifies the date of the last successful application of resources fetched from Compass.' + format: date-time + nullable: true + type: string + lastSuccessfulFetch: + description: 'Specifies the date of the last successful fetch of resources from the Director.' + format: date-time + nullable: true + type: string + type: object + required: + - connectionState + - connectionStatus + type: object + required: + - spec + type: object + served: true + storage: true +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/tests/hack/ci/resources/charts/compass-runtime-agent/.helmignore b/tests/hack/ci/resources/charts/compass-runtime-agent/.helmignore new file mode 100644 index 00000000..f0c13194 --- /dev/null +++ b/tests/hack/ci/resources/charts/compass-runtime-agent/.helmignore @@ -0,0 +1,21 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj diff --git a/tests/hack/ci/resources/charts/compass-runtime-agent/Chart.yaml b/tests/hack/ci/resources/charts/compass-runtime-agent/Chart.yaml new file mode 100644 index 00000000..dfbe15f9 --- /dev/null +++ b/tests/hack/ci/resources/charts/compass-runtime-agent/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +description: Kyma component 'compass-runtime-agent' +name: compass-runtime-agent +version: 1.0.0 +home: https://kyma-project.io +icon: https://github.com/kyma-project/kyma/blob/main/logo.png?raw=true diff --git a/tests/hack/ci/resources/charts/compass-runtime-agent/templates/_helpers.tpl b/tests/hack/ci/resources/charts/compass-runtime-agent/templates/_helpers.tpl new file mode 100644 index 00000000..a5e2cedf --- /dev/null +++ b/tests/hack/ci/resources/charts/compass-runtime-agent/templates/_helpers.tpl @@ -0,0 +1,18 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a URL for container images +*/}} +{{- define "imageurl" -}} +{{- $registry := default $.reg.path $.img.containerRegistryPath -}} +{{- $path := ternary (print $registry) (print $registry "/" $.img.directory) (empty $.img.directory) -}} +{{- $version := ternary (print ":" $.img.version) (print "@sha256:" $.img.sha) (empty $.img.sha) -}} +{{- print $path "/" $.img.name $version -}} +{{- end -}} + diff --git a/tests/hack/ci/resources/charts/compass-runtime-agent/templates/cluster-role-binding.yaml b/tests/hack/ci/resources/charts/compass-runtime-agent/templates/cluster-role-binding.yaml new file mode 100644 index 00000000..c5f50e41 --- /dev/null +++ b/tests/hack/ci/resources/charts/compass-runtime-agent/templates/cluster-role-binding.yaml @@ -0,0 +1,48 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ .Chart.Name }} + labels: + app: {{ .Chart.Name }} + release: {{ .Release.Name }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/name: {{ template "name" . }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/instance: {{ .Release.Name }} +rules: + - apiGroups: ["compass.kyma-project.io"] + resources: ["compassconnections"] + verbs: ["create", "get", "list", "update", "delete", "watch"] + - apiGroups: ["applicationconnector.kyma-project.io"] + resources: ["applications"] + verbs: ["get", "list", "create", "update", "delete"] + - apiGroups: [""] + resources: ["nodes", "persistentvolumes"] + verbs: ["get", "list"] + - apiGroups: ["metrics.k8s.io"] + resources: ["nodes"] + verbs: ["get", "list"] + - apiGroups: [""] + resources: ["secrets"] + resourceNames: ["compass-agent-configuration","cluster-client-certificates"] + verbs: ["get", "delete"] +--- +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: {{ .Chart.Name }} + labels: + app: {{ .Chart.Name }} + release: {{ .Chart.Name }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/name: {{ template "name" . }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/instance: {{ .Release.Name }} +subjects: + - kind: ServiceAccount + name: {{ .Chart.Name }} + namespace: {{ .Release.Namespace }} +roleRef: + kind: ClusterRole + name: {{ .Chart.Name }} + apiGroup: rbac.authorization.k8s.io diff --git a/tests/hack/ci/resources/charts/compass-runtime-agent/templates/deployment.yaml b/tests/hack/ci/resources/charts/compass-runtime-agent/templates/deployment.yaml new file mode 100644 index 00000000..d71d76de --- /dev/null +++ b/tests/hack/ci/resources/charts/compass-runtime-agent/templates/deployment.yaml @@ -0,0 +1,115 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Chart.Name }} + namespace: {{ .Release.Namespace }} + labels: + app: {{ .Chart.Name }} + release: {{ .Release.Name }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/name: {{ template "name" . }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/instance: {{ .Release.Name }} +spec: + selector: + matchLabels: + app: {{ .Chart.Name }} + release: {{ .Release.Name }} + template: + metadata: + annotations: + sidecar.istio.io/inject: "true" + labels: + app: {{ .Chart.Name }} + release: {{ .Release.Name }} + spec: + securityContext: + runAsUser: 65535 + runAsGroup: 65535 + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + serviceAccountName: {{ .Chart.Name }} + containers: + - name: {{ .Chart.Name }} + securityContext: + privileged: false + allowPrivilegeEscalation: false + runAsNonRoot: true + capabilities: + drop: + - ALL + procMount: default + readOnlyRootFilesystem: true + ports: + - containerPort: {{ .Values.compassRuntimeAgent.healthCheck.port }} + hostPort: 0 + name: http-health + image: {{ include "imageurl" (dict "reg" .Values.global.containerRegistry "img" .Values.global.images.compass_runtime_agent) }} + imagePullPolicy: {{ .Values.compassRuntimeAgent.image.pullPolicy }} + args: + - "/app/compass-runtime-agent" + env: + - name: APP_AGENT_CONFIGURATION_SECRET + value: "{{ .Values.compassRuntimeAgent.config.secret.namespace }}/{{ .Values.compassRuntimeAgent.config.secret.name }}" + - name: APP_CONTROLLER_SYNC_PERIOD + value: {{ .Values.compassRuntimeAgent.sync.controllerSyncPeriod | quote }} + - name: APP_MINIMAL_COMPASS_SYNC_TIME + value: {{ .Values.compassRuntimeAgent.sync.minimalConfigSyncTime | quote }} + - name: APP_CERT_VALIDITY_RENEWAL_THRESHOLD + value: {{ .Values.compassRuntimeAgent.certificates.renewal.validityThreshold | quote }} + - name: APP_CLUSTER_CERTIFICATES_SECRET + value: "{{ .Values.compassRuntimeAgent.certificates.clientCertificate.secret.namespace }}/{{ .Values.compassRuntimeAgent.certificates.clientCertificate.secret.name }}" + - name: APP_CA_CERTIFICATES_SECRET + value: "{{ .Values.compassRuntimeAgent.certificates.caCertificate.secret.namespace }}/{{ .Values.compassRuntimeAgent.certificates.caCertificate.secret.name }}" + - name: APP_SKIP_COMPASS_TLS_VERIFY + value: {{ .Values.compassRuntimeAgent.compass.skipTLSVerification | quote }} + - name: APP_SKIP_APPS_TLS_VERIFY + value: {{ .Values.compassRuntimeAgent.config.skipAppsTLSVerification | quote }} + - name: APP_GATEWAY_PORT + value: {{ .Values.compassRuntimeAgent.resources.gatewayPort | quote }} + - name: APP_UPLOAD_SERVICE_URL + value: {{ .Values.compassRuntimeAgent.resources.uploadServiceUrl | quote }} + - name: APP_QUERY_LOGGING + value: {{ .Values.compassRuntimeAgent.debug.queryLogging | quote }} + - name: APP_METRICS_LOGGING_TIME_INTERVAL + value: {{ .Values.compassRuntimeAgent.metrics.loggingTimeInterval | quote }} + - name: APP_RUNTIME_EVENTS_URL + value: "https://gateway.{{ .Values.global.domainName }}" + - name: APP_RUNTIME_CONSOLE_URL + value: "https://console.{{ .Values.global.domainName }}" + - name: APP_HEALTH_PORT + value: {{ .Values.compassRuntimeAgent.healthCheck.port | quote }} + {{ if .Values.compassRuntimeAgent.certificates.caCertificate.secret.migration}} + - name: APP_CA_CERT_SECRET_TO_MIGRATE + value: "{{ .Values.compassRuntimeAgent.certificates.caCertificate.secret.namespace }}/{{ .Values.compassRuntimeAgent.certificates.caCertificate.secret.migration.name | default "" }}" + - name: APP_CA_CERT_SECRET_KEYS_TO_MIGRATE + value: '{{ .Values.compassRuntimeAgent.certificates.caCertificate.secret.migration.keys | default "[]" | toJson }}' + {{ end }} + {{ if .Values.compassRuntimeAgent.config.secret.migration.enabled }} + - name: APP_AGENT_CONFIGURATION_SECRET_TO_MIGRATE + value: "{{ .Values.compassRuntimeAgent.config.secret.migration.namespace }}/{{ .Values.compassRuntimeAgent.config.secret.name | default "" }}" + {{ end }} + {{ if .Values.compassRuntimeAgent.certificates.clientCertificate.secret.migration.enabled }} + - name: APP_CLUSTER_CERTIFICATES_SECRET_TO_MIGRATE + value: "{{ .Values.compassRuntimeAgent.certificates.clientCertificate.secret.migration.namespace }}/{{ .Values.compassRuntimeAgent.certificates.clientCertificate.secret.name | default "" }}" + {{ end }} + - name: APP_CENTRAL_GATEWAY_SERVICE_URL + value: {{ .Values.compassRuntimeAgent.resources.centralGatewayServiceUrl | quote }} + livenessProbe: + httpGet: + port: {{ .Values.compassRuntimeAgent.healthCheck.port }} + path: "/healthz" + initialDelaySeconds: {{ .Values.compassRuntimeAgent.livenessProbe.initialDelaySeconds }} + timeoutSeconds: {{ .Values.compassRuntimeAgent.livenessProbe.timeoutSeconds }} + periodSeconds: {{.Values.compassRuntimeAgent.livenessProbe.periodSeconds }} + readinessProbe: + httpGet: + port: {{.Values.compassRuntimeAgent.healthCheck.port }} + path: "/healthz" + initialDelaySeconds: {{ .Values.compassRuntimeAgent.readinessProbe.initialDelaySeconds }} + timeoutSeconds: {{ .Values.compassRuntimeAgent.readinessProbe.timeoutSeconds }} + periodSeconds: {{.Values.compassRuntimeAgent.readinessProbe.periodSeconds }} + {{- if .Values.priorityClassName }} + priorityClassName: {{ .Values.priorityClassName }} + {{- end }} diff --git a/tests/hack/ci/resources/charts/compass-runtime-agent/templates/priority-class.yaml b/tests/hack/ci/resources/charts/compass-runtime-agent/templates/priority-class.yaml new file mode 100644 index 00000000..037e0819 --- /dev/null +++ b/tests/hack/ci/resources/charts/compass-runtime-agent/templates/priority-class.yaml @@ -0,0 +1,7 @@ +apiVersion: scheduling.k8s.io/v1 +kind: PriorityClass +metadata: + name: {{ .Values.priorityClassName }} +value: 2000000 +globalDefault: false +description: "Scheduling priority of compass-runtime-agent component. By default, compass-runtime-agent should not be blocked by unschedulable user workloads." \ No newline at end of file diff --git a/tests/hack/ci/resources/charts/compass-runtime-agent/templates/resources-role-binding.yaml b/tests/hack/ci/resources/charts/compass-runtime-agent/templates/resources-role-binding.yaml new file mode 100644 index 00000000..b6c5c3f3 --- /dev/null +++ b/tests/hack/ci/resources/charts/compass-runtime-agent/templates/resources-role-binding.yaml @@ -0,0 +1,40 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ .Chart.Name }} + namespace: {{ .Values.compassRuntimeAgent.resources.systemNamespace }} + labels: + app: {{ .Chart.Name }} + release: {{ .Chart.Name }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/name: {{ template "name" . }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/instance: {{ .Release.Name }} +rules: + - apiGroups: [""] + resources: ["services"] + verbs: ["create", "get", "delete"] + - apiGroups: [""] + resources: ["secrets"] + verbs: ["create", "get", "update", "delete"] +--- +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: {{ .Chart.Name }} + namespace: {{ .Values.compassRuntimeAgent.resources.systemNamespace }} + labels: + app: {{ .Chart.Name }} + release: {{ .Chart.Name }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/name: {{ template "name" . }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/instance: {{ .Release.Name }} +subjects: + - kind: ServiceAccount + name: {{ .Chart.Name }} + namespace: {{ .Release.Namespace }} +roleRef: + kind: Role + name: {{ .Chart.Name }} + apiGroup: rbac.authorization.k8s.io diff --git a/tests/hack/ci/resources/charts/compass-runtime-agent/templates/role-binding.yaml b/tests/hack/ci/resources/charts/compass-runtime-agent/templates/role-binding.yaml new file mode 100644 index 00000000..0c571918 --- /dev/null +++ b/tests/hack/ci/resources/charts/compass-runtime-agent/templates/role-binding.yaml @@ -0,0 +1,76 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ .Chart.Name }}-client-cert-role + namespace: {{ .Values.compassRuntimeAgent.certificates.clientCertificate.secret.namespace | default "default" }} + labels: + app: {{ .Chart.Name }} + release: {{ .Chart.Name }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/name: {{ template "name" . }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/instance: {{ .Release.Name }} +rules: + - apiGroups: [""] + resources: ["secrets"] + verbs: ["create", "get", "update", "delete"] +--- +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: {{ .Chart.Name }}-client-cert-rolebinding + namespace: {{ .Values.compassRuntimeAgent.certificates.clientCertificate.secret.namespace | default "default" }} + labels: + app: {{ .Chart.Name }} + release: {{ .Release.Name }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/name: {{ template "name" . }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/instance: {{ .Release.Name }} +subjects: + - kind: ServiceAccount + name: {{ .Chart.Name }} + namespace: {{ .Release.Namespace }} +roleRef: + kind: Role + name: {{ .Chart.Name }}-client-cert-role + apiGroup: rbac.authorization.k8s.io +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ .Chart.Name }}-ca-cert-role + namespace: {{ .Values.compassRuntimeAgent.certificates.caCertificate.secret.namespace | default "default" }} + labels: + app: {{ .Chart.Name }} + release: {{ .Chart.Name }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/name: {{ template "name" . }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/instance: {{ .Release.Name }} +rules: + - apiGroups: [""] + resources: ["secrets"] + verbs: ["create", "get", "update", "delete"] +--- +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: {{ .Chart.Name }}-ca-cert-rolebinding + namespace: {{ .Values.compassRuntimeAgent.certificates.caCertificate.secret.namespace | default "default" }} + labels: + app: {{ .Chart.Name }} + release: {{ .Chart.Name }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/name: {{ template "name" . }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/instance: {{ .Release.Name }} +subjects: + - kind: ServiceAccount + name: {{ .Chart.Name }} + namespace: {{ .Release.Namespace }} +roleRef: + kind: Role + name: {{ .Chart.Name }}-ca-cert-role + apiGroup: rbac.authorization.k8s.io + diff --git a/tests/hack/ci/resources/charts/compass-runtime-agent/templates/service-account.yaml b/tests/hack/ci/resources/charts/compass-runtime-agent/templates/service-account.yaml new file mode 100644 index 00000000..3a930dc8 --- /dev/null +++ b/tests/hack/ci/resources/charts/compass-runtime-agent/templates/service-account.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ .Chart.Name }} + namespace: {{ .Release.Namespace }} + labels: + app: {{ .Chart.Name }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/name: {{ template "name" . }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/instance: {{ .Release.Name }} \ No newline at end of file diff --git a/tests/hack/ci/resources/charts/compass-runtime-agent/templates/service.yaml b/tests/hack/ci/resources/charts/compass-runtime-agent/templates/service.yaml new file mode 100644 index 00000000..119457d8 --- /dev/null +++ b/tests/hack/ci/resources/charts/compass-runtime-agent/templates/service.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Chart.Name }}-service + namespace: {{ .Values.global.namespace }} + labels: + control-plane: {{ .Chart.Name }} + controller-tools.k8s.io: "1.0" + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/name: {{ template "name" . }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/instance: {{ .Release.Name }} +spec: + selector: + control-plane: {{ .Chart.Name }} + controller-tools.k8s.io: "1.0" + ports: + - port: 443 + diff --git a/tests/hack/ci/resources/charts/compass-runtime-agent/templates/skr-configmap.yaml b/tests/hack/ci/resources/charts/compass-runtime-agent/templates/skr-configmap.yaml new file mode 100644 index 00000000..387d761b --- /dev/null +++ b/tests/hack/ci/resources/charts/compass-runtime-agent/templates/skr-configmap.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: skr-configmap + namespace: {{ .Values.global.skrConfigmapNamespace }} +data: + is-managed-kyma-runtime: "true" diff --git a/tests/hack/ci/resources/charts/compass-runtime-agent/values.yaml b/tests/hack/ci/resources/charts/compass-runtime-agent/values.yaml new file mode 100644 index 00000000..dbad8ab5 --- /dev/null +++ b/tests/hack/ci/resources/charts/compass-runtime-agent/values.yaml @@ -0,0 +1,89 @@ +global: + domainName: kyma.example.com + containerRegistry: + path: europe-docker.pkg.dev/kyma-project + images: + compass_runtime_agent: + name: "compass-runtime-agent" + version: "v20231218-ff3777c4" + directory: "prod" + istio: + gateway: + name: kyma-gateway + namespace: kyma-system + skrConfigmapNamespace: kyma-system + +managementPlane: {} # default value + +priorityClassName: "compass-runtime-agent-priority-class" + +compassRuntimeAgent: + image: + pullPolicy: IfNotPresent + sync: + controllerSyncPeriod: 180s + minimalConfigSyncTime: 15s + resources: + systemNamespace: "kyma-system" + dexSecretNamespace: "kyma-system" + dexSecretName: "admin-user" + gatewayPort: 8080 + centralGatewayServiceUrl: http://central-application-gateway.kyma-system.svc.cluster.local:8082 + config: + insecureConfigurationFetch: true + skipAppsTLSVerification: false + secret: + name: compass-agent-configuration + namespace: kyma-system + migration: + namespace: compass-system + enabled: true + certificates: + renewal: + validityThreshold: "0.3" + clientCertificate: + secret: + name: cluster-client-certificates + namespace: kyma-system + migration: + namespace: compass-system + enabled: true + caCertificate: + secret: + name: kyma-gateway-certs-cacert + namespace: istio-system + migration: + name: app-connector-certs + keys: ["cacert"] + compass: + skipTLSVerification: true + debug: + queryLogging: false + metrics: + loggingTimeInterval: 30m + healthCheck: + port: 8090 + proxyStatusPort: 15020 + tests: + labels: + integration: true + after-upgrade: true + enabled: true + mockService: + port: 8080 + configApplicationWaitTime: 50s + proxyInvalidationWaitTime: 120s + applicationInstallationTimeout: 180s + graphqlLogs: false + director: + url: "https://compass-gateway.{{ .Values.global.domainName }}/director/graphql" + idProvider: + clientTimeout: 10s + livenessProbe: + initialDelaySeconds: 50 + timeoutSeconds: 1 + periodSeconds: 10 + readinessProbe: + initialDelaySeconds: 10 + timeoutSeconds: 1 + periodSeconds: 2 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..46b02b1e --- /dev/null +++ b/tests/resources/charts/compass-runtime-agent-test/values.yaml @@ -0,0 +1,19 @@ +namespace: "test" +testTenant: "3e64ebae-38b5-46a0-b1ed-9ccee153a0ae" +oauthCredentialsSecretName: "oauth-compass-credentials" +oauthCredentialsNamespace: "test" +skipDirectorCertVerification: true +serviceAccountName: "test-compass-runtime-agent" + +containerRegistry: + path: "k3d-kyma-registry:5000" + +images: + compassTest: + name: "compass-runtime-agent-test" + version: "002" + +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/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/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..c7cec324 --- /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: "1.0.0" + 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..a845175d --- /dev/null +++ b/tests/test/compass-runtime-agent/testkit/oauth/client.go @@ -0,0 +1,120 @@ +package oauth + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/http/httputil" + "net/url" + "strings" + "time" + + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/client-go/kubernetes/typed/core/v1" +) + +//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, + } +} From bb37c368d2ee1e12bedce77e7bd873b4ed60ed90 Mon Sep 17 00:00:00 2001 From: m00g3n Date: Mon, 18 Mar 2024 11:41:14 +0100 Subject: [PATCH 02/11] fix tests --- pkg/crypto/sha256/utils_test.go | 2 +- pkg/reconciler/fn_state_matcher_test.go | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/crypto/sha256/utils_test.go b/pkg/crypto/sha256/utils_test.go index b85e825d..71a88b3b 100644 --- a/pkg/crypto/sha256/utils_test.go +++ b/pkg/crypto/sha256/utils_test.go @@ -60,7 +60,7 @@ func Test_calculateSHA256(t *testing.T) { return u }(), }, - want: "9NtR-1kpz4ub0a8jS4YySJEGZKmPfvC5FLh5GNW5UlA=", + want: "YVinn88v80IHE3oiOSjwAPUG0OumBQ-dy1ypGbAounU=", }, } for _, tt := range tests { diff --git a/pkg/reconciler/fn_state_matcher_test.go b/pkg/reconciler/fn_state_matcher_test.go index e5a37cdf..81cf9d6a 100644 --- a/pkg/reconciler/fn_state_matcher_test.go +++ b/pkg/reconciler/fn_state_matcher_test.go @@ -50,6 +50,8 @@ func (m *stateFnNameMatcher) Match(actual any) (success bool, err error) { m.expName = m.expName[eliof+1:] } + fmt.Println("match:", m.actName, m.expName, m.Expected.name()) + return m.actName == m.expName, nil } From 911b1a7ec6cd1a5844318f4d51790f00641f559c Mon Sep 17 00:00:00 2001 From: m00g3n Date: Mon, 18 Mar 2024 11:59:38 +0100 Subject: [PATCH 03/11] update dependencies --- Dockerfile | 2 +- application-gateway-test.log | 10 +++++ go.mod | 3 +- go.sum | 5 +-- tests/Dockerfile.compass-runtime-agent | 4 +- tests/Dockerfile.connectivity-validator | 4 +- tests/Dockerfile.gateway | 4 +- tests/Dockerfile.mockapp | 4 +- tests/go.mod | 17 ++++---- tests/go.sum | 53 +++++++++---------------- 10 files changed, 49 insertions(+), 57 deletions(-) create mode 100644 application-gateway-test.log diff --git a/Dockerfile b/Dockerfile index c00b44fc..a291a9eb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build the manager binary -FROM golang:1.21.8 as builder +FROM golang:1.21.1 as builder WORKDIR /workspace diff --git a/application-gateway-test.log b/application-gateway-test.log new file mode 100644 index 00000000..36cd3ccb --- /dev/null +++ b/application-gateway-test.log @@ -0,0 +1,10 @@ + --- PASS: TestGatewaySuite/TestGetRequest/Code_Rewriting (0.09s) + --- PASS: TestGatewaySuite/TestGetRequest/Code_Rewriting/Should_return_502_given_500 (0.01s) + --- PASS: TestGatewaySuite/TestGetRequest/Code_Rewriting/Should_return_502_given_503 (0.06s) + --- PASS: TestGatewaySuite/TestGetRequest/Code_Rewriting/Should_return_502_given_502 (0.01s) + --- PASS: TestGatewaySuite/TestGetRequest/Code_Rewriting/Should_return_200_given_123 (0.01s) + --- PASS: TestGatewaySuite/TestResponseBody (0.28s) + --- PASS: TestGatewaySuite/TestResponseBody/Should_return_451_forwarded_from_target_endpoint (0.00s) + --- PASS: TestGatewaySuite/TestResponseBody/Should_return_307_forwarded_from_target_endpoint (0.06s) + --- PASS: TestGatewaySuite/TestResponseBody/Should_return_203_forwarded_from_target_endpoint (0.21s) +PASS diff --git a/go.mod b/go.mod index 7d34dcfc..44714d03 100644 --- a/go.mod +++ b/go.mod @@ -80,9 +80,10 @@ require ( ) replace ( - golang.org/x/crypto => golang.org/x/crypto v0.12.0 + golang.org/x/crypto => golang.org/x/crypto v0.17.0 golang.org/x/net => golang.org/x/net v0.17.0 golang.org/x/sys => golang.org/x/sys v0.11.0 golang.org/x/text => golang.org/x/text v0.12.0 golang.org/x/tools => golang.org/x/tools v0.12.0 + google.golang.org/protobuf => google.golang.org/protobuf v1.33.0 ) diff --git a/go.sum b/go.sum index b5809220..70259eef 100644 --- a/go.sum +++ b/go.sum @@ -117,7 +117,7 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= @@ -131,7 +131,6 @@ golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= @@ -150,8 +149,6 @@ google.golang.org/genproto v0.0.0-20231002182017-d307bd883b97 h1:SeZZZx0cP0fqUyA google.golang.org/genproto v0.0.0-20231002182017-d307bd883b97/go.mod h1:t1VqOqqvce95G3hIDCT5FeO3YUc6Q4Oe24L/+rNMxRk= google.golang.org/genproto/googleapis/api v0.0.0-20230920204549-e6e6cdab5c13 h1:U7+wNaVuSTaUqNvK2+osJ9ejEZxbjHHk8F2b6Hpx0AE= google.golang.org/genproto/googleapis/api v0.0.0-20230920204549-e6e6cdab5c13/go.mod h1:RdyHbowztCGQySiCvQPgWQWgWhGnouTdCflKoDBt32U= -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.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/tests/Dockerfile.compass-runtime-agent b/tests/Dockerfile.compass-runtime-agent index 08813029..ecb50750 100644 --- a/tests/Dockerfile.compass-runtime-agent +++ b/tests/Dockerfile.compass-runtime-agent @@ -1,5 +1,5 @@ -# image builder base on golang:1.21.1-alpine3.18 -FROM golang@sha256:0c860c7ceba62231d0f99fb92e9d7c1577f26fea794a12c75756a8f64b146e45 as builder +# image builder base on golang:1.22.1-alpine3.19 +FROM golang@sha256:0466223b8544fb7d4ff04748acc4d75a608234bf4e79563bff208d2060c0dd79 as builder WORKDIR /compass-test/ diff --git a/tests/Dockerfile.connectivity-validator b/tests/Dockerfile.connectivity-validator index 85aa6c17..1fd553c8 100644 --- a/tests/Dockerfile.connectivity-validator +++ b/tests/Dockerfile.connectivity-validator @@ -1,5 +1,5 @@ -# image builder base on golang:1.21.1-alpine3.18 -FROM golang@sha256:0c860c7ceba62231d0f99fb92e9d7c1577f26fea794a12c75756a8f64b146e45 as builder +# image builder base on golang:1.22.1-alpine3.19 +FROM golang@sha256:0466223b8544fb7d4ff04748acc4d75a608234bf4e79563bff208d2060c0dd79 as builder WORKDIR /validator-test/ diff --git a/tests/Dockerfile.gateway b/tests/Dockerfile.gateway index 9e620a90..ab3046ad 100644 --- a/tests/Dockerfile.gateway +++ b/tests/Dockerfile.gateway @@ -1,5 +1,5 @@ -# image builder base on golang:1.21.1-alpine3.18 -FROM golang@sha256:0c860c7ceba62231d0f99fb92e9d7c1577f26fea794a12c75756a8f64b146e45 as builder +# image builder base on golang:1.22.1-alpine3.19 +FROM golang@sha256:0466223b8544fb7d4ff04748acc4d75a608234bf4e79563bff208d2060c0dd79 as builder WORKDIR /gateway-test/ diff --git a/tests/Dockerfile.mockapp b/tests/Dockerfile.mockapp index 14203dcf..ca62d70c 100644 --- a/tests/Dockerfile.mockapp +++ b/tests/Dockerfile.mockapp @@ -1,5 +1,5 @@ -# image builder base on golang:1.21.1-alpine3.18 -FROM golang@sha256:0c860c7ceba62231d0f99fb92e9d7c1577f26fea794a12c75756a8f64b146e45 as builder +# image builder base on golang:1.22.1-alpine3.19 +FROM golang@sha256:0466223b8544fb7d4ff04748acc4d75a608234bf4e79563bff208d2060c0dd79 as builder WORKDIR /mock-app/ diff --git a/tests/go.mod b/tests/go.mod index e69d3ee7..1b27f217 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -62,12 +62,12 @@ require ( 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/crypto v0.14.0 // indirect + golang.org/x/net v0.10.0 // 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/sys v0.15.0 // indirect + golang.org/x/term v0.15.0 // indirect + golang.org/x/text v0.14.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 @@ -83,7 +83,8 @@ require ( ) 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 + golang.org/x/crypto => golang.org/x/crypto v0.17.0 + golang.org/x/net => golang.org/x/net v0.17.0 + golang.org/x/text => golang.org/x/text v0.12.0 + google.golang.org/protobuf => google.golang.org/protobuf v1.33.0 ) diff --git a/tests/go.sum b/tests/go.sum index d7602a66..b6c0f238 100644 --- a/tests/go.sum +++ b/tests/go.sum @@ -116,10 +116,6 @@ github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y 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= @@ -135,7 +131,6 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw 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= @@ -299,8 +294,8 @@ 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/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= 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= @@ -332,8 +327,9 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB 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/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= 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= @@ -351,6 +347,7 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ 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/sync v0.1.0/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= @@ -377,19 +374,17 @@ golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7w 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/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.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= @@ -440,6 +435,7 @@ golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc 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/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 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= @@ -492,7 +488,6 @@ google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfG 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= @@ -510,20 +505,8 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa 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= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 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= From fe192b8f87134b07f8f40864218262d79e0ecd67 Mon Sep 17 00:00:00 2001 From: m00g3n Date: Mon, 18 Mar 2024 20:04:40 +0100 Subject: [PATCH 04/11] add application connector validator tests --- .../kyma-integration-k3d-validator-tests.yml | 39 +++++++++++ .gitignore | 2 + application-gateway-test.log | 10 --- .../Makefile.test-application-conn-validator | 66 +++++++------------ tests/Makefile.test-application-gateway | 6 +- tests/hack/ci/Makefile | 30 +++++++-- 6 files changed, 93 insertions(+), 60 deletions(-) create mode 100644 .github/workflows/kyma-integration-k3d-validator-tests.yml delete mode 100644 application-gateway-test.log diff --git a/.github/workflows/kyma-integration-k3d-validator-tests.yml b/.github/workflows/kyma-integration-k3d-validator-tests.yml new file mode 100644 index 00000000..ecc19ef2 --- /dev/null +++ b/.github/workflows/kyma-integration-k3d-validator-tests.yml @@ -0,0 +1,39 @@ +name: Run app-con-validator 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: Insall yq + run: sudo add-apt-repository ppa:rmescandon/yq && sudo apt update && sudo apt install yq -y + - name: Set up cache + uses: actions/cache@v3 + with: + path: | + ~/.cache/go-build + ~/go/pkg/mod + /home/runner/work/application-connector-manager/application-connector-manager/bin + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + - name: Run unit tests + run: make -C tests/hack/ci k3d-validator-tests + - name: Archive test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: app-gateway-test-results + path: application-connector-validator-test.log + diff --git a/.gitignore b/.gitignore index d4281c39..64c0e9b0 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,8 @@ vendor/* # Output of the go coverage tool, specifically when used with LiteIDE *.out +application-gateway-test.log +compass-runtime-agent-test.log # Kubernetes Generated files - skip generated files, except for vendored files diff --git a/application-gateway-test.log b/application-gateway-test.log deleted file mode 100644 index 36cd3ccb..00000000 --- a/application-gateway-test.log +++ /dev/null @@ -1,10 +0,0 @@ - --- PASS: TestGatewaySuite/TestGetRequest/Code_Rewriting (0.09s) - --- PASS: TestGatewaySuite/TestGetRequest/Code_Rewriting/Should_return_502_given_500 (0.01s) - --- PASS: TestGatewaySuite/TestGetRequest/Code_Rewriting/Should_return_502_given_503 (0.06s) - --- PASS: TestGatewaySuite/TestGetRequest/Code_Rewriting/Should_return_502_given_502 (0.01s) - --- PASS: TestGatewaySuite/TestGetRequest/Code_Rewriting/Should_return_200_given_123 (0.01s) - --- PASS: TestGatewaySuite/TestResponseBody (0.28s) - --- PASS: TestGatewaySuite/TestResponseBody/Should_return_451_forwarded_from_target_endpoint (0.00s) - --- PASS: TestGatewaySuite/TestResponseBody/Should_return_307_forwarded_from_target_endpoint (0.06s) - --- PASS: TestGatewaySuite/TestResponseBody/Should_return_203_forwarded_from_target_endpoint (0.21s) -PASS diff --git a/tests/Makefile.test-application-conn-validator b/tests/Makefile.test-application-conn-validator index f408cdd7..672a488a 100644 --- a/tests/Makefile.test-application-conn-validator +++ b/tests/Makefile.test-application-conn-validator @@ -6,54 +6,38 @@ 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_LOG = ${PWD}/application-connector-validator-test.log 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 +.PHONY: patch-validator +patch-validator: + yq e -i '. |= (select(.kind == "Deployment" and .metadata.name == "central-application-connectivity-validator") | .spec.template.spec.containers[0].args[5] = "--eventingPublisherHost=echoserver.test.svc.cluster.local" | .spec.template.spec.containers[0].args[6] = "--eventingDestinationPath=/anything/rewrite" | .spec.template.metadata.annotations.["traffic.sidecar.istio.io/excludeInboundPorts"] += "8080")' ${PWD}/application-connector.yaml -test-validator-debug: patch-for-validator-test validator-create-resources +.PHONY: test-validator +test-validator: patch-validator create-resources + @echo "::group::test-validator" 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 -n $(NAMESPACE) -l batch.kubernetes.io/job-name=application-gateway-test logs 2>&1 > $(TEST_LOG) + @echo "::endgroup::" + +.PHONY: create-resources +create-resources: + @echo "::group::create-test-namespace" + 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 \ + @echo "::endgroup::" + @echo "::group::create-resources::install-echoserver" + @helm template ${PWD}/tests/resources/charts/application-connectivity-validator-test/charts/echoserver \ --set global.namespace=$(NAMESPACE) \ - | kubectl apply -f - + | kubectl apply -f - kubectl rollout status deployment echoserver -n test --timeout=90s - - @helm template resources/charts/application-connectivity-validator-test/charts/test \ + @echo "::endgroup::" + @echo "::group::create-resources::install-test" + @helm template ${PWD}/tests/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 - - + --values ${PWD}/tests/resources/charts/application-connectivity-validator-test/values.yaml \ + | kubectl apply -f - + @echo "::endgroup::" diff --git a/tests/Makefile.test-application-gateway b/tests/Makefile.test-application-gateway index 29ca21d0..2b1fc877 100644 --- a/tests/Makefile.test-application-gateway +++ b/tests/Makefile.test-application-gateway @@ -18,8 +18,8 @@ clean: clean-gateway-test .PHONY: test-gateway test-gateway: disable-sidecar-for-mtls-test generate-certs create-resources @echo "::group::test-gateway" - kubectl wait --for=condition=complete --timeout=$(TEST_TIMEOUT) -n $(NAMESPACE) job/application-gateway-test - kubectl -n $(NAMESPACE) -l batch.kubernetes.io/job-name=application-gateway-test logs 2>&1 > $(TEST_LOG) + kubectl wait --for=condition=complete --timeout=$(TEST_TIMEOUT) -n $(NAMESPACE) job/application-gateway-test; \ + kubectl -n $(NAMESPACE) -l batch.kubernetes.io/job-name=application-gateway-test logs 2>&1 > $(TEST_LOG) @echo "::endgroup::" .PHONY: create-resources @@ -36,7 +36,7 @@ create-resources: | kubectl apply -f - kubectl rollout status deployment mock-application -n test --timeout=90s @echo "::endgroup::" - @echo "::group::create-resources::install-mock-app" + @echo "::group::create-resources::install-test" helm template ${PWD}/tests/resources/charts/gateway-test/charts/test \ --set namespace=$(NAMESPACE) \ --set mockServiceName=$(MOCK_SERVICE_NAME) \ diff --git a/tests/hack/ci/Makefile b/tests/hack/ci/Makefile index cdcea9ae..088374cd 100644 --- a/tests/hack/ci/Makefile +++ b/tests/hack/ci/Makefile @@ -85,13 +85,31 @@ gateway-tests: @echo "::endgroup::" .PHONY: k3d-gateway-tests -k3d-gateway-tests: create-k3d install-istio install-application-connector apply-appcon-crd gateway-tests +k3d-gateway-tests: create-k3d \ + install-istio \ + install-application-connector \ + apply-appcon-crd \ + gateway-tests + +.PHONY: patch-validator +patch-validator: + @echo "::group::patch-validator" + @make -f ${PROJECT_ROOT}/tests/Makefile.test-application-conn-validator patch-validator + @echo "::endgroup::" -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 +.PHONY: validator-tests +validator-tests: + @echo "::group::validator-tests" + @make -f ${PROJECT_ROOT}/tests/Makefile.test-application-conn-validator test + @echo "::endgroup::" + +.PHONY: k3d-validator-tests +k3d-validator-tests: patch-validator \ + create-k3d \ + install-istio \ + install-application-connector \ + apply-appcon-crd \ + validator-tests .PHONY: k3d-agent-tests k3d-agent-tests: create-k3d \ From 6287e851f91db56f694679e8e24e36c30cd8def8 Mon Sep 17 00:00:00 2001 From: m00g3n Date: Tue, 19 Mar 2024 13:47:30 +0100 Subject: [PATCH 05/11] update golang version to 1.22.1 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index a291a9eb..a45870e7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build the manager binary -FROM golang:1.21.1 as builder +FROM golang:1.22.1 as builder WORKDIR /workspace From efed6e32981a18ce569ebeec7d2ebb52f7e4b88b Mon Sep 17 00:00:00 2001 From: m00g3n Date: Wed, 20 Mar 2024 11:45:08 +0100 Subject: [PATCH 06/11] code cleanup --- pkg/reconciler/fn_state_matcher_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/pkg/reconciler/fn_state_matcher_test.go b/pkg/reconciler/fn_state_matcher_test.go index 81cf9d6a..e5a37cdf 100644 --- a/pkg/reconciler/fn_state_matcher_test.go +++ b/pkg/reconciler/fn_state_matcher_test.go @@ -50,8 +50,6 @@ func (m *stateFnNameMatcher) Match(actual any) (success bool, err error) { m.expName = m.expName[eliof+1:] } - fmt.Println("match:", m.actName, m.expName, m.Expected.name()) - return m.actName == m.expName, nil } From 9e1488567c890c320e15bdb5fba77e30d22d02b1 Mon Sep 17 00:00:00 2001 From: m00g3n Date: Tue, 26 Mar 2024 11:41:42 +0100 Subject: [PATCH 07/11] update cra k3d tests --- .../kyma-integration-k3d-validator-tests.yml | 2 +- .../Makefile.test-application-conn-validator | 4 +-- tests/Makefile.test-application-gateway | 4 +-- tests/Makefile.test-compass-runtime-agent | 12 ++++--- tests/hack/ci/Makefile | 8 ++++- .../compass-runtime-agent-test/values.yaml | 2 +- tests/scripts/check-pod-logs.sh | 36 ------------------- tests/scripts/fetch-test-logs.sh | 35 ++++++++++++++++++ .../testkit/init/compassconnection.go | 13 +------ .../testkit/init/init.go | 5 +-- 10 files changed, 57 insertions(+), 64 deletions(-) delete mode 100755 tests/scripts/check-pod-logs.sh create mode 100755 tests/scripts/fetch-test-logs.sh diff --git a/.github/workflows/kyma-integration-k3d-validator-tests.yml b/.github/workflows/kyma-integration-k3d-validator-tests.yml index ecc19ef2..5a86d9a1 100644 --- a/.github/workflows/kyma-integration-k3d-validator-tests.yml +++ b/.github/workflows/kyma-integration-k3d-validator-tests.yml @@ -35,5 +35,5 @@ jobs: uses: actions/upload-artifact@v4 with: name: app-gateway-test-results - path: application-connector-validator-test.log + path: application-connectivity-validator-test.log diff --git a/tests/Makefile.test-application-conn-validator b/tests/Makefile.test-application-conn-validator index 672a488a..d0d4ba6d 100644 --- a/tests/Makefile.test-application-conn-validator +++ b/tests/Makefile.test-application-conn-validator @@ -6,7 +6,6 @@ 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 -TEST_LOG = ${PWD}/application-connector-validator-test.log test: test-validator @@ -17,8 +16,7 @@ patch-validator: .PHONY: test-validator test-validator: patch-validator create-resources @echo "::group::test-validator" - kubectl wait --for=condition=complete --timeout=$(TEST_TIMEOUT) -n $(NAMESPACE) job/application-connectivity-validator-test; \ - kubectl -n $(NAMESPACE) -l batch.kubernetes.io/job-name=application-gateway-test logs 2>&1 > $(TEST_LOG) + ${PWD}/tests/scripts/fetch-test-logs.sh application-connectivity-validator-test ${PWD} @echo "::endgroup::" .PHONY: create-resources diff --git a/tests/Makefile.test-application-gateway b/tests/Makefile.test-application-gateway index 2b1fc877..3153f810 100644 --- a/tests/Makefile.test-application-gateway +++ b/tests/Makefile.test-application-gateway @@ -6,7 +6,6 @@ GOPATH ?= $(shell go env GOPATH) MOCK_SERVICE_NAME="mock-application" APP_URL = "$(MOCK_SERVICE_NAME).$(NAMESPACE).svc.cluster.local" TEST_TIMEOUT = "3m" -TEST_LOG = ${PWD}/application-gateway-test.log MAKEFILE_NAME=Makefile.test-application-gateway .PHONY: test @@ -18,8 +17,7 @@ clean: clean-gateway-test .PHONY: test-gateway test-gateway: disable-sidecar-for-mtls-test generate-certs create-resources @echo "::group::test-gateway" - kubectl wait --for=condition=complete --timeout=$(TEST_TIMEOUT) -n $(NAMESPACE) job/application-gateway-test; \ - kubectl -n $(NAMESPACE) -l batch.kubernetes.io/job-name=application-gateway-test logs 2>&1 > $(TEST_LOG) + ${PWD}/tests/scripts/fetch-test-logs.sh application-gateway-test ${PWD} @echo "::endgroup::" .PHONY: create-resources diff --git a/tests/Makefile.test-compass-runtime-agent b/tests/Makefile.test-compass-runtime-agent index f887c3a1..8389c559 100644 --- a/tests/Makefile.test-compass-runtime-agent +++ b/tests/Makefile.test-compass-runtime-agent @@ -5,11 +5,10 @@ GOPATH ?= $(shell go env GOPATH) DIRECTOR_URL=https://compass-gateway-auth-oauth.$(COMPASS_HOST)/director/graphql TOKENS_ENDPOINT=https://oauth2.${COMPASS_HOST}/oauth2/token -TEST_TIMEOUT = "3m" +TEST_TIMEOUT = "4m" CLUSTER_NAME ?= kyma REGISTRY_PORT ?= 5001 REGISTRY_NAME ?= ${CLUSTER_NAME}-registry -TEST_LOG = ${PWD}/compass-runtime-agent-test.log .PHONY: test test: test-compass-runtime-agent clean-compass-runtime-agent-test @@ -17,11 +16,14 @@ test: test-compass-runtime-agent clean-compass-runtime-agent-test .PHONY: clean clean: clean-compass-runtime-agent-test +.PHONY: patch-compass-runtime-agent +patch-compass-runtime-agent: + yq e -i '. |= (select(.kind == "Deployment" and .metadata.name == "compass-runtime-agent") | .spec.template.spec.containers[0].env[1] = { "name": "APP_CONTROLLER_SYNC_PERIOD", "value": "15s" })' ${PWD}/application-connector.yaml + .PHONY: test-compass-runtime-agent test-compass-runtime-agent: install-compass-runtime-agent-test - @echo "::group::test-compass-runtime-agent" - kubectl wait --for=condition=complete --timeout=$(TEST_TIMEOUT) -n $(NAMESPACE) job/compass-runtime-agent-test; \ - kubectl -n $(NAMESPACE) -l batch.kubernetes.io/job-name=compass-runtime-agent-test logs 2>&1 > $(TEST_LOG) + @echo "::group::fetch-compass-runtime-agent-test-logs" + ${PWD}/tests/scripts/fetch-test-logs.sh compass-runtime-agent-test ${PWD} @echo "::endgroup::" .PHONY: create-resources diff --git a/tests/hack/ci/Makefile b/tests/hack/ci/Makefile index 088374cd..51ee6a24 100644 --- a/tests/hack/ci/Makefile +++ b/tests/hack/ci/Makefile @@ -97,6 +97,12 @@ patch-validator: @make -f ${PROJECT_ROOT}/tests/Makefile.test-application-conn-validator patch-validator @echo "::endgroup::" +.PHONY: patch-compass-runtime-agent +patch-compass-runtime-agent: + @echo "::group::patch-compass-runtime-agent" + @make -f ${PROJECT_ROOT}/tests/Makefile.test-compass-runtime-agent patch-compass-runtime-agent + @echo "::endgroup::" + .PHONY: validator-tests validator-tests: @echo "::group::validator-tests" @@ -114,11 +120,11 @@ k3d-validator-tests: patch-validator \ .PHONY: k3d-agent-tests k3d-agent-tests: create-k3d \ install-istio \ + patch-compass-runtime-agent \ install-application-connector \ apply-compass-connection-crd \ apply-appcon-crd \ compass-runtime-agent-test-image \ - install-compass-runtime-agent \ agent-tests .PHONY: agent-tests diff --git a/tests/resources/charts/compass-runtime-agent-test/values.yaml b/tests/resources/charts/compass-runtime-agent-test/values.yaml index 46b02b1e..193f3e9e 100644 --- a/tests/resources/charts/compass-runtime-agent-test/values.yaml +++ b/tests/resources/charts/compass-runtime-agent-test/values.yaml @@ -1,5 +1,5 @@ namespace: "test" -testTenant: "3e64ebae-38b5-46a0-b1ed-9ccee153a0ae" +testTenant: "461f6292-8085-41c8-af0c-e185f39b5e18" oauthCredentialsSecretName: "oauth-compass-credentials" oauthCredentialsNamespace: "test" skipDirectorCertVerification: true diff --git a/tests/scripts/check-pod-logs.sh b/tests/scripts/check-pod-logs.sh deleted file mode 100755 index acba6d06..00000000 --- a/tests/scripts/check-pod-logs.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/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/fetch-test-logs.sh b/tests/scripts/fetch-test-logs.sh new file mode 100755 index 00000000..e6cffc5c --- /dev/null +++ b/tests/scripts/fetch-test-logs.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash + +# JOB_NAME - the name of the job that is runing tests (required) +# LOGS_OUT - path to directory where the logs will be stored (optional, default: ${PWD}) +# NAMESPACE the - namespace where the test job is runing (optional,default: test) +# TEST_TIMEOUT - duration to wait on tests to finish (optional, default: 900s) + +function fetch_tests() { + local JOB_NAME=$1 + # check if JOB_NAME is provided + if [ -z "$JOB_NAME" ]; then + echo "Usage: $0 [LOGS_OUT] [NAMESPACE] [TEST_TIMEOUT]" + exit 1 + fi + local LOGS_OUT=${2:-${PWD}} + local NAMESPACE=${3:-test} + local TEST_TIMEOUT=${4:-900s} + # wait for the job to finish + kubectl wait job/$JOB_NAME \ + -n $NAMESPACE \ + --for=condition=complete \ + --timeout=$TEST_TIMEOUT + # store the exit code of the job + local __job_result__=$? + # try to get the logs of the job and store them in the TEST_LOG file + kubectl logs \ + -n $NAMESPACE \ + -f job/$JOB_NAME \ + 2>&1 > "$LOGS_OUT"/$JOB_NAME.log + # exit with original job exit code + exit $__job_result__ +} + +fetch_tests $@ + diff --git a/tests/test/compass-runtime-agent/testkit/init/compassconnection.go b/tests/test/compass-runtime-agent/testkit/init/compassconnection.go index 41d2885e..a5ae506a 100644 --- a/tests/test/compass-runtime-agent/testkit/init/compassconnection.go +++ b/tests/test/compass-runtime-agent/testkit/init/compassconnection.go @@ -35,18 +35,7 @@ func NewCompassConnectionCRConfiguration(compassConnectionInterface CompassConne } 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 + return newRollbackFunc(), nil } func (cc compassConnectionCRConfiguration) backup() (types.RollbackFunc, error) { compassConnectionCR, err := cc.compassConnectionInterface.Get(context.TODO(), ConnectionCRName, meta.GetOptions{}) diff --git a/tests/test/compass-runtime-agent/testkit/init/init.go b/tests/test/compass-runtime-agent/testkit/init/init.go index 68b397c8..94849b3c 100644 --- a/tests/test/compass-runtime-agent/testkit/init/init.go +++ b/tests/test/compass-runtime-agent/testkit/init/init.go @@ -2,6 +2,7 @@ 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" @@ -11,7 +12,7 @@ const ( CompassSystemNamespace = "kyma-system" IstioSystemNamespace = "istio-system" CompassRuntimeAgentDeployment = "compass-runtime-agent" - NewCompassRuntimeConfigName = "test-compass-runtime-agent-config" + NewCompassRuntimeConfigName = "compass-agent-configuration" NewCACertSecretName = "ca-cert-test" NewClientCertSecretName = "client-cert-test" NewControllerSyncPeriodTime = "15s" @@ -80,7 +81,7 @@ func (crc compassRuntimeAgentConfigurator) Do(runtimeName, formationName string) 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) + newControllerSyncPeriodTime := NewControllerSyncPeriodTime log.Info("Preparing Compass Runtime Agent configuration secret") deploymentRollbackFunc, err := crc.deploymentConfigurator.Do(newCACertNamespacedSecretName, From 69d5f4f364dba8a994383e2446f875c482635f6a Mon Sep 17 00:00:00 2001 From: m00g3n Date: Wed, 27 Mar 2024 14:00:55 +0100 Subject: [PATCH 08/11] update k3d in compass-runtime-agent workflow --- .github/workflows/kyma-integration-k3d-agent-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/kyma-integration-k3d-agent-tests.yml b/.github/workflows/kyma-integration-k3d-agent-tests.yml index fe9d333a..a2aef9d6 100644 --- a/.github/workflows/kyma-integration-k3d-agent-tests.yml +++ b/.github/workflows/kyma-integration-k3d-agent-tests.yml @@ -14,7 +14,7 @@ jobs: - name: Install k3d env: K3D_URL: https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh - DEFAULT_K3D_VERSION: v5.4.6 + DEFAULT_K3D_VERSION: v5.6.0 run: curl --silent --fail $K3D_URL | TAG=$DEFAULT_K3D_VERSION bash - name: Set up cache uses: actions/cache@v3 From 300cd55a49ea9d4011b3b77e59e571526f171d33 Mon Sep 17 00:00:00 2001 From: m00g3n Date: Wed, 27 Mar 2024 14:30:50 +0100 Subject: [PATCH 09/11] test dns issue on GHA --- .../workflows/kyma-integration-k3d-agent-tests.yml | 14 ++++++++++++-- tests/Makefile.test-compass-runtime-agent | 2 +- tests/scripts/fetch-test-logs.sh | 5 +++-- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/.github/workflows/kyma-integration-k3d-agent-tests.yml b/.github/workflows/kyma-integration-k3d-agent-tests.yml index a2aef9d6..a3c8c904 100644 --- a/.github/workflows/kyma-integration-k3d-agent-tests.yml +++ b/.github/workflows/kyma-integration-k3d-agent-tests.yml @@ -1,8 +1,18 @@ name: Run compass-rt-agemt integration tests on k3d +env: + COMPASS_CLIENT_ID: "${{ secrets.COMPASS_CLIENT_ID }}" + COMPASS_CLIENT_SECRET: "${{ secrets.COMPASS_CLIENT_SECRET }}" + COMPASS_HOST: "${{ secrets.COMPASS_HOST }}" on: push: branches: [ main ] - pull_request: + pull_request_target: + branches: [ main ] + types: + - opened + - reopened + - synchronize + - ready_for_review jobs: test: runs-on: ubuntu-latest @@ -27,7 +37,7 @@ jobs: restore-keys: | ${{ runner.os }}-go- - name: Run unit tests - run: make -C tests/hack/ci k3d-agent-tests + run: make -C tests/hack/ci k3d-agent-tests - name: Archive test results if: always() uses: actions/upload-artifact@v4 diff --git a/tests/Makefile.test-compass-runtime-agent b/tests/Makefile.test-compass-runtime-agent index 8389c559..4f416273 100644 --- a/tests/Makefile.test-compass-runtime-agent +++ b/tests/Makefile.test-compass-runtime-agent @@ -3,7 +3,7 @@ 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 +TOKENS_ENDPOINT=https://oauth2.$(COMPASS_HOST)/oauth2/token TEST_TIMEOUT = "4m" CLUSTER_NAME ?= kyma diff --git a/tests/scripts/fetch-test-logs.sh b/tests/scripts/fetch-test-logs.sh index e6cffc5c..96fde6f3 100755 --- a/tests/scripts/fetch-test-logs.sh +++ b/tests/scripts/fetch-test-logs.sh @@ -14,7 +14,7 @@ function fetch_tests() { fi local LOGS_OUT=${2:-${PWD}} local NAMESPACE=${3:-test} - local TEST_TIMEOUT=${4:-900s} + local TEST_TIMEOUT=${4:-300s} # wait for the job to finish kubectl wait job/$JOB_NAME \ -n $NAMESPACE \ @@ -31,5 +31,6 @@ function fetch_tests() { exit $__job_result__ } +echo "host:"$COMPASS_HOST + fetch_tests $@ - From 75c3c1045e098144ae2aa6c32e78c4b97341b263 Mon Sep 17 00:00:00 2001 From: m00g3n Date: Thu, 28 Mar 2024 11:27:27 +0100 Subject: [PATCH 10/11] refactor cra workflow --- .../kyma-integration-k3d-agent-tests.yml | 45 ++++------------- .github/workflows/reusable-k3d-agent-test.yml | 50 +++++++++++++++++++ tests/scripts/fetch-test-logs.sh | 2 - 3 files changed, 59 insertions(+), 38 deletions(-) create mode 100644 .github/workflows/reusable-k3d-agent-test.yml diff --git a/.github/workflows/kyma-integration-k3d-agent-tests.yml b/.github/workflows/kyma-integration-k3d-agent-tests.yml index a3c8c904..5b10b4bd 100644 --- a/.github/workflows/kyma-integration-k3d-agent-tests.yml +++ b/.github/workflows/kyma-integration-k3d-agent-tests.yml @@ -1,8 +1,4 @@ -name: Run compass-rt-agemt integration tests on k3d -env: - COMPASS_CLIENT_ID: "${{ secrets.COMPASS_CLIENT_ID }}" - COMPASS_CLIENT_SECRET: "${{ secrets.COMPASS_CLIENT_SECRET }}" - COMPASS_HOST: "${{ secrets.COMPASS_HOST }}" +name: run-cra-k3d on: push: branches: [ main ] @@ -14,34 +10,11 @@ on: - synchronize - ready_for_review 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.6.0 - run: curl --silent --fail $K3D_URL | TAG=$DEFAULT_K3D_VERSION bash - - name: Set up cache - uses: actions/cache@v3 - with: - path: | - ~/.cache/go-build - ~/go/pkg/mod - /home/runner/work/application-connector-manager/application-connector-manager/bin - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go- - - name: Run unit tests - run: make -C tests/hack/ci k3d-agent-tests - - name: Archive test results - if: always() - uses: actions/upload-artifact@v4 - with: - name: app-gateway-test-results - path: compass-runtime-agent-test.log - + run-cra-k3d: + uses: "./.github/workflows/reusable-k3d-agent-test.yml" + with: + k3d-version: v5.6.0 + secrets: + compass-host: ${{ secrets.COMPASS_HOST }} + compass-client-id: ${{ secrets.COMPASS_CLIENT_ID }} + compass-client-secret: ${{ secrets.COMPASS_CLIENT_SECRET }} diff --git a/.github/workflows/reusable-k3d-agent-test.yml b/.github/workflows/reusable-k3d-agent-test.yml new file mode 100644 index 00000000..8fc6902c --- /dev/null +++ b/.github/workflows/reusable-k3d-agent-test.yml @@ -0,0 +1,50 @@ +name: k3d CRA test +on: + workflow_call: + inputs: + k3d-version: + required: true + type: string + default: v5.6.0 + secrets: + compass-host: + required: true + compass-client-id: + required: true + compass-client-secret: + required: true +jobs: + test-cra: + runs-on: ubuntu-latest + steps: + - uses: azure/setup-helm@v4.1.0 + id: install-helm + - name: checkout + uses: actions/checkout@v3 + - name: install-k3d + env: + K3D_URL: https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh + DEFAULT_K3D_VERSION: ${{ inputs.k3d-version }} + run: curl --silent --fail $K3D_URL | TAG=$DEFAULT_K3D_VERSION bash + - name: setup-cache + uses: actions/cache@v3 + with: + path: | + ~/.cache/go-build + ~/go/pkg/mod + /home/runner/work/application-connector-manager/application-connector-manager/bin + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + - name: run-tests + env: + COMPASS_CLIENT_ID: ${{ secrets.compass-client-id }} + COMPASS_CLIENT_SECRET: ${{ secrets.compass-client-secret }} + COMPASS_HOST: ${{ secrets.compass-host }} + run: make -C tests/hack/ci k3d-agent-tests + - name: archive-logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: app-gateway-test-results + path: compass-runtime-agent-test.log diff --git a/tests/scripts/fetch-test-logs.sh b/tests/scripts/fetch-test-logs.sh index 96fde6f3..3420b3da 100755 --- a/tests/scripts/fetch-test-logs.sh +++ b/tests/scripts/fetch-test-logs.sh @@ -31,6 +31,4 @@ function fetch_tests() { exit $__job_result__ } -echo "host:"$COMPASS_HOST - fetch_tests $@ From 7bd7a6e0ba6bd10278e86d82c2cc398b5c776132 Mon Sep 17 00:00:00 2001 From: m00g3n Date: Wed, 3 Apr 2024 14:23:29 +0200 Subject: [PATCH 11/11] fix archived artifacts names --- .github/workflows/kyma-integration-k3d-validator-tests.yml | 2 +- .github/workflows/reusable-k3d-agent-test.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/kyma-integration-k3d-validator-tests.yml b/.github/workflows/kyma-integration-k3d-validator-tests.yml index 5a86d9a1..f0229e1c 100644 --- a/.github/workflows/kyma-integration-k3d-validator-tests.yml +++ b/.github/workflows/kyma-integration-k3d-validator-tests.yml @@ -34,6 +34,6 @@ jobs: if: always() uses: actions/upload-artifact@v4 with: - name: app-gateway-test-results + name: application-connectivity-validator-test-results path: application-connectivity-validator-test.log diff --git a/.github/workflows/reusable-k3d-agent-test.yml b/.github/workflows/reusable-k3d-agent-test.yml index 8fc6902c..5c75f44c 100644 --- a/.github/workflows/reusable-k3d-agent-test.yml +++ b/.github/workflows/reusable-k3d-agent-test.yml @@ -46,5 +46,5 @@ jobs: if: always() uses: actions/upload-artifact@v4 with: - name: app-gateway-test-results + name: compass-runtime-agent-test-results path: compass-runtime-agent-test.log