Browse Source

e2e testing for gcp Workload Identity

Lucas Severo Alves 4 years ago
parent
commit
25763fde81

+ 290 - 0
.github/workflows/e2e-managed.yml

@@ -0,0 +1,290 @@
+# Run secret-dependent e2e tests only after /ok-to-test approval
+on:
+  pull_request:
+  repository_dispatch:
+    types: [ok-to-test-command]
+
+env:
+  # Common versions
+  GO_VERSION: '1.17'
+  GOLANGCI_VERSION: 'v1.33'
+  DOCKER_BUILDX_VERSION: 'v0.4.2'
+
+  # Common users. We can't run a step 'if secrets.GHCR_USERNAME != ""' but we can run
+  # a step 'if env.GHCR_USERNAME' != ""', so we copy these to succinctly test whether
+  # credentials have been provided before trying to run steps that need them.
+  GHCR_USERNAME: ${{ secrets.GHCR_USERNAME }}
+  GCP_SM_SA_JSON: ${{ secrets.GCP_SM_SA_JSON}}
+  GCP_PROJECT_ID: ${{ secrets.GCP_PROJECT_ID}}
+  TF_VAR_GCP_PROJECT_ID: ${{ secrets.GCP_PROJECT_ID}}
+  GCP_SM_SA_GKE_JSON: ${{ secrets.GCP_SM_SA_GKE_JSON}}
+  GCP_GKE_CLUSTER: test-cluster
+  GCP_GKE_ZONE: ${{ secrets.GCP_GKE_ZONE}}
+  GCP_GSA_NAME: ${{ secrets.GCP_GSA_NAME}} # Goolge Service Account
+  GCP_KSA_NAME: ${{ secrets.GCP_KSA_NAME}} # Kubernetes Service Account
+  TF_VAR_GCP_GSA_NAME: ${{ secrets.GCP_GSA_NAME}} # Goolge Service Account for tf
+  TF_VAR_GCP_KSA_NAME: ${{ secrets.GCP_KSA_NAME}} # Kubernetes Service Account for tf
+  AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID}}
+  AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET}}
+  TENANT_ID: ${{ secrets.TENANT_ID}}
+  VAULT_URL: ${{ secrets.VAULT_URL}}
+  IMAGE_REGISTRY: ghcr.io/external-secrets/external-secrets
+  E2E_IMAGE_REGISTRY: ghcr.io/external-secrets/external-secrets-e2e
+  E2E_VERSION: test
+
+name: e2e tests
+
+jobs:
+  # Branch-based pull request
+  integration-trusted-managed:
+    runs-on: ubuntu-latest
+    if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
+    steps:
+
+    - name: Branch based PR checkout
+      uses: actions/checkout@v2
+
+    # <insert integration tests needing secrets>
+
+    - name: Fetch History
+      run: git fetch --prune --unshallow
+
+    - name: Setup Go
+      uses: actions/setup-go@v2
+      with:
+        go-version: ${{ env.GO_VERSION }}
+
+    - name: Find the Go Cache
+      id: go
+      run: |
+        echo "::set-output name=build-cache::$(go env GOCACHE)"
+        echo "::set-output name=mod-cache::$(go env GOMODCACHE)"
+
+    - name: Cache the Go Build Cache
+      uses: actions/cache@v2.1.7
+      with:
+        path: ${{ steps.go.outputs.build-cache }}
+        key: ${{ runner.os }}-build-unit-tests-${{ hashFiles('**/go.sum') }}
+        restore-keys: ${{ runner.os }}-build-unit-tests-
+
+    - name: Cache Go Dependencies
+      uses: actions/cache@v2.1.7
+      with:
+        path: ${{ steps.go.outputs.mod-cache }}
+        key: ${{ runner.os }}-pkg-${{ hashFiles('**/go.sum') }}
+        restore-keys: ${{ runner.os }}-pkg-
+    
+    - name: Setup gcloud CLI
+      uses: google-github-actions/setup-gcloud@master
+      with:
+        service_account_key: ${{ env.GCP_SM_SA_GKE_JSON }}
+        project_id: ${{ env.GCP_PROJECT_ID }}
+    
+    - name: Setup TFLint
+      uses: terraform-linters/setup-tflint@v1
+      with:
+        tflint_version: v0.28.0  # Must be specified. See: https://github.com/terraform-linters/tflint/releases for latest versions
+
+    - name: Run TFLint
+      run: find ${{ github.workspace }} | grep tf$ | xargs -n1 dirname | xargs -IXXX -n1 /bin/sh -c 'set -o errexit; cd XXX; pwd; tflint --loglevel=info .; cd - >/dev/null'
+
+    - name: Setup TF Gcloud Provider
+      run: |-
+        mkdir -p terraform/gcp/secrets
+        echo ${GCP_SM_SA_GKE_JSON} > terraform/gcp/secrets/gcloud-service-account-key.json
+
+    - name: Show TF GKE
+      run: |-
+        make tf.show.gcp
+
+    - name: Setup Infracost
+      uses: infracost/actions/setup@v1
+      with:
+        api-key: ${{ secrets.INFRACOST_API_KEY }}
+
+    - name: Generate Infracost JSON
+      run: infracost breakdown --path terraform/gcp/plan.json --format json --out-file /tmp/infracost.json
+
+    - name: Post Infracost comment
+      uses: infracost/actions/comment@v1
+      with:
+        path: /tmp/infracost.json
+        # Choose the commenting behavior, 'update' is a good default:
+        behavior: update # Create a single comment and update it. The "quietest" option.                 
+        # behavior: delete-and-new # Delete previous comments and create a new one.
+        # behavior: hide-and-new # Minimize previous comments and create a new one.
+        # behavior: new # Create a new cost estimate comment on every push.
+
+    - name: Apply TF GKE
+      run: |-
+        make tf.apply.gcp
+
+    - name: Get the GKE credentials
+      run: |-
+        gcloud container clusters get-credentials "$GCP_GKE_CLUSTER" --zone "$GCP_GKE_ZONE" --project "$GCP_PROJECT_ID"
+
+    - name: Login to Docker
+      uses: docker/login-action@v1
+      if: env.GHCR_USERNAME != ''
+      with:
+        registry: ghcr.io
+        username: ${{ secrets.GHCR_USERNAME }}
+        password: ${{ secrets.GHCR_TOKEN }}
+
+    - name: Run e2e Tests for GCP
+      run: |
+        export E2E_VERSION=$GITHUB_SHA
+        export PR_IMG_TAG=$GITHUB_SHA
+        export PATH=$PATH:$(go env GOPATH)/bin
+        go get github.com/onsi/ginkgo/ginkgo
+        make test.e2e.managed FOCUS="gcpmanaged"
+
+    - name: Destroy TF GKE
+      if: always()
+      run: |-
+        make tf.destroy.gcp
+
+
+  # Repo owner has commented /ok-to-test on a (fork-based) pull request
+  integration-fork-managed:
+    runs-on: ubuntu-latest
+    if:
+      github.event_name == 'repository_dispatch'
+    steps:
+
+    - name: Branch based PR checkout
+      uses: actions/checkout@v2
+
+    # <insert integration tests needing secrets>
+
+    - name: Fetch History
+      run: git fetch --prune --unshallow
+
+    - name: Setup Go
+      uses: actions/setup-go@v2
+      with:
+        go-version: ${{ env.GO_VERSION }}
+
+    - name: Find the Go Cache
+      id: go
+      run: |
+        echo "::set-output name=build-cache::$(go env GOCACHE)"
+        echo "::set-output name=mod-cache::$(go env GOMODCACHE)"
+
+    - name: Cache the Go Build Cache
+      uses: actions/cache@v2.1.7
+      with:
+        path: ${{ steps.go.outputs.build-cache }}
+        key: ${{ runner.os }}-build-unit-tests-${{ hashFiles('**/go.sum') }}
+        restore-keys: ${{ runner.os }}-build-unit-tests-
+
+    - name: Cache Go Dependencies
+      uses: actions/cache@v2.1.7
+      with:
+        path: ${{ steps.go.outputs.mod-cache }}
+        key: ${{ runner.os }}-pkg-${{ hashFiles('**/go.sum') }}
+        restore-keys: ${{ runner.os }}-pkg-
+    
+    - name: Setup gcloud CLI
+      uses: google-github-actions/setup-gcloud@master
+      with:
+        service_account_key: ${{ env.GCP_SM_SA_GKE_JSON }}
+        project_id: ${{ env.GCP_PROJECT_ID }}
+    
+    - name: Setup TFLint
+      uses: terraform-linters/setup-tflint@v1
+      with:
+        tflint_version: v0.28.0  # Must be specified. See: https://github.com/terraform-linters/tflint/releases for latest versions
+
+    - name: Run TFLint
+      run: find ${{ github.workspace }} | grep tf$ | xargs -n1 dirname | xargs -IXXX -n1 /bin/sh -c 'set -o errexit; cd XXX; pwd; tflint --loglevel=info .; cd - >/dev/null'
+
+    - name: Setup TF Gcloud Provider
+      run: |-
+        mkdir -p terraform/gcp/secrets
+        echo ${GCP_SM_SA_GKE_JSON} > terraform/gcp/secrets/gcloud-service-account-key.json
+
+    - name: Show TF GKE
+      run: |-
+        make tf.show.gcp
+
+    - name: Setup Infracost
+      uses: infracost/actions/setup@v1
+      with:
+        api-key: ${{ secrets.INFRACOST_API_KEY }}
+
+    - name: Generate Infracost JSON for GKE
+      run: infracost breakdown --path terraform/gcp/plan.json --format json --out-file /tmp/infracost.json
+
+    - name: Post Infracost comment
+      uses: infracost/actions/comment@v1
+      with:
+        path: /tmp/infracost.json
+        # Choose the commenting behavior, 'update' is a good default:
+        behavior: update # Create a single comment and update it. The "quietest" option.                 
+        # behavior: delete-and-new # Delete previous comments and create a new one.
+        # behavior: hide-and-new # Minimize previous comments and create a new one.
+        # behavior: new # Create a new cost estimate comment on every push.
+
+    - name: Apply TF GKE
+      run: |-
+        make tf.apply.gcp
+
+    - name: Get the GKE credentials
+      run: |-
+        gcloud container clusters get-credentials "$GCP_GKE_CLUSTER" --zone "$GCP_GKE_ZONE" --project "$GCP_PROJECT_ID"
+
+    - name: Login to Docker
+      uses: docker/login-action@v1
+      if: env.GHCR_USERNAME != ''
+      with:
+        registry: ghcr.io
+        username: ${{ secrets.GHCR_USERNAME }}
+        password: ${{ secrets.GHCR_TOKEN }}
+
+    - name: Run e2e Tests for GCP
+      run: |
+        export E2E_VERSION=$GITHUB_SHA
+        export PR_IMG_TAG=$GITHUB_SHA
+        export PATH=$PATH:$(go env GOPATH)/bin
+        go get github.com/onsi/ginkgo/ginkgo
+        make test.e2e.managed FOCUS="gcpmanaged"
+
+    - name: Destroy TF GKE
+      if: always()
+      run: |-
+        make tf.destroy.gcp
+
+    # Update check run called "integration-fork"
+    - uses: actions/github-script@v1
+      id: update-check-run
+      if: ${{ always() }}
+      env:
+        number: ${{ github.event.client_payload.pull_request.number }}
+        job: ${{ github.job }}
+        # Conveniently, job.status maps to https://developer.github.com/v3/checks/runs/#update-a-check-run
+        conclusion: ${{ job.status }}
+      with:
+        github-token: ${{ secrets.GITHUB_TOKEN }}
+        script: |
+          const { data: pull } = await github.pulls.get({
+            ...context.repo,
+            pull_number: process.env.number
+          });
+          const ref = pull.head.sha;
+          console.log("\n\nPR sha: " + ref)
+          const { data: checks } = await github.checks.listForRef({
+            ...context.repo,
+            ref
+          });
+          console.log("\n\nPR CHECKS: " + checks)
+          const check = checks.check_runs.filter(c => c.name === process.env.job);
+          console.log("\n\nPR Filtered CHECK: " + check)
+          console.log(check)
+          const { data: result } = await github.checks.update({
+            ...context.repo,
+            check_run_id: check[0].id,
+            status: 'completed',
+            conclusion: process.env.conclusion
+          });
+          return result;

+ 5 - 0
.github/workflows/e2e.yml

@@ -15,11 +15,16 @@ env:
   # credentials have been provided before trying to run steps that need them.
   GHCR_USERNAME: ${{ secrets.GHCR_USERNAME }}
   GCP_SM_SA_JSON: ${{ secrets.GCP_SM_SA_JSON}}
+  GCP_GKE_ZONE: ${{ secrets.GCP_GKE_ZONE}}
+  GCP_GSA_NAME: ${{ secrets.GCP_GSA_NAME}} # Goolge Service Account
+  GCP_KSA_NAME: ${{ secrets.GCP_KSA_NAME}} # Kubernetes Service Account
   GCP_PROJECT_ID: ${{ secrets.GCP_PROJECT_ID}}
   AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID}}
   AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET}}
   TENANT_ID: ${{ secrets.TENANT_ID}}
   VAULT_URL: ${{ secrets.VAULT_URL}}
+  E2E_IMAGE_REGISTRY: local/external-secrets-e2e
+  E2E_VERSION: test
 
 name: e2e tests
 

+ 39 - 0
.gitignore

@@ -20,3 +20,42 @@ deploy/charts/external-secrets/templates/crds/*.yaml
 site/
 e2e/k8s/deploy
 e2e/e2e.test
+
+# tf ignores
+# Local .terraform directories
+**/.terraform/*
+
+# .tfstate files
+*.tfstate
+*.tfstate.*
+
+# Crash log files
+crash.log
+crash.*.log
+
+# Exclude all .tfvars files, which are likely to contain sentitive data, such as
+# password, private keys, and other secrets. These should not be part of version
+# control as they are data points which are potentially sensitive and subject
+# to change depending on the environment.
+#
+*.tfvars
+
+# Ignore override files as they are usually used to override resources locally and so
+# are not checked in
+override.tf
+override.tf.json
+*_override.tf
+*_override.tf.json
+
+# Include override files you do wish to add to version control using negated pattern
+#
+# !example_override.tf
+
+# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan
+# example: *tfplan*
+
+# Ignore CLI configuration files
+.terraformrc
+terraform.rc
+**/secrets/**
+.terraform.lock.hcl

+ 35 - 0
Makefile

@@ -16,11 +16,14 @@ all: $(addprefix build-,$(ARCH))
 # Image registry for build/push image targets
 IMAGE_REGISTRY ?= ghcr.io/external-secrets/external-secrets
 
+PR_IMG_TAG ?=
+
 # Produce CRDs that work back to Kubernetes 1.11 (no version conversion)
 CRD_OPTIONS ?= "crd:trivialVersions=true"
 CRD_DIR     ?= deploy/crds
 
 HELM_DIR    ?= deploy/charts/external-secrets
+TF_DIR ?= terraform
 
 OUTPUT_DIR  ?= bin
 
@@ -88,6 +91,12 @@ test.e2e: generate ## Run e2e tests
 	$(MAKE) -C ./e2e test
 	@$(OK) go test unit-tests
 
+.PHONY: test.e2e.managed
+test.e2e.managed: generate ## Run e2e tests
+	@$(INFO) go test e2e-tests
+	$(MAKE) -C ./e2e test.managed
+	@$(OK) go test unit-tests
+
 .PHONY: build
 build: $(addprefix build-,$(ARCH)) ## Build binary 
 
@@ -221,6 +230,32 @@ docker.promote: ## Promote the docker image to the registry
 	@$(OK) docker push $(RELEASE_TAG) \
 
 # ====================================================================================
+# Terraform
+
+tf.plan.gcp: ## Runs terrform plan for gcp provider bringing GKE up
+	@cd $(TF_DIR)/gcp; \
+	terraform init; \
+	terraform plan -auto-approve
+
+tf.apply.gcp: ## Runs terrform apply for gcp provider bringing GKE up
+	@cd $(TF_DIR)/gcp; \
+	terraform init; \
+	terraform apply -auto-approve
+
+tf.destroy.gcp: ## Runs terrform destroy for gcp provider bringing GKE down
+	@cd $(TF_DIR)/gcp; \
+	terraform init; \
+	terraform destroy -auto-approve
+
+tf.show.gcp: ## Runs terrform show for gcp and outputs to a file
+	@cd $(TF_DIR)/gcp; \
+	terraform init; \
+	terraform plan -out tfplan.binary; \
+	terraform show -json tfplan.binary > plan.json
+
+
+
+# ====================================================================================
 # Help
 
 # only comments after make target name are shown as help text

+ 2 - 0
docs/provider-google-secrets-manager.md

@@ -58,6 +58,8 @@ spec:
             namespace: team-a
 ```
 
+*You need to give the Google service account the `roles/iam.serviceAccountTokenCreator` role so it can generate a service account token for you (not necessary in the Pod-based Workload Identity bellow)*
+
 #### Using Pod-based Workload Identity
 
 You can attach a Workload Identity directly to the ESO pod. ESO then has access to all the APIs defined in the attached service account policy. You attach the workload identity by (1) creating a service account with a attached workload identity (described above) and (2) using this particular service account in the pod's `serviceAccountName` field.

+ 30 - 0
e2e/Makefile

@@ -6,8 +6,12 @@ IMG_TAG     = test
 IMG         = local/external-secrets-e2e:$(IMG_TAG)
 KIND_IMG    = "kindest/node:v1.21.1@sha256:69860bda5563ac81e3c0057d654b5253219618a22ec3a346306239bba8cfa1a6"
 BUILD_ARGS  ?=
+IMAGE_REGISTRY ?=
 export FOCUS := $(FOCUS)
 
+export E2E_IMAGE_REGISTRY ?=
+export E2E_VERSION ?=
+
 start-kind: ## Start kind cluster
 	kind create cluster \
 	  --name external-secrets \
@@ -25,6 +29,25 @@ test: e2e-image ## Run e2e tests against current kube context
 	kind load docker-image --name="external-secrets" $(IMG)
 	./run.sh
 
+test.managed: e2e-remote-values e2e-image.managed  ## Run e2e tests against current kube context
+	$(MAKE) -C ../ docker.build \
+		VERSION=$(PR_IMG_TAG) \
+		ARCH=amd64 \
+		BUILD_ARGS="${BUILD_ARGS} --build-arg TARGETARCH=amd64 --build-arg TARGETOS=linux"
+	$(MAKE) -C ../ docker.push \
+		VERSION=$(PR_IMG_TAG)
+	$(MAKE) -C ../ docker.push \
+		IMAGE_REGISTRY=$(E2E_IMAGE_REGISTRY) \
+		VERSION=$(E2E_VERSION)
+	./run.sh
+
+e2e-remote-values:
+	sed -i "s|repository: [^ ]*|repository: $(IMAGE_REGISTRY)|g" k8s/eso.values.yaml
+	sed -i "s|tag: [^ ]*|tag: $(PR_IMG_TAG)|g" k8s/eso.values.yaml
+	sed -i "s|repository: [^ ]*|repository: $(IMAGE_REGISTRY)|g" k8s/eso.scoped.values.yaml
+	sed -i "s|tag: [^ ]*|tag: $(PR_IMG_TAG)|g" k8s/eso.scoped.values.yaml
+
+
 e2e-bin:
 	CGO_ENABLED=0 go run github.com/onsi/ginkgo/ginkgo build .
 
@@ -35,6 +58,13 @@ e2e-image: e2e-bin
 	cp -r ../deploy ./k8s
 	docker build $(BUILD_ARGS) -t $(IMG) .
 
+e2e-image.managed: e2e-bin
+	-rm -rf ./k8s/deploy
+	mkdir -p k8s
+	$(MAKE) -C ../ helm.generate
+	cp -r ../deploy ./k8s
+	docker build $(BUILD_ARGS) -t ghcr.io/external-secrets/external-secrets-e2e:$(E2E_VERSION) .
+
 stop-kind: ## Stop kind cluster
 	kind delete cluster \
 		--name external-secrets \

+ 1 - 2
e2e/e2e_test.go

@@ -21,7 +21,6 @@ import (
 	// nolint
 	. "github.com/onsi/gomega"
 
-	"github.com/external-secrets/external-secrets/e2e/framework"
 	"github.com/external-secrets/external-secrets/e2e/framework/addon"
 	"github.com/external-secrets/external-secrets/e2e/framework/util"
 	_ "github.com/external-secrets/external-secrets/e2e/suite"
@@ -29,7 +28,7 @@ import (
 
 var _ = SynchronizedBeforeSuite(func() []byte {
 	cfg := &addon.Config{}
-	cfg.KubeConfig, cfg.KubeClientSet, cfg.CRClient = framework.NewConfig()
+	cfg.KubeConfig, cfg.KubeClientSet, cfg.CRClient = util.NewConfig()
 
 	By("installing localstack")
 	addon.InstallGlobalAddon(addon.NewLocalstack(), cfg)

+ 46 - 0
e2e/framework/addon/eso.go

@@ -13,6 +13,19 @@ limitations under the License.
 */
 package addon
 
+import (
+	"fmt"
+	"os"
+
+	// nolint
+	. "github.com/onsi/ginkgo"
+	// nolint
+	. "github.com/onsi/gomega"
+
+	// nolint
+	"github.com/external-secrets/external-secrets/e2e/framework/util"
+)
+
 type ESO struct {
 	Addon
 }
@@ -28,6 +41,39 @@ func NewESO() *ESO {
 	}
 }
 
+func (l *ESO) Install() error {
+	By("Installing eso\n")
+	err := l.Addon.Install()
+	if err != nil {
+		return err
+	}
+
+	By("afterInstall eso\n")
+	err = l.afterInstall()
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (l *ESO) afterInstall() error {
+	gcpProjectID := os.Getenv("GCP_PROJECT_ID")
+	gcpGSAName := os.Getenv("GCP_GSA_NAME")
+	gcpKSAName := os.Getenv("GCP_KSA_NAME")
+	_, kubeClientSet, _ := util.NewConfig()
+
+	annotations := make(map[string]string)
+	annotations["iam.gke.io/gcp-service-account"] = fmt.Sprintf("%s@%s.iam.gserviceaccount.com", gcpGSAName, gcpProjectID)
+	_, err := util.UpdateKubeSA(gcpKSAName, kubeClientSet, "default", annotations)
+	Expect(err).NotTo(HaveOccurred())
+
+	_, err = util.UpdateKubeSA("external-secrets-e2e", kubeClientSet, "default", annotations)
+	Expect(err).NotTo(HaveOccurred())
+
+	return nil
+}
+
 func NewScopedESO() *ESO {
 	return &ESO{
 		&HelmChart{

+ 2 - 0
e2e/framework/eso.go

@@ -26,6 +26,7 @@ import (
 	"k8s.io/apimachinery/pkg/util/wait"
 
 	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+	"github.com/external-secrets/external-secrets/e2e/framework/log"
 )
 
 // WaitForSecretValue waits until a secret comes into existence and compares the secret.Data
@@ -38,6 +39,7 @@ func (f *Framework) WaitForSecretValue(namespace, name string, expected *v1.Secr
 			Name:      name,
 		}, secret)
 		if apierrors.IsNotFound(err) {
+			log.Logf("Secret Not Found. Expected: %+v, Got: %+v", expected, secret)
 			return false, nil
 		}
 		return equalSecrets(expected, secret), nil

+ 17 - 30
e2e/framework/framework.go

@@ -14,19 +14,18 @@ limitations under the License.
 package framework
 
 import (
-	"os"
 
 	// nolint
 	. "github.com/onsi/ginkgo"
 
 	// nolint
 	. "github.com/onsi/gomega"
+	// nolint
+	. "github.com/onsi/ginkgo/extensions/table"
 	api "k8s.io/api/core/v1"
-	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/client-go/kubernetes"
 	kscheme "k8s.io/client-go/kubernetes/scheme"
 	"k8s.io/client-go/rest"
-	"k8s.io/client-go/tools/clientcmd"
 	crclient "sigs.k8s.io/controller-runtime/pkg/client"
 
 	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
@@ -34,11 +33,9 @@ import (
 	"github.com/external-secrets/external-secrets/e2e/framework/util"
 )
 
-var Scheme = runtime.NewScheme()
-
 func init() {
-	_ = kscheme.AddToScheme(Scheme)
-	_ = esv1alpha1.AddToScheme(Scheme)
+	_ = kscheme.AddToScheme(util.Scheme)
+	_ = esv1alpha1.AddToScheme(util.Scheme)
 }
 
 type Framework struct {
@@ -64,7 +61,7 @@ func New(baseName string) *Framework {
 	f := &Framework{
 		BaseName: baseName,
 	}
-	f.KubeConfig, f.KubeClientSet, f.CRClient = NewConfig()
+	f.KubeConfig, f.KubeClientSet, f.CRClient = util.NewConfig()
 
 	BeforeEach(f.BeforeEach)
 	AfterEach(f.AfterEach)
@@ -110,27 +107,17 @@ func (f *Framework) Install(a addon.Addon) {
 	Expect(err).NotTo(HaveOccurred())
 }
 
-// NewConfig loads and returns the kubernetes credentials from the environment.
-// KUBECONFIG env var takes precedence and falls back to in-cluster config.
-func NewConfig() (*rest.Config, *kubernetes.Clientset, crclient.Client) {
-	var kubeConfig *rest.Config
-	var err error
-	kcPath := os.Getenv("KUBECONFIG")
-	if kcPath != "" {
-		kubeConfig, err = clientcmd.BuildConfigFromFlags("", kcPath)
-		Expect(err).NotTo(HaveOccurred())
-	} else {
-		kubeConfig, err = rest.InClusterConfig()
-		Expect(err).NotTo(HaveOccurred())
-	}
+// Compose helps define multiple testcases with same/different auth methods.
+func Compose(descAppend string, f *Framework, fn func(f *Framework) (string, func(*TestCase)), tweaks ...func(*TestCase)) TableEntry {
+	desc, tfn := fn(f)
+	tweaks = append(tweaks, tfn)
+	te := Entry(desc + " " + descAppend)
 
-	By("creating a kubernetes client")
-	kubeClientSet, err := kubernetes.NewForConfig(kubeConfig)
-	Expect(err).NotTo(HaveOccurred())
-
-	By("creating a controller-runtime client")
-	CRClient, err := crclient.New(kubeConfig, crclient.Options{Scheme: Scheme})
-	Expect(err).NotTo(HaveOccurred())
-
-	return kubeConfig, kubeClientSet, CRClient
+	// need to convert []func to []interface{}
+	ifs := make([]interface{}, len(tweaks))
+	for i := 0; i < len(tweaks); i++ {
+		ifs[i] = tweaks[i]
+	}
+	te.Parameters = ifs
+	return te
 }

+ 6 - 1
e2e/framework/testcase.go

@@ -22,6 +22,7 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
 	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+	"github.com/external-secrets/external-secrets/e2e/framework/log"
 )
 
 var TargetSecretName = "target-secret"
@@ -72,7 +73,11 @@ func TableFunc(f *Framework, prov SecretStoreProvider) func(...func(*TestCase))
 		}
 
 		// wait for Kind=Secret to have the expected data
-		_, err = tc.Framework.WaitForSecretValue(tc.Framework.Namespace.Name, TargetSecretName, tc.ExpectedSecret)
+		secret, err := tc.Framework.WaitForSecretValue(tc.Framework.Namespace.Name, TargetSecretName, tc.ExpectedSecret)
+		if err != nil {
+			log.Logf("Did not match. Expected: %+v, Got: %+v", tc.ExpectedSecret, secret)
+		}
+
 		Expect(err).ToNot(HaveOccurred())
 	}
 }

+ 47 - 0
e2e/framework/util/util.go

@@ -18,18 +18,28 @@ import (
 	"context"
 	"fmt"
 	"net/http"
+	"os"
 	"time"
 
+	// nolint
+	. "github.com/onsi/ginkgo"
+	// nolint
+	. "github.com/onsi/gomega"
 	v1 "k8s.io/api/core/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/util/wait"
 	"k8s.io/client-go/kubernetes"
 	"k8s.io/client-go/kubernetes/scheme"
 	restclient "k8s.io/client-go/rest"
+	"k8s.io/client-go/tools/clientcmd"
 	"k8s.io/client-go/tools/remotecommand"
+	crclient "sigs.k8s.io/controller-runtime/pkg/client"
 )
 
+var Scheme = runtime.NewScheme()
+
 const (
 	// How often to poll for conditions.
 	Poll = 2 * time.Second
@@ -206,3 +216,40 @@ func WaitForURL(url string) error {
 		return false, err
 	})
 }
+
+// UpdateKubeSA updates a new Kubernetes Service Account for a test.
+func UpdateKubeSA(baseName string, kubeClientSet kubernetes.Interface, ns string, annotations map[string]string) (*v1.ServiceAccount, error) {
+	sa := &v1.ServiceAccount{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:        baseName,
+			Annotations: annotations,
+		},
+	}
+
+	return kubeClientSet.CoreV1().ServiceAccounts(ns).Update(context.TODO(), sa, metav1.UpdateOptions{})
+}
+
+// NewConfig loads and returns the kubernetes credentials from the environment.
+// KUBECONFIG env var takes precedence and falls back to in-cluster config.
+func NewConfig() (*restclient.Config, *kubernetes.Clientset, crclient.Client) {
+	var kubeConfig *restclient.Config
+	var err error
+	kcPath := os.Getenv("KUBECONFIG")
+	if kcPath != "" {
+		kubeConfig, err = clientcmd.BuildConfigFromFlags("", kcPath)
+		Expect(err).NotTo(HaveOccurred())
+	} else {
+		kubeConfig, err = restclient.InClusterConfig()
+		Expect(err).NotTo(HaveOccurred())
+	}
+
+	By("creating a kubernetes client")
+	kubeClientSet, err := kubernetes.NewForConfig(kubeConfig)
+	Expect(err).NotTo(HaveOccurred())
+
+	By("creating a controller-runtime client")
+	CRClient, err := crclient.New(kubeConfig, crclient.Options{Scheme: Scheme})
+	Expect(err).NotTo(HaveOccurred())
+
+	return kubeConfig, kubeClientSet, CRClient
+}

+ 6 - 2
e2e/run.sh

@@ -25,7 +25,6 @@ cd $DIR
 
 echo "Kubernetes cluster:"
 kubectl get nodes -o wide
-kubectl describe node external-secrets-control-plane
 
 echo -e "Granting permissions to e2e service account..."
 kubectl create serviceaccount external-secrets-e2e || true
@@ -52,6 +51,11 @@ kubectl run --rm \
   --env="FOCUS=${FOCUS:-.*}" \
   --env="GCP_SM_SA_JSON=${GCP_SM_SA_JSON:-}" \
   --env="GCP_PROJECT_ID=${GCP_PROJECT_ID:-}" \
+  --env="TF_VAR_GCP_PROJECT_ID=${TF_VAR_GCP_PROJECT_ID:-}" \
+  --env="GCP_GSA_NAME=${GCP_GSA_NAME:-}" \
+  --env="GCP_KSA_NAME=${GCP_KSA_NAME:-}" \
+  --env="TF_VAR_GCP_GSA_NAME=${TF_VAR_GCP_GSA_NAME:-}" \
+  --env="TF_VAR_GCP_KSA_NAME=${TF_VAR_GCP_KSA_NAME:-}" \
   --env="AZURE_CLIENT_ID=${AZURE_CLIENT_ID:-}" \
   --env="AZURE_CLIENT_SECRET=${AZURE_CLIENT_SECRET:-}" \
   --env="AKEYLESS_ACCESS_ID=${AKEYLESS_ACCESS_ID:-}" \
@@ -67,4 +71,4 @@ kubectl run --rm \
   --env="ORACLE_FINGERPRINT=${ORACLE_FINGERPRINT:-}" \
   --env="ORACLE_KEY=${ORACLE_KEY:-}" \
   --overrides='{ "apiVersion": "v1", "spec":{"serviceAccountName": "external-secrets-e2e"}}' \
-  e2e --image=local/external-secrets-e2e:test
+  e2e --image=${E2E_IMAGE_REGISTRY}:${E2E_VERSION}

+ 2 - 2
e2e/suite/gcp/gcp.go

@@ -36,10 +36,10 @@ var _ = Describe("[gcp] ", func() {
 	f := framework.New("eso-gcp")
 	credentials := os.Getenv("GCP_SM_SA_JSON")
 	projectID := os.Getenv("GCP_PROJECT_ID")
-	prov := &gcpProvider{}
+	prov := &GcpProvider{}
 
 	if credentials != "" && projectID != "" {
-		prov = newgcpProvider(f, credentials, projectID)
+		prov = NewgcpProvider(f, credentials, projectID, "", "", "", "")
 	}
 
 	// P12Cert case creates a secret with a p12 cert containing a privkey and cert bundled together.

+ 106 - 39
e2e/suite/gcp/provider.go

@@ -23,11 +23,14 @@ import (
 
 	// nolint
 	. "github.com/onsi/gomega"
+	"golang.org/x/oauth2"
 	"golang.org/x/oauth2/google"
+	"golang.org/x/oauth2/jwt"
 	"google.golang.org/api/option"
 	secretmanagerpb "google.golang.org/genproto/googleapis/cloud/secretmanager/v1"
 	v1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	utilpointer "k8s.io/utils/pointer"
 
 	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
 	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
@@ -35,28 +38,74 @@ import (
 	gcpsm "github.com/external-secrets/external-secrets/pkg/provider/gcp/secretmanager"
 )
 
-type gcpProvider struct {
-	credentials string
-	projectID   string
-	framework   *framework.Framework
+const (
+	PodIDSecretStoreName     = "pod-identity"
+	SpecifcSASecretStoreName = "specific-sa"
+)
+
+func makeStore(s *GcpProvider) *esv1alpha1.SecretStore {
+	return &esv1alpha1.SecretStore{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      s.framework.Namespace.Name,
+			Namespace: s.framework.Namespace.Name,
+		},
+		Spec: esv1alpha1.SecretStoreSpec{
+			Provider: &esv1alpha1.SecretStoreProvider{
+				GCPSM: &esv1alpha1.GCPSMProvider{
+					ProjectID: s.projectID,
+				},
+			},
+		},
+	}
+}
+
+// nolint // Better to keep names consistent even if it stutters;
+type GcpProvider struct {
+	credentials             string
+	projectID               string
+	framework               *framework.Framework
+	clusterLocation         string
+	clusterName             string
+	serviceAccountName      string
+	serviceAccountNamespace string
 }
 
-func newgcpProvider(f *framework.Framework, credentials, projectID string) *gcpProvider {
-	prov := &gcpProvider{
-		credentials: credentials,
-		projectID:   projectID,
-		framework:   f,
+func NewgcpProvider(f *framework.Framework, credentials, projectID string,
+	clusterLocation string, clusterName string, serviceAccountName string, serviceAccountNamespace string) *GcpProvider {
+	prov := &GcpProvider{
+		credentials:             credentials,
+		projectID:               projectID,
+		framework:               f,
+		clusterLocation:         clusterLocation,
+		clusterName:             clusterName,
+		serviceAccountName:      serviceAccountName,
+		serviceAccountNamespace: serviceAccountNamespace,
 	}
 	BeforeEach(prov.BeforeEach)
 	return prov
 }
 
-func (s *gcpProvider) CreateSecret(key, val string) {
+func (s *GcpProvider) getClient(ctx context.Context, credentials string) (client *secretmanager.Client, err error) {
+	if credentials == "" {
+		var ts oauth2.TokenSource
+		ts, err = google.DefaultTokenSource(ctx, gcpsm.CloudPlatformRole)
+		Expect(err).ToNot(HaveOccurred())
+		client, err = secretmanager.NewClient(ctx, option.WithTokenSource(ts))
+		Expect(err).ToNot(HaveOccurred())
+	} else {
+		var config *jwt.Config
+		config, err = google.JWTConfigFromJSON([]byte(s.credentials), gcpsm.CloudPlatformRole)
+		Expect(err).ToNot(HaveOccurred())
+		ts := config.TokenSource(ctx)
+		client, err = secretmanager.NewClient(ctx, option.WithTokenSource(ts))
+		Expect(err).ToNot(HaveOccurred())
+	}
+	return client, err
+}
+
+func (s *GcpProvider) CreateSecret(key, val string) {
 	ctx := context.Background()
-	config, err := google.JWTConfigFromJSON([]byte(s.credentials), gcpsm.CloudPlatformRole)
-	Expect(err).ToNot(HaveOccurred())
-	ts := config.TokenSource(ctx)
-	client, err := secretmanager.NewClient(ctx, option.WithTokenSource(ts))
+	client, err := s.getClient(ctx, s.credentials)
 	Expect(err).ToNot(HaveOccurred())
 	defer client.Close()
 	// Create the request to create the secret.
@@ -83,12 +132,10 @@ func (s *gcpProvider) CreateSecret(key, val string) {
 	Expect(err).ToNot(HaveOccurred())
 }
 
-func (s *gcpProvider) DeleteSecret(key string) {
+func (s *GcpProvider) DeleteSecret(key string) {
 	ctx := context.Background()
-	config, err := google.JWTConfigFromJSON([]byte(s.credentials), gcpsm.CloudPlatformRole)
+	client, err := s.getClient(ctx, s.credentials)
 	Expect(err).ToNot(HaveOccurred())
-	ts := config.TokenSource(ctx)
-	client, err := secretmanager.NewClient(ctx, option.WithTokenSource(ts))
 	Expect(err).ToNot(HaveOccurred())
 	defer client.Close()
 	req := &secretmanagerpb.DeleteSecretRequest{
@@ -98,7 +145,7 @@ func (s *gcpProvider) DeleteSecret(key string) {
 	Expect(err).ToNot(HaveOccurred())
 }
 
-func (s *gcpProvider) BeforeEach() {
+func (s *GcpProvider) BeforeEach() {
 	By("creating a gcp secret")
 	gcpCreds := &v1.Secret{
 		ObjectMeta: metav1.ObjectMeta{
@@ -110,30 +157,50 @@ func (s *gcpProvider) BeforeEach() {
 		},
 	}
 	err := s.framework.CRClient.Create(context.Background(), gcpCreds)
-	Expect(err).ToNot(HaveOccurred())
+	if err != nil {
+		err = s.framework.CRClient.Update(context.Background(), gcpCreds)
+		Expect(err).ToNot(HaveOccurred())
+	}
+	By("creating an secret stores gcp")
+	s.CreateSAKeyStore(s.framework.Namespace.Name)
+	s.CreatePodIDStore(s.framework.Namespace.Name)
+	s.CreateSpecifcSASecretStore(s.framework.Namespace.Name)
+}
 
-	By("creating an secret store for vault")
-	secretStore := &esv1alpha1.SecretStore{
-		ObjectMeta: metav1.ObjectMeta{
-			Name:      s.framework.Namespace.Name,
-			Namespace: s.framework.Namespace.Name,
+func (s *GcpProvider) CreateSAKeyStore(ns string) {
+	secretStore := makeStore(s)
+	secretStore.Spec.Provider.GCPSM.Auth = esv1alpha1.GCPSMAuth{
+		SecretRef: &esv1alpha1.GCPSMAuthSecretRef{
+			SecretAccessKey: esmeta.SecretKeySelector{
+				Name: "provider-secret",
+				Key:  "secret-access-credentials",
+			},
 		},
-		Spec: esv1alpha1.SecretStoreSpec{
-			Provider: &esv1alpha1.SecretStoreProvider{
-				GCPSM: &esv1alpha1.GCPSMProvider{
-					ProjectID: s.projectID,
-					Auth: esv1alpha1.GCPSMAuth{
-						SecretRef: &esv1alpha1.GCPSMAuthSecretRef{
-							SecretAccessKey: esmeta.SecretKeySelector{
-								Name: "provider-secret",
-								Key:  "secret-access-credentials",
-							},
-						},
-					},
-				},
+	}
+	err := s.framework.CRClient.Create(context.Background(), secretStore)
+	Expect(err).ToNot(HaveOccurred())
+}
+
+func (s *GcpProvider) CreatePodIDStore(ns string) {
+	secretStore := makeStore(s)
+	secretStore.ObjectMeta.Name = PodIDSecretStoreName
+	err := s.framework.CRClient.Create(context.Background(), secretStore)
+	Expect(err).ToNot(HaveOccurred())
+}
+
+func (s *GcpProvider) CreateSpecifcSASecretStore(ns string) {
+	secretStore := makeStore(s)
+	secretStore.ObjectMeta.Name = SpecifcSASecretStoreName
+	secretStore.Spec.Provider.GCPSM.Auth = esv1alpha1.GCPSMAuth{
+		WorkloadIdentity: &esv1alpha1.GCPWorkloadIdentity{
+			ClusterLocation: s.clusterLocation,
+			ClusterName:     s.clusterName,
+			ServiceAccountRef: esmeta.ServiceAccountSelector{
+				Name:      s.serviceAccountName,
+				Namespace: utilpointer.StringPtr(s.serviceAccountNamespace),
 			},
 		},
 	}
-	err = s.framework.CRClient.Create(context.Background(), secretStore)
+	err := s.framework.CRClient.Create(context.Background(), secretStore)
 	Expect(err).ToNot(HaveOccurred())
 }

+ 84 - 0
e2e/suite/gcpmanaged/gcpmanaged.go

@@ -0,0 +1,84 @@
+/*
+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.
+limitations under the License.
+*/
+package gcpmanaged
+
+import (
+	"os"
+
+	// nolint
+	. "github.com/onsi/ginkgo"
+	// nolint
+	. "github.com/onsi/ginkgo/extensions/table"
+
+	// nolint
+	// . "github.com/onsi/gomega"
+	"github.com/external-secrets/external-secrets/e2e/framework"
+	"github.com/external-secrets/external-secrets/e2e/suite/common"
+	"github.com/external-secrets/external-secrets/e2e/suite/gcp"
+)
+
+const (
+	withPodID     = "sync secrets with pod identity"
+	withSpecifcSA = "sync secrets with specificSA identity"
+)
+
+var _ = Describe("[gcpmanaged] ", func() {
+	if os.Getenv("FOCUS") == "gcpmanaged" {
+		f := framework.New("eso-gcp-managed")
+		projectID := os.Getenv("GCP_PROJECT_ID")
+		clusterLocation := "europe-west1"
+		clusterName := "test-cluster"
+		serviceAccountName := os.Getenv("GCP_KSA_NAME")
+		serviceAccountNamespace := "default"
+		prov := &gcp.GcpProvider{}
+		if projectID != "" {
+			prov = gcp.NewgcpProvider(f, "", projectID, clusterLocation, clusterName, serviceAccountName, serviceAccountNamespace)
+		}
+		DescribeTable("sync secrets",
+			framework.TableFunc(f,
+				prov),
+			// uses pod id
+			framework.Compose(withPodID, f, common.SimpleDataSync, usePodIDESReference),
+			framework.Compose(withPodID, f, common.JSONDataWithProperty, usePodIDESReference),
+			framework.Compose(withPodID, f, common.JSONDataFromSync, usePodIDESReference),
+			framework.Compose(withPodID, f, common.NestedJSONWithGJSON, usePodIDESReference),
+			framework.Compose(withPodID, f, common.JSONDataWithTemplate, usePodIDESReference),
+			framework.Compose(withPodID, f, common.DockerJSONConfig, usePodIDESReference),
+			framework.Compose(withPodID, f, common.DataPropertyDockerconfigJSON, usePodIDESReference),
+			framework.Compose(withPodID, f, common.SSHKeySync, usePodIDESReference),
+			framework.Compose(withPodID, f, common.SSHKeySyncDataProperty, usePodIDESReference),
+			framework.Compose(withPodID, f, common.SyncWithoutTargetName, usePodIDESReference),
+			framework.Compose(withPodID, f, common.JSONDataWithoutTargetName, usePodIDESReference),
+			// uses specific sa
+			framework.Compose(withSpecifcSA, f, common.JSONDataFromSync, useSpecifcSAESReference),
+			framework.Compose(withSpecifcSA, f, common.JSONDataWithProperty, useSpecifcSAESReference),
+			framework.Compose(withSpecifcSA, f, common.JSONDataFromSync, useSpecifcSAESReference),
+			framework.Compose(withSpecifcSA, f, common.NestedJSONWithGJSON, useSpecifcSAESReference),
+			framework.Compose(withSpecifcSA, f, common.JSONDataWithTemplate, useSpecifcSAESReference),
+			framework.Compose(withSpecifcSA, f, common.DockerJSONConfig, useSpecifcSAESReference),
+			framework.Compose(withSpecifcSA, f, common.DataPropertyDockerconfigJSON, useSpecifcSAESReference),
+			framework.Compose(withSpecifcSA, f, common.SSHKeySync, useSpecifcSAESReference),
+			framework.Compose(withSpecifcSA, f, common.SSHKeySyncDataProperty, useSpecifcSAESReference),
+			framework.Compose(withSpecifcSA, f, common.SyncWithoutTargetName, useSpecifcSAESReference),
+			framework.Compose(withSpecifcSA, f, common.JSONDataWithoutTargetName, useSpecifcSAESReference),
+		)
+	}
+})
+
+func usePodIDESReference(tc *framework.TestCase) {
+	tc.ExternalSecret.Spec.SecretStoreRef.Name = gcp.PodIDSecretStoreName
+}
+
+func useSpecifcSAESReference(tc *framework.TestCase) {
+	tc.ExternalSecret.Spec.SecretStoreRef.Name = gcp.SpecifcSASecretStoreName
+}

+ 1 - 0
e2e/suite/import.go

@@ -19,5 +19,6 @@ import (
 	_ "github.com/external-secrets/external-secrets/e2e/suite/aws"
 	_ "github.com/external-secrets/external-secrets/e2e/suite/azure"
 	_ "github.com/external-secrets/external-secrets/e2e/suite/gcp"
+	_ "github.com/external-secrets/external-secrets/e2e/suite/gcpmanaged"
 	_ "github.com/external-secrets/external-secrets/e2e/suite/vault"
 )

+ 30 - 44
e2e/suite/vault/vault.go

@@ -39,58 +39,44 @@ var _ = Describe("[vault] ", func() {
 		framework.TableFunc(f,
 			newVaultProvider(f)),
 		// uses token auth
-		compose(withTokenAuth, f, common.JSONDataFromSync, useTokenAuth),
-		compose(withTokenAuth, f, common.JSONDataWithProperty, useTokenAuth),
-		compose(withTokenAuth, f, common.JSONDataWithTemplate, useTokenAuth),
-		compose(withTokenAuth, f, common.DataPropertyDockerconfigJSON, useTokenAuth),
-		compose(withTokenAuth, f, common.JSONDataWithoutTargetName, useTokenAuth),
+		framework.Compose(withTokenAuth, f, common.JSONDataFromSync, useTokenAuth),
+		framework.Compose(withTokenAuth, f, common.JSONDataWithProperty, useTokenAuth),
+		framework.Compose(withTokenAuth, f, common.JSONDataWithTemplate, useTokenAuth),
+		framework.Compose(withTokenAuth, f, common.DataPropertyDockerconfigJSON, useTokenAuth),
+		framework.Compose(withTokenAuth, f, common.JSONDataWithoutTargetName, useTokenAuth),
 		// use cert auth
-		compose(withCertAuth, f, common.JSONDataFromSync, useCertAuth),
-		compose(withCertAuth, f, common.JSONDataWithProperty, useCertAuth),
-		compose(withCertAuth, f, common.JSONDataWithTemplate, useCertAuth),
-		compose(withCertAuth, f, common.DataPropertyDockerconfigJSON, useCertAuth),
-		compose(withCertAuth, f, common.JSONDataWithoutTargetName, useTokenAuth),
+		framework.Compose(withCertAuth, f, common.JSONDataFromSync, useCertAuth),
+		framework.Compose(withCertAuth, f, common.JSONDataWithProperty, useCertAuth),
+		framework.Compose(withCertAuth, f, common.JSONDataWithTemplate, useCertAuth),
+		framework.Compose(withCertAuth, f, common.DataPropertyDockerconfigJSON, useCertAuth),
+		framework.Compose(withCertAuth, f, common.JSONDataWithoutTargetName, useTokenAuth),
 		// use approle auth
-		compose(withApprole, f, common.JSONDataFromSync, useApproleAuth),
-		compose(withApprole, f, common.JSONDataWithProperty, useApproleAuth),
-		compose(withApprole, f, common.JSONDataWithTemplate, useApproleAuth),
-		compose(withApprole, f, common.DataPropertyDockerconfigJSON, useApproleAuth),
-		compose(withApprole, f, common.JSONDataWithoutTargetName, useTokenAuth),
+		framework.Compose(withApprole, f, common.JSONDataFromSync, useApproleAuth),
+		framework.Compose(withApprole, f, common.JSONDataWithProperty, useApproleAuth),
+		framework.Compose(withApprole, f, common.JSONDataWithTemplate, useApproleAuth),
+		framework.Compose(withApprole, f, common.DataPropertyDockerconfigJSON, useApproleAuth),
+		framework.Compose(withApprole, f, common.JSONDataWithoutTargetName, useTokenAuth),
 		// use v1 provider
-		compose(withV1, f, common.JSONDataFromSync, useV1Provider),
-		compose(withV1, f, common.JSONDataWithProperty, useV1Provider),
-		compose(withV1, f, common.JSONDataWithTemplate, useV1Provider),
-		compose(withV1, f, common.DataPropertyDockerconfigJSON, useV1Provider),
-		compose(withV1, f, common.JSONDataWithoutTargetName, useTokenAuth),
+		framework.Compose(withV1, f, common.JSONDataFromSync, useV1Provider),
+		framework.Compose(withV1, f, common.JSONDataWithProperty, useV1Provider),
+		framework.Compose(withV1, f, common.JSONDataWithTemplate, useV1Provider),
+		framework.Compose(withV1, f, common.DataPropertyDockerconfigJSON, useV1Provider),
+		framework.Compose(withV1, f, common.JSONDataWithoutTargetName, useTokenAuth),
 		// use jwt provider
-		compose(withJWT, f, common.JSONDataFromSync, useJWTProvider),
-		compose(withJWT, f, common.JSONDataWithProperty, useJWTProvider),
-		compose(withJWT, f, common.JSONDataWithTemplate, useJWTProvider),
-		compose(withJWT, f, common.DataPropertyDockerconfigJSON, useJWTProvider),
-		compose(withJWT, f, common.JSONDataWithoutTargetName, useTokenAuth),
+		framework.Compose(withJWT, f, common.JSONDataFromSync, useJWTProvider),
+		framework.Compose(withJWT, f, common.JSONDataWithProperty, useJWTProvider),
+		framework.Compose(withJWT, f, common.JSONDataWithTemplate, useJWTProvider),
+		framework.Compose(withJWT, f, common.DataPropertyDockerconfigJSON, useJWTProvider),
+		framework.Compose(withJWT, f, common.JSONDataWithoutTargetName, useTokenAuth),
 		// use kubernetes provider
-		compose(withK8s, f, common.JSONDataFromSync, useKubernetesProvider),
-		compose(withK8s, f, common.JSONDataWithProperty, useKubernetesProvider),
-		compose(withK8s, f, common.JSONDataWithTemplate, useKubernetesProvider),
-		compose(withK8s, f, common.DataPropertyDockerconfigJSON, useKubernetesProvider),
-		compose(withK8s, f, common.JSONDataWithoutTargetName, useTokenAuth),
+		framework.Compose(withK8s, f, common.JSONDataFromSync, useKubernetesProvider),
+		framework.Compose(withK8s, f, common.JSONDataWithProperty, useKubernetesProvider),
+		framework.Compose(withK8s, f, common.JSONDataWithTemplate, useKubernetesProvider),
+		framework.Compose(withK8s, f, common.DataPropertyDockerconfigJSON, useKubernetesProvider),
+		framework.Compose(withK8s, f, common.JSONDataWithoutTargetName, useTokenAuth),
 	)
 })
 
-func compose(descAppend string, f *framework.Framework, fn func(f *framework.Framework) (string, func(*framework.TestCase)), tweaks ...func(*framework.TestCase)) TableEntry {
-	desc, tfn := fn(f)
-	tweaks = append(tweaks, tfn)
-	te := Entry(desc + " " + descAppend)
-
-	// need to convert []func to []interface{}
-	ifs := make([]interface{}, len(tweaks))
-	for i := 0; i < len(tweaks); i++ {
-		ifs[i] = tweaks[i]
-	}
-	te.Parameters = ifs
-	return te
-}
-
 func useTokenAuth(tc *framework.TestCase) {
 	tc.ExternalSecret.Spec.SecretStoreRef.Name = tc.Framework.Namespace.Name
 }

+ 59 - 0
terraform/gcp/eso_gcp_modules/gke/main.tf

@@ -0,0 +1,59 @@
+resource "google_service_account" "default" {
+  account_id = var.GCP_GSA_NAME
+}
+
+resource "google_project_iam_member" "secretadmin" {
+  project = var.project_id
+  role    = "roles/secretmanager.admin"
+  member  = "serviceAccount:${google_service_account.default.email}"
+}
+
+resource "google_project_iam_member" "service_account_token_creator" {
+  project = var.project_id
+  role    = "roles/iam.serviceAccountTokenCreator"
+  member  = "serviceAccount:${google_service_account.default.email}"
+}
+
+resource "google_service_account_iam_member" "pod_identity" {
+  role               = "roles/iam.workloadIdentityUser"
+  member             = "serviceAccount:${var.project_id}.svc.id.goog[default/${var.GCP_KSA_NAME}]"
+  service_account_id = google_service_account.default.name
+}
+
+resource "google_service_account_iam_member" "pod_identity_e2e" {
+  role               = "roles/iam.workloadIdentityUser"
+  member             = "serviceAccount:${var.project_id}.svc.id.goog[default/external-secrets-e2e]"
+  service_account_id = google_service_account.default.name
+}
+
+resource "google_container_cluster" "primary" {
+  name                     = "${var.env}-cluster"
+  location                 = var.zone
+  remove_default_node_pool = true
+  initial_node_count       = var.initial_node_count
+  network                  = var.network
+  subnetwork               = var.subnetwork
+  ip_allocation_policy {}
+  workload_identity_config {
+    workload_pool = "${var.project_id}.svc.id.goog"
+  }
+  resource_labels = {
+    "example" = "value"
+  }
+}
+
+resource "google_container_node_pool" "nodes" {
+  name       = "${google_container_cluster.primary.name}-node-pool"
+  location   = google_container_cluster.primary.location
+  cluster    = google_container_cluster.primary.name
+  node_count = var.node_count
+
+  node_config {
+    preemptible     = var.preemptible
+    machine_type    = "n1-standard-2"
+    service_account = google_service_account.default.email
+    oauth_scopes = [
+      "https://www.googleapis.com/auth/cloud-platform"
+    ]
+  }
+}

+ 48 - 0
terraform/gcp/eso_gcp_modules/gke/variable.tf

@@ -0,0 +1,48 @@
+variable "project_id" {
+  default = "my-project-1475718618821"
+}
+variable "env" {
+  default = "dev"
+}
+variable "region" {
+  default = "europe-west1"
+}
+variable "zone" {
+  default = "europe-west1-b"
+}
+variable "zones" {
+  default = ["europe-west1-a", "europe-west1-b", "europe-west1-c"]
+}
+variable "network" {
+  default = "dev-vpc"
+}
+variable "subnetwork" {
+  default = "dev-subnetwork"
+}
+variable "ip_pod_range" {
+  default = "dev-pod-ip-range"
+}
+variable "ip_service_range" {
+  default = "dev-service-ip-range"
+}
+variable "horizontal_pod_autoscaling" {
+  default = false
+}
+variable "node_count" {
+  default = 2
+}
+variable "node_min_count" {
+  default = 2
+}
+variable "node_max_count" {
+  default = 2
+}
+variable "initial_node_count" {
+  default = 2
+}
+variable "preemptible" {
+  default = true
+}
+
+variable "GCP_GSA_NAME" {type = string}
+variable "GCP_KSA_NAME" {type = string}

+ 27 - 0
terraform/gcp/eso_gcp_modules/network/main.tf

@@ -0,0 +1,27 @@
+resource "google_compute_network" "env-vpc" {
+  name          =  "${var.env}-vpc"
+  auto_create_subnetworks = false
+}
+
+resource "google_compute_subnetwork" "env-subnet" {
+  name          = "${google_compute_network.env-vpc.name}-subnet"
+  region        = var.region
+  network       = google_compute_network.env-vpc.name
+  ip_cidr_range = "10.10.0.0/24"
+}
+
+output "vpc-name" {
+  value = google_compute_network.env-vpc.name
+}
+output "vpc-id" {
+  value = google_compute_network.env-vpc.id
+}
+output "vpc-object" {
+  value = google_compute_network.env-vpc.self_link
+}
+output "subnet-name" {
+  value = google_compute_subnetwork.env-subnet.name
+}
+output "subnet-ip_cidr_range" {
+  value = google_compute_subnetwork.env-subnet.ip_cidr_range
+}

+ 15 - 0
terraform/gcp/eso_gcp_modules/network/variable.tf

@@ -0,0 +1,15 @@
+variable "env" {
+  default = "dev"
+}
+variable "ip_cidr_range" {
+  default = "10.69.0.0/16"
+}
+variable "ip_pod_range" {
+  default = "10.70.0.0/16"
+}
+variable "ip_service_range" {
+  default = "10.71.0.0/16"
+}
+variable "region" {
+  default = "europe-west1"
+}

+ 28 - 0
terraform/gcp/main.tf

@@ -0,0 +1,28 @@
+terraform {
+  backend "gcs" {
+    bucket = "eso-infra-state"
+    prefix = "eso-infra-state/state"
+    credentials = "secrets/gcloud-service-account-key.json"
+  }
+}
+
+module "test-network" {
+  source = "./eso_gcp_modules/network"
+  env = var.env
+  region = var.region
+  ip_cidr_range = var.ip_cidr_range
+}
+
+module "test-cluster" {
+  source = "./eso_gcp_modules/gke"
+  project_id = var.project_id
+  env = var.env
+  region = var.region
+  network = module.test-network.vpc-object
+  subnetwork = module.test-network.subnet-name
+  node_count = var.node_count
+  initial_node_count = var.initial_node_count
+  preemptible = true
+  GCP_GSA_NAME = var.GCP_GSA_NAME
+  GCP_KSA_NAME = var.GCP_KSA_NAME
+}

+ 13 - 0
terraform/gcp/provider.tf

@@ -0,0 +1,13 @@
+provider "google" {
+  project = "external-secrets-operator"
+  region = "europe-west1"
+  zone = "europe-west1-b"
+  credentials = file(var.credentials_path)
+}
+
+provider "google-beta" {
+  project = "external-secrets-operator"
+  region = "europe-west1"
+  zone = "europe-west1-b"
+  credentials = file(var.credentials_path)
+}

+ 3 - 0
terraform/gcp/provider_variables.tf

@@ -0,0 +1,3 @@
+variable "credentials_path" {
+  default = "secrets/gcloud-service-account-key.json"
+}

+ 0 - 0
terraform/gcp/secrets/.gitkeep


+ 17 - 0
terraform/gcp/variable.tf

@@ -0,0 +1,17 @@
+variable "env" { default = "test" }
+variable "region" { default = "europe-west1" }
+variable "zone" { default = "europe-west1-b" }
+variable "project_id" { default = "external-secrets-operator" }
+variable "horizontal_pod_autoscaling" { default = false }
+variable "node_count" { default = 2 }
+variable "node_min_count" { default = 2 }
+variable "node_max_count" { default = 2 }
+variable "initial_node_count" { default = 2 }
+variable "max_scale" { default = "10" }
+variable "ip_cidr_range" { default = "10.69.0.0/16" }
+variable "ip-pod-range" { default = "10.70.0.0/16" }
+variable "ip_service_range" { default = "10.71.0.0/16" }
+variable "preemptible" { default = true }
+variable "GCP_PROJECT_ID" {type = string}
+variable "GCP_GSA_NAME" {type = string}
+variable "GCP_KSA_NAME" {type = string}