Browse Source

Merge pull request #319 from Jabray5/gitlab-ci-secrets

Gitlab CI variables
paul-the-alien[bot] 4 years ago
parent
commit
5e433b6ac9

+ 3 - 1
README.md

@@ -18,6 +18,7 @@ Multiple people and organizations are joining efforts to create a single Externa
 - [Azure Key Vault](https://external-secrets.io/provider-azure-key-vault/)
 - [IBM Cloud Secrets Manager](https://external-secrets.io/provider-ibm-secrets-manager/)
 - [Yandex Lockbox](https://external-secrets.io/provider-yandex-lockbox/)
+- [Gitlab Project Variables](https://external-secrets.io/provider-gitlab-project-variables/)
 
 ## Stability and Support Level
 
@@ -36,7 +37,8 @@ Multiple people and organizations are joining efforts to create a single Externa
 | ------------------------------------------------------------------- | :-------: | :----------------------------------------: |
 | [Azure KV](https://external-secrets.io/provider-azure-key-vault/)   |   alpha   | @ahmedmus-1A @asnowfix @ncourbet-1A @1A-mj |
 | [IBM SM](https://external-secrets.io/provider-ibm-secrets-manager/) |   alpha   |   @knelasevero @sebagomez @ricardoptcosta  |
-| [Yandex Lockbox](https://external-secrets.io/provider-yandex-lockbox/) |   alpha   |   @AndreyZamyslov @knelasevero         |
+| [Yandex Lockbox](https://external-secrets.io/provider-yandex-lockbox/) |   alpha   |   @AndreyZamyslov @knelasevero          |
+| [Gitlab Project Variables](https://external-secrets.io/provider-gitlab-project-variables/) |   alpha   |   @Jabray5          |
 
 ## Documentation
 

+ 38 - 0
apis/externalsecrets/v1alpha1/secretstore_gitlab_types.go

@@ -0,0 +1,38 @@
+/*
+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.
+*/
+
+package v1alpha1
+
+import (
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+)
+
+// Configures an store to sync secrets using a IBM Cloud Secrets Manager
+// backend.
+type GitlabProvider struct {
+	// Auth configures how secret-manager authenticates with the IBM secrets manager.
+	Auth GitlabAuth `json:"auth"`
+
+	// ProjectID project where secret is located
+	ProjectID string `json:"projectID,omitempty"`
+}
+
+type GitlabAuth struct {
+	SecretRef GitlabSecretRef `json:"SecretRef"`
+}
+
+type GitlabSecretRef struct {
+	// The Access Token is used for authentication
+	AccessToken esmeta.SecretKeySelector `json:"accessToken,omitempty"`
+}

+ 4 - 0
apis/externalsecrets/v1alpha1/secretstore_types.go

@@ -57,6 +57,10 @@ type SecretStoreProvider struct {
 	// YandexLockbox configures this store to sync secrets using Yandex Lockbox provider
 	// +optional
 	YandexLockbox *YandexLockboxProvider `json:"yandexlockbox,omitempty"`
+
+	// GItlab configures this store to sync secrets using Gitlab Variables provider
+	// +optional
+	Gitlab *GitlabProvider `json:"gitlab,omitempty"`
 }
 
 type SecretStoreConditionType string

+ 53 - 0
apis/externalsecrets/v1alpha1/zz_generated.deepcopy.go

@@ -505,6 +505,54 @@ func (in *GCPSMProvider) DeepCopy() *GCPSMProvider {
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *GitlabAuth) DeepCopyInto(out *GitlabAuth) {
+	*out = *in
+	in.SecretRef.DeepCopyInto(&out.SecretRef)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitlabAuth.
+func (in *GitlabAuth) DeepCopy() *GitlabAuth {
+	if in == nil {
+		return nil
+	}
+	out := new(GitlabAuth)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *GitlabProvider) DeepCopyInto(out *GitlabProvider) {
+	*out = *in
+	in.Auth.DeepCopyInto(&out.Auth)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitlabProvider.
+func (in *GitlabProvider) DeepCopy() *GitlabProvider {
+	if in == nil {
+		return nil
+	}
+	out := new(GitlabProvider)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *GitlabSecretRef) DeepCopyInto(out *GitlabSecretRef) {
+	*out = *in
+	in.AccessToken.DeepCopyInto(&out.AccessToken)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitlabSecretRef.
+func (in *GitlabSecretRef) DeepCopy() *GitlabSecretRef {
+	if in == nil {
+		return nil
+	}
+	out := new(GitlabSecretRef)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *IBMAuth) DeepCopyInto(out *IBMAuth) {
 	*out = *in
 	in.SecretRef.DeepCopyInto(&out.SecretRef)
@@ -649,6 +697,11 @@ func (in *SecretStoreProvider) DeepCopyInto(out *SecretStoreProvider) {
 		*out = new(YandexLockboxProvider)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.Gitlab != nil {
+		in, out := &in.Gitlab, &out.Gitlab
+		*out = new(GitlabProvider)
+		(*in).DeepCopyInto(*out)
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretStoreProvider.

+ 40 - 0
deploy/crds/external-secrets.io_clustersecretstores.yaml

@@ -250,6 +250,46 @@ spec:
                         description: ProjectID project where secret is located
                         type: string
                     type: object
+                  gitlab:
+                    description: GItlab configures this store to sync secrets using
+                      Gitlab Variables provider
+                    properties:
+                      auth:
+                        description: Auth configures how secret-manager authenticates
+                          with the IBM secrets manager.
+                        properties:
+                          SecretRef:
+                            properties:
+                              accessToken:
+                                description: The Access Token is used for authentication
+                                properties:
+                                  key:
+                                    description: The key of the entry in the Secret
+                                      resource's `data` field to be used. Some instances
+                                      of this field may be defaulted, in others it
+                                      may be required.
+                                    type: string
+                                  name:
+                                    description: The name of the Secret resource being
+                                      referred to.
+                                    type: string
+                                  namespace:
+                                    description: Namespace of the resource being referred
+                                      to. Ignored if referent is not cluster-scoped.
+                                      cluster-scoped defaults to the namespace of
+                                      the referent.
+                                    type: string
+                                type: object
+                            type: object
+                        required:
+                        - SecretRef
+                        type: object
+                      projectID:
+                        description: ProjectID project where secret is located
+                        type: string
+                    required:
+                    - auth
+                    type: object
                   ibm:
                     description: IBM configures this store to sync secrets using IBM
                       Cloud provider

+ 40 - 0
deploy/crds/external-secrets.io_secretstores.yaml

@@ -250,6 +250,46 @@ spec:
                         description: ProjectID project where secret is located
                         type: string
                     type: object
+                  gitlab:
+                    description: GItlab configures this store to sync secrets using
+                      Gitlab Variables provider
+                    properties:
+                      auth:
+                        description: Auth configures how secret-manager authenticates
+                          with the IBM secrets manager.
+                        properties:
+                          SecretRef:
+                            properties:
+                              accessToken:
+                                description: The Access Token is used for authentication
+                                properties:
+                                  key:
+                                    description: The key of the entry in the Secret
+                                      resource's `data` field to be used. Some instances
+                                      of this field may be defaulted, in others it
+                                      may be required.
+                                    type: string
+                                  name:
+                                    description: The name of the Secret resource being
+                                      referred to.
+                                    type: string
+                                  namespace:
+                                    description: Namespace of the resource being referred
+                                      to. Ignored if referent is not cluster-scoped.
+                                      cluster-scoped defaults to the namespace of
+                                      the referent.
+                                    type: string
+                                type: object
+                            type: object
+                        required:
+                        - SecretRef
+                        type: object
+                      projectID:
+                        description: ProjectID project where secret is located
+                        type: string
+                    required:
+                    - auth
+                    type: object
                   ibm:
                     description: IBM configures this store to sync secrets using IBM
                       Cloud provider

BIN
docs/pictures/screenshot_gitlab_projectID.png


BIN
docs/pictures/screenshot_gitlab_token.png


BIN
docs/pictures/screenshot_gitlab_token_created.png


+ 54 - 0
docs/provider-gitlab-project-variables.md

@@ -0,0 +1,54 @@
+## Gitlab Project Variables
+
+External Secrets Operator integrates with [Gitlab API](https://docs.gitlab.com/ee/api/project_level_variables.html) to sync Gitlab project variables to secrets held on the Kubernetes cluster.
+
+### Authentication
+
+The API requires an access token and project ID. To create a new access token, go to your user settings and select 'access tokens'. Give your token a name, expiration date, and select the permissions required (Note 'api' is required).
+
+![token-details](./pictures/screenshot_gitlab_token.png)
+
+Click 'Create personal access token', and your token will be generated and displayed on screen. Copy or save this token since you can't access it again. 
+![token-created](./pictures/screenshot_gitlab_token_created.png)
+
+
+
+### Access Token secret
+
+Create a secret containing your access token:
+
+```yaml
+{% include 'gitlab-credentials-secret.yaml' %}
+```
+
+### Update secret store
+Be sure the `gitlab` provider is listed in the `Kind=SecretStore` and the ProjectID is set
+
+```yaml
+{% include 'gitlab-secret-store.yaml' %}
+```
+
+Your project ID can be found on your project's page.
+![projectID](./pictures/screenshot_gitlab_projectID.png)
+
+### Creating external secret
+
+To sync a Gitlab variable to a secret on the Kubernetes cluster, a `Kind=ExternalSecret` is needed.
+
+```yaml
+{% include 'gitlab-external-secret.yaml' %}
+```
+
+#### Using DataFrom
+
+DataFrom can be used to get a variable as a JSON string and attempt to parse it.
+
+```yaml
+{% include 'gitlab-external-secret-json.yaml' %}
+```
+
+### Getting the Kubernetes secret
+The operator will fetch the project variable and inject it as a `Kind=Secret`.
+```
+kubectl get secret gitlab-secret-to-create -o jsonpath='{.data.secretKey}' | base64 -d
+```

+ 9 - 0
docs/snippets/gitlab-credentials-secret.yaml

@@ -0,0 +1,9 @@
+apiVersion: v1
+kind: Secret
+metadata:
+  name: gitlab-secret
+  labels: 
+    type: gitlab
+type: Opaque 
+stringData:
+  token: "**access token goes here**"

+ 18 - 0
docs/snippets/gitlab-external-secret-json.yaml

@@ -0,0 +1,18 @@
+apiVersion: external-secrets.io/v1alpha1
+kind: ExternalSecret
+metadata:
+  name: gitlab-external-secret-example
+spec:
+  refreshInterval: 1h
+
+  secretStoreRef:
+    kind: SecretStore
+    name: gitlab-secret-store # Must match SecretStore on the cluster
+
+  target:
+    name: gitlab-secret-to-create # Name for the secret to be created on the cluster
+    creationPolicy: Owner
+
+  # each secret name in the KV will be used as the secret key in the SECRET k8s target object
+  dataFrom:
+  - key: "myJsonVariable" # Key of the variable on Gitlab

+ 19 - 0
docs/snippets/gitlab-external-secret.yaml

@@ -0,0 +1,19 @@
+apiVersion: external-secrets.io/v1alpha1
+kind: ExternalSecret
+metadata:
+  name: gitlab-external-secret-example
+spec:
+  refreshInterval: 1h
+
+  secretStoreRef:
+    kind: SecretStore
+    name: gitlab-secret-store # Must match SecretStore on the cluster
+
+  target:
+    name: gitlab-secret-to-create # Name for the secret to be created on the cluster
+    creationPolicy: Owner
+
+  data:
+    - secretKey: secretKey # Key given to the secret to be created on the cluster
+      remoteRef: 
+        key: myGitlabVariable # Key of the variable on Gitlab

+ 14 - 0
docs/snippets/gitlab-secret-store.yaml

@@ -0,0 +1,14 @@
+apiVersion: external-secrets.io/v1alpha1
+kind: SecretStore
+metadata:
+  name: gitlab-secret-store
+spec:
+  provider:
+    # provider type: gitlab
+    gitlab:
+      auth:
+        SecretRef:
+          accessToken:
+            name: gitlab-secret
+            key: token
+      projectID: "**project ID goes here**"

+ 117 - 0
docs/spec.md

@@ -1176,6 +1176,109 @@ string
 <p>GenericStore is a common interface for interacting with ClusterSecretStore
 or a namespaced SecretStore.</p>
 </p>
+<h3 id="external-secrets.io/v1alpha1.GitlabAuth">GitlabAuth
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1alpha1.GitlabProvider">GitlabProvider</a>)
+</p>
+<p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>SecretRef</code></br>
+<em>
+<a href="#external-secrets.io/v1alpha1.GitlabSecretRef">
+GitlabSecretRef
+</a>
+</em>
+</td>
+<td>
+</td>
+</tr>
+</tbody>
+</table>
+<h3 id="external-secrets.io/v1alpha1.GitlabProvider">GitlabProvider
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1alpha1.SecretStoreProvider">SecretStoreProvider</a>)
+</p>
+<p>
+<p>Configures an store to sync secrets using a IBM Cloud Secrets Manager
+backend.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>auth</code></br>
+<em>
+<a href="#external-secrets.io/v1alpha1.GitlabAuth">
+GitlabAuth
+</a>
+</em>
+</td>
+<td>
+<p>Auth configures how secret-manager authenticates with the IBM secrets manager.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>projectID</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<p>ProjectID project where secret is located</p>
+</td>
+</tr>
+</tbody>
+</table>
+<h3 id="external-secrets.io/v1alpha1.GitlabSecretRef">GitlabSecretRef
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1alpha1.GitlabAuth">GitlabAuth</a>)
+</p>
+<p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>accessToken</code></br>
+<em>
+github.com/external-secrets/external-secrets/apis/meta/v1.SecretKeySelector
+</em>
+</td>
+<td>
+<p>The Access Token is used for authentication</p>
+</td>
+</tr>
+</tbody>
+</table>
 <h3 id="external-secrets.io/v1alpha1.IBMAuth">IBMAuth
 </h3>
 <p>
@@ -1483,6 +1586,20 @@ YandexLockboxProvider
 <p>YandexLockbox configures this store to sync secrets using Yandex Lockbox provider</p>
 </td>
 </tr>
+<tr>
+<td>
+<code>gitlab</code></br>
+<em>
+<a href="#external-secrets.io/v1alpha1.GitlabProvider">
+GitlabProvider
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>GItlab configures this store to sync secrets using Gitlab Variables provider</p>
+</td>
+</tr>
 </tbody>
 </table>
 <h3 id="external-secrets.io/v1alpha1.SecretStoreRef">SecretStoreRef

+ 2 - 0
e2e/run.sh

@@ -58,5 +58,7 @@ kubectl run --rm \
   --env="AZURE_CLIENT_SECRET=${AZURE_CLIENT_SECRET:-}" \
   --env="TENANT_ID=${TENANT_ID:-}" \
   --env="VAULT_URL=${VAULT_URL:-}" \
+  --env="GITLAB_TOKEN=${GITLAB_TOKEN:-}" \
+  --env="GITLAB_PROJECT_ID=${GITLAB_PROJECT_ID:-}" \
   --overrides='{ "apiVersion": "v1", "spec":{"serviceAccountName": "external-secrets-e2e"}}' \
   e2e --image=local/external-secrets-e2e:test

+ 45 - 0
e2e/suite/gitlab/gitlab.go

@@ -0,0 +1,45 @@
+/*
+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 gitlab
+
+// TODO - Gitlab only accepts variable names with alphanumeric and '_'
+// whereas ESO only accepts names with alphanumeric and '-'.
+// Current workaround is to remove all hyphens and underscores set in e2e/framework/util/util.go
+// and in e2e/suite/common/common.go, but this breaks Azure provider.
+
+import (
+	"os"
+
+	// nolint
+	. "github.com/onsi/ginkgo"
+	// nolint
+	. "github.com/onsi/ginkgo/extensions/table"
+
+	"github.com/external-secrets/external-secrets/e2e/framework"
+	"github.com/external-secrets/external-secrets/e2e/suite/common"
+)
+
+var _ = Describe("[gitlab] ", func() {
+	f := framework.New("esogitlab")
+	credentials := os.Getenv("GITLAB_TOKEN")
+	projectID := os.Getenv("GITLAB_PROJECT_ID")
+	prov := newGitlabProvider(f, credentials, projectID)
+
+	DescribeTable("sync secrets", framework.TableFunc(f, prov),
+		Entry(common.SimpleDataSync(f)),
+		Entry(common.JSONDataWithProperty(f)),
+		Entry(common.JSONDataFromSync(f)),
+		Entry(common.NestedJSONWithGJSON(f)),
+		Entry(common.JSONDataWithTemplate(f)),
+	)
+})

+ 131 - 0
e2e/suite/gitlab/provider.go

@@ -0,0 +1,131 @@
+/*
+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.
+*/
+package gitlab
+
+import (
+	"context"
+	"strings"
+
+	// nolint
+	. "github.com/onsi/ginkgo"
+
+	// nolint
+	. "github.com/onsi/gomega"
+	gitlab "github.com/xanzy/go-gitlab"
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+	"github.com/external-secrets/external-secrets/e2e/framework"
+)
+
+type gitlabProvider struct {
+	credentials string
+	projectID   string
+	framework   *framework.Framework
+}
+
+func newGitlabProvider(f *framework.Framework, credentials, projectID string) *gitlabProvider {
+	prov := &gitlabProvider{
+		credentials: credentials,
+		projectID:   projectID,
+		framework:   f,
+	}
+	BeforeEach(prov.BeforeEach)
+	return prov
+}
+
+func (s *gitlabProvider) CreateSecret(key, val string) {
+	// **Open the client
+	client, err := gitlab.NewClient(s.credentials)
+	Expect(err).ToNot(HaveOccurred())
+	// Open the client**
+
+	// Set variable options
+	variableKey := strings.ReplaceAll(key, "-", "_")
+	variableValue := val
+
+	opt := gitlab.CreateProjectVariableOptions{
+		Key:              &variableKey,
+		Value:            &variableValue,
+		VariableType:     nil,
+		Protected:        nil,
+		Masked:           nil,
+		EnvironmentScope: nil,
+	}
+
+	// Create a variable
+	_, _, err = client.ProjectVariables.CreateVariable(s.projectID, &opt)
+
+	Expect(err).ToNot(HaveOccurred())
+	// Versions aren't supported by Gitlab, but we could add
+	// more parameters to test
+}
+
+func (s *gitlabProvider) DeleteSecret(key string) {
+	// **Open a client
+	client, err := gitlab.NewClient(s.credentials)
+	Expect(err).ToNot(HaveOccurred())
+	// Open a client**
+
+	// Delete the secret
+	_, err = client.ProjectVariables.RemoveVariable(s.projectID, strings.ReplaceAll(key, "-", "_"))
+	Expect(err).ToNot(HaveOccurred())
+}
+
+func (s *gitlabProvider) BeforeEach() {
+	By("creating a gitlab variable")
+	gitlabCreds := &v1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "provider-secret",
+			Namespace: s.framework.Namespace.Name,
+		},
+		// Puts access token into StringData
+
+		StringData: map[string]string{
+			"token":     s.credentials,
+			"projectID": s.projectID,
+		},
+	}
+	err := s.framework.CRClient.Create(context.Background(), gitlabCreds)
+	Expect(err).ToNot(HaveOccurred())
+
+	// Create a secret store - change these values to match YAML
+	By("creating a secret store for credentials")
+	secretStore := &esv1alpha1.SecretStore{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      s.framework.Namespace.Name,
+			Namespace: s.framework.Namespace.Name,
+		},
+		Spec: esv1alpha1.SecretStoreSpec{
+			Provider: &esv1alpha1.SecretStoreProvider{
+				Gitlab: &esv1alpha1.GitlabProvider{
+					ProjectID: s.projectID,
+					Auth: esv1alpha1.GitlabAuth{
+						SecretRef: esv1alpha1.GitlabSecretRef{
+							AccessToken: esmeta.SecretKeySelector{
+								Name: "provider-secret",
+								Key:  "token",
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+
+	err = s.framework.CRClient.Create(context.Background(), secretStore)
+	Expect(err).ToNot(HaveOccurred())
+}

+ 3 - 1
go.mod

@@ -3,6 +3,8 @@ module github.com/external-secrets/external-secrets
 go 1.16
 
 replace (
+	github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1 => ./apis/externalsecrets/v1alpha1
+	github.com/external-secrets/external-secrets/pkg/provider/gitlab => ./pkg/provider/gitlab
 	google.golang.org/grpc => google.golang.org/grpc v1.27.0
 
 	k8s.io/api => k8s.io/api v0.21.2
@@ -51,7 +53,6 @@ require (
 	github.com/googleapis/gax-go v1.0.3
 	github.com/hashicorp/go-hclog v0.14.1 // indirect
 	github.com/hashicorp/go-multierror v1.1.1 // indirect
-	github.com/hashicorp/go-retryablehttp v0.6.7 // indirect
 	github.com/hashicorp/hcl v1.0.1-vault // indirect
 	github.com/hashicorp/vault/api v1.0.5-0.20210224012239-b540be4b7ec4
 	github.com/kr/pretty v0.2.1 // indirect
@@ -64,6 +65,7 @@ require (
 	github.com/spf13/cobra v1.1.3 // indirect
 	github.com/stretchr/testify v1.7.0
 	github.com/tidwall/gjson v1.7.5
+	github.com/xanzy/go-gitlab v0.50.1
 	github.com/yandex-cloud/go-genproto v0.0.0-20210809082946-a97da516c588
 	github.com/yandex-cloud/go-sdk v0.0.0-20210809100642-c13c40a429fa
 	github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a

+ 8 - 2
go.sum

@@ -294,6 +294,8 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
+github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
@@ -355,8 +357,8 @@ github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9
 github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY=
 github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
 github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
-github.com/hashicorp/go-retryablehttp v0.6.7 h1:8/CAEZt/+F7kR7GevNHulKkUjLht3CPmn7egmhieNKo=
-github.com/hashicorp/go-retryablehttp v0.6.7/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
+github.com/hashicorp/go-retryablehttp v0.6.8 h1:92lWxgpa+fF3FozM4B3UZtHZMJX8T5XT+TFdCxsPyWs=
+github.com/hashicorp/go-retryablehttp v0.6.8/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
 github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
 github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
 github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
@@ -644,6 +646,8 @@ github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqri
 github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
 github.com/ulikunitz/xz v0.5.5/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
 github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
+github.com/xanzy/go-gitlab v0.50.1 h1:eH1G0/ZV1j81rhGrtbcePjbM5Ern7mPA4Xjt+yE+2PQ=
+github.com/xanzy/go-gitlab v0.50.1/go.mod h1:Q+hQhV508bDPoBijv7YjK/Lvlb4PhVhJdKqXVQrUoAE=
 github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
 github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
 github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
@@ -790,6 +794,7 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
 golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0=
 golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/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=
@@ -978,6 +983,7 @@ google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M
 google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
 google.golang.org/api v0.30.0 h1:yfrXXP61wVuLb0vBcG6qaOoIoqYEzOQS8jum51jkv2w=
 google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
+google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 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=

+ 2 - 0
hack/api-docs/mkdocs.yml

@@ -48,6 +48,8 @@ nav:
     - HashiCorp Vault: provider-hashicorp-vault.md
     - Yandex:
         - Lockbox: provider-yandex-lockbox.md
+    - Gitlab:
+      - Gitlab Project Variables: provider-gitlab-project-variables.md
   - References:
     - API specification: spec.md
   - Contributing:

+ 39 - 0
pkg/provider/gitlab/fake/fake.go

@@ -0,0 +1,39 @@
+/*
+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.
+*/
+package fake
+
+import (
+	gitlab "github.com/xanzy/go-gitlab"
+)
+
+type GitlabMockClient struct {
+	getVariable func(pid interface{}, key string, options ...gitlab.RequestOptionFunc) (*gitlab.ProjectVariable, *gitlab.Response, error)
+}
+
+func (mc *GitlabMockClient) GetVariable(pid interface{}, key string, options ...gitlab.RequestOptionFunc) (*gitlab.ProjectVariable, *gitlab.Response, error) {
+	return mc.getVariable(pid, key, nil)
+}
+
+func (mc *GitlabMockClient) WithValue(projectIDinput, keyInput string, output *gitlab.ProjectVariable, err error) {
+	if mc != nil {
+		mc.getVariable = func(pid interface{}, key string, options ...gitlab.RequestOptionFunc) (*gitlab.ProjectVariable, *gitlab.Response, error) {
+			// type secretmanagerpb.AccessSecretVersionRequest contains unexported fields
+			// use cmpopts.IgnoreUnexported to ignore all the unexported fields in the cmp.
+			// if !cmp.Equal(paramReq, input, cmpopts.IgnoreUnexported(gitlab.ProjectVariable{})) {
+			// 	return nil, nil, fmt.Errorf("unexpected test argument")
+			// }
+			return output, nil, err
+		}
+	}
+}

+ 203 - 0
pkg/provider/gitlab/gitlab.go

@@ -0,0 +1,203 @@
+/*
+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.
+*/
+package gitlab
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"strings"
+
+	"github.com/tidwall/gjson"
+	gitlab "github.com/xanzy/go-gitlab"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/types"
+	kclient "sigs.k8s.io/controller-runtime/pkg/client"
+
+	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+	"github.com/external-secrets/external-secrets/e2e/framework/log"
+	"github.com/external-secrets/external-secrets/pkg/provider"
+	"github.com/external-secrets/external-secrets/pkg/provider/schema"
+	"github.com/external-secrets/external-secrets/pkg/utils"
+)
+
+// Requires GITLAB_TOKEN and GITLAB_PROJECT_ID to be set in environment variables
+
+const (
+	errGitlabCredSecretName                   = "credentials are empty"
+	errInvalidClusterStoreMissingSAKNamespace = "invalid clusterStore missing SAK namespace"
+	errFetchSAKSecret                         = "couldn't find secret on cluster: %w"
+	errMissingSAK                             = "missing credentials while setting auth"
+	errUninitalizedGitlabProvider             = "provider gitlab is not initialized"
+	errJSONSecretUnmarshal                    = "unable to unmarshal secret: %w"
+)
+
+type Client interface {
+	GetVariable(pid interface{}, key string, options ...gitlab.RequestOptionFunc) (*gitlab.ProjectVariable, *gitlab.Response, error)
+}
+
+// Gitlab Provider struct with reference to a github client and a projectID.
+type Gitlab struct {
+	client    Client
+	projectID interface{}
+}
+
+// Client for interacting with kubernetes cluster...?
+type gClient struct {
+	kube        kclient.Client
+	store       *esv1alpha1.GitlabProvider
+	namespace   string
+	storeKind   string
+	credentials []byte
+}
+
+func init() {
+	schema.Register(&Gitlab{}, &esv1alpha1.SecretStoreProvider{
+		Gitlab: &esv1alpha1.GitlabProvider{},
+	})
+}
+
+// Set gClient credentials to Access Token.
+func (c *gClient) setAuth(ctx context.Context) error {
+	credentialsSecret := &corev1.Secret{}
+	credentialsSecretName := c.store.Auth.SecretRef.AccessToken.Name
+	if credentialsSecretName == "" {
+		return fmt.Errorf(errGitlabCredSecretName)
+	}
+	objectKey := types.NamespacedName{
+		Name:      credentialsSecretName,
+		Namespace: c.namespace,
+	}
+	// only ClusterStore is allowed to set namespace (and then it's required)
+	if c.storeKind == esv1alpha1.ClusterSecretStoreKind {
+		if c.store.Auth.SecretRef.AccessToken.Namespace == nil {
+			return fmt.Errorf(errInvalidClusterStoreMissingSAKNamespace)
+		}
+		objectKey.Namespace = *c.store.Auth.SecretRef.AccessToken.Namespace
+	}
+
+	err := c.kube.Get(ctx, objectKey, credentialsSecret)
+	if err != nil {
+		return fmt.Errorf(errFetchSAKSecret, err)
+	}
+
+	c.credentials = credentialsSecret.Data[c.store.Auth.SecretRef.AccessToken.Key]
+	if (c.credentials == nil) || (len(c.credentials) == 0) {
+		return fmt.Errorf(errMissingSAK)
+	}
+	// I don't know where ProjectID is being set
+	// This line SHOULD set it, but instead just breaks everything :)
+	// c.store.ProjectID = string(credentialsSecret.Data[c.store.ProjectID])
+	return nil
+}
+
+// Function newGitlabProvider returns a reference to a new instance of a 'Gitlab' struct.
+func NewGitlabProvider() *Gitlab {
+	return &Gitlab{}
+}
+
+// Method on Gitlab Provider to set up client with credentials and populate projectID.
+func (g *Gitlab) NewClient(ctx context.Context, store esv1alpha1.GenericStore, kube kclient.Client, namespace string) (provider.SecretsClient, error) {
+	storeSpec := store.GetSpec()
+	if storeSpec == nil || storeSpec.Provider == nil || storeSpec.Provider.Gitlab == nil {
+		return nil, fmt.Errorf("no store type or wrong store type")
+	}
+	storeSpecGitlab := storeSpec.Provider.Gitlab
+
+	cliStore := gClient{
+		kube:      kube,
+		store:     storeSpecGitlab,
+		namespace: namespace,
+		storeKind: store.GetObjectKind().GroupVersionKind().Kind,
+	}
+
+	if err := cliStore.setAuth(ctx); err != nil {
+		return nil, err
+	}
+
+	var err error
+	// Create a new Gitlab client using credentials
+	gitlabClient, err := gitlab.NewClient(string(cliStore.credentials), nil)
+	if err != nil {
+		log.Logf("Failed to create client: %v", err)
+	}
+
+	g.client = gitlabClient.ProjectVariables
+	g.projectID = cliStore.store.ProjectID
+
+	return g, nil
+}
+
+func (g *Gitlab) GetSecret(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error) {
+	if utils.IsNil(g.client) {
+		return nil, fmt.Errorf(errUninitalizedGitlabProvider)
+	}
+	// Need to replace hyphens with underscores to work with Gitlab API
+	ref.Key = strings.ReplaceAll(ref.Key, "-", "_")
+	// Retrieves a gitlab variable in the form
+	// {
+	// 	"key": "TEST_VARIABLE_1",
+	// 	"variable_type": "env_var",
+	// 	"value": "TEST_1",
+	// 	"protected": false,
+	// 	"masked": true
+	data, _, err := g.client.GetVariable(g.projectID, ref.Key, nil) // Optional 'filter' parameter could be added later
+	if err != nil {
+		return nil, err
+	}
+
+	if ref.Property == "" {
+		if data.Value != "" {
+			return []byte(data.Value), nil
+		}
+		return nil, fmt.Errorf("invalid secret received. no secret string for key: %s", ref.Key)
+	}
+
+	var payload string
+	if data.Value != "" {
+		payload = data.Value
+	}
+
+	val := gjson.Get(payload, ref.Property)
+	if !val.Exists() {
+		return nil, fmt.Errorf("key %s does not exist in secret %s", ref.Property, ref.Key)
+	}
+	return []byte(val.String()), nil
+}
+
+func (g *Gitlab) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
+	// Gets a secret as normal, expecting secret value to be a json object
+	data, err := g.GetSecret(ctx, ref)
+	if err != nil {
+		return nil, fmt.Errorf("error getting secret %s: %w", ref.Key, err)
+	}
+
+	// Maps the json data to a string:string map
+	kv := make(map[string]string)
+	err = json.Unmarshal(data, &kv)
+	if err != nil {
+		return nil, fmt.Errorf(errJSONSecretUnmarshal, err)
+	}
+
+	// Converts values in K:V pairs into bytes, while leaving keys as strings
+	secretData := make(map[string][]byte)
+	for k, v := range kv {
+		secretData[k] = []byte(v)
+	}
+	return secretData, nil
+}
+
+func (g *Gitlab) Close(ctx context.Context) error {
+	return nil
+}

+ 178 - 0
pkg/provider/gitlab/gitlab_test.go

@@ -0,0 +1,178 @@
+/*
+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.
+*/
+package gitlab
+
+import (
+	"context"
+	"fmt"
+	"reflect"
+	"strings"
+	"testing"
+
+	gitlab "github.com/xanzy/go-gitlab"
+
+	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+	fakegitlab "github.com/external-secrets/external-secrets/pkg/provider/gitlab/fake"
+)
+
+type secretManagerTestCase struct {
+	mockClient        *fakegitlab.GitlabMockClient
+	apiInputProjectID string
+	apiInputKey       string
+	apiOutput         *gitlab.ProjectVariable
+	ref               *esv1alpha1.ExternalSecretDataRemoteRef
+	projectID         *string
+	apiErr            error
+	expectError       string
+	expectedSecret    string
+	// for testing secretmap
+	expectedData map[string][]byte
+}
+
+func makeValidSecretManagerTestCase() *secretManagerTestCase {
+	smtc := secretManagerTestCase{
+		mockClient:        &fakegitlab.GitlabMockClient{},
+		apiInputProjectID: makeValidAPIInputProjectID(),
+		apiInputKey:       makeValidAPIInputKey(),
+		ref:               makeValidRef(),
+		projectID:         nil,
+		apiOutput:         makeValidAPIOutput(),
+		apiErr:            nil,
+		expectError:       "",
+		expectedSecret:    "",
+		expectedData:      map[string][]byte{},
+	}
+	smtc.mockClient.WithValue(smtc.apiInputProjectID, smtc.apiInputKey, smtc.apiOutput, smtc.apiErr)
+	return &smtc
+}
+
+func makeValidRef() *esv1alpha1.ExternalSecretDataRemoteRef {
+	return &esv1alpha1.ExternalSecretDataRemoteRef{
+		Key:     "test-secret",
+		Version: "default",
+	}
+}
+
+func makeValidAPIInputProjectID() string {
+	return "testID"
+}
+
+func makeValidAPIInputKey() string {
+	return "testKey"
+}
+
+func makeValidAPIOutput() *gitlab.ProjectVariable {
+	return &gitlab.ProjectVariable{
+		Key:   "testKey",
+		Value: "",
+	}
+}
+
+func makeValidSecretManagerTestCaseCustom(tweaks ...func(smtc *secretManagerTestCase)) *secretManagerTestCase {
+	smtc := makeValidSecretManagerTestCase()
+	for _, fn := range tweaks {
+		fn(smtc)
+	}
+	smtc.mockClient.WithValue(smtc.apiInputProjectID, smtc.apiInputKey, smtc.apiOutput, smtc.apiErr)
+	return smtc
+}
+
+// This case can be shared by both GetSecret and GetSecretMap tests.
+// bad case: set apiErr.
+var setAPIErr = func(smtc *secretManagerTestCase) {
+	smtc.apiErr = fmt.Errorf("oh no")
+	smtc.expectError = "oh no"
+}
+
+var setNilMockClient = func(smtc *secretManagerTestCase) {
+	smtc.mockClient = nil
+	smtc.expectError = errUninitalizedGitlabProvider
+}
+
+// test the sm<->gcp interface
+// make sure correct values are passed and errors are handled accordingly.
+func TestGitlabSecretManagerGetSecret(t *testing.T) {
+	secretValue := "changedvalue"
+	// good case: default version is set
+	// key is passed in, output is sent back
+
+	setSecretString := func(smtc *secretManagerTestCase) {
+		smtc.apiOutput = &gitlab.ProjectVariable{
+			Key:   "testkey",
+			Value: "changedvalue",
+		}
+		smtc.expectedSecret = secretValue
+	}
+
+	successCases := []*secretManagerTestCase{
+		makeValidSecretManagerTestCaseCustom(setSecretString),
+		makeValidSecretManagerTestCaseCustom(setAPIErr),
+		makeValidSecretManagerTestCaseCustom(setNilMockClient),
+	}
+
+	sm := Gitlab{}
+	for k, v := range successCases {
+		sm.client = v.mockClient
+		out, err := sm.GetSecret(context.Background(), *v.ref)
+		if !ErrorContains(err, v.expectError) {
+			t.Errorf("[%d] unexpected error: %s, expected: '%s'", k, err.Error(), v.expectError)
+		}
+		if string(out) != v.expectedSecret {
+			t.Errorf("[%d] unexpected secret: expected %s, got %s", k, v.expectedSecret, string(out))
+		}
+	}
+}
+
+func TestGetSecretMap(t *testing.T) {
+	// good case: default version & deserialization
+	setDeserialization := func(smtc *secretManagerTestCase) {
+		smtc.apiOutput.Value = `{"foo":"bar"}`
+		smtc.expectedData["foo"] = []byte("bar")
+	}
+
+	// bad case: invalid json
+	setInvalidJSON := func(smtc *secretManagerTestCase) {
+		smtc.apiOutput.Value = `-----------------`
+		smtc.expectError = "unable to unmarshal secret"
+	}
+
+	successCases := []*secretManagerTestCase{
+		makeValidSecretManagerTestCaseCustom(setDeserialization),
+		makeValidSecretManagerTestCaseCustom(setInvalidJSON),
+		makeValidSecretManagerTestCaseCustom(setNilMockClient),
+		makeValidSecretManagerTestCaseCustom(setAPIErr),
+	}
+
+	sm := Gitlab{}
+	for k, v := range successCases {
+		sm.client = v.mockClient
+		out, err := sm.GetSecretMap(context.Background(), *v.ref)
+		if !ErrorContains(err, v.expectError) {
+			t.Errorf("[%d] unexpected error: %s, expected: '%s'", k, err.Error(), v.expectError)
+		}
+		if err == nil && !reflect.DeepEqual(out, v.expectedData) {
+			t.Errorf("[%d] unexpected secret data: expected %#v, got %#v", k, v.expectedData, out)
+		}
+	}
+}
+
+func ErrorContains(out error, want string) bool {
+	if out == nil {
+		return want == ""
+	}
+	if want == "" {
+		return false
+	}
+	return strings.Contains(out.Error(), want)
+}

+ 1 - 0
pkg/provider/register/register.go

@@ -20,6 +20,7 @@ import (
 	_ "github.com/external-secrets/external-secrets/pkg/provider/aws"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/azure/keyvault"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/gcp/secretmanager"
+	_ "github.com/external-secrets/external-secrets/pkg/provider/gitlab"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/ibm"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/vault"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox"

+ 1 - 1
pkg/provider/schema/schema.go

@@ -92,7 +92,7 @@ func GetProvider(s esv1alpha1.GenericStore) (provider.Provider, error) {
 // or an error if the provider is not configured.
 func getProviderName(storeSpec *esv1alpha1.SecretStoreProvider) (string, error) {
 	storeBytes, err := json.Marshal(storeSpec)
-	if err != nil {
+	if err != nil || storeBytes == nil {
 		return "", fmt.Errorf("failed to marshal store spec: %w", err)
 	}