Просмотр исходного кода

feat(generators): add GitLab deploy token generator (#6493)

* feat(generators): add GitLab deploy token generator

Add a GitlabDeployToken generator that mints project or group deploy tokens
via the GitLab API, analogous to the existing GithubAccessToken generator. The
spec takes a projectID or groupID (mutually exclusive), a name, scopes, and an
optional expiresAt and username, and authenticates with a GitLab access token
referenced from a secret.

Unlike GitHub installation tokens, GitLab deploy tokens are persistent, so the
generator returns the created token id as generator state and revokes the
previous token on regeneration and on deletion through the GeneratorState
cleanup path (idempotent on a 404).

Fixes: external-secrets/external-secrets#3714
Signed-off-by: Alexander Chernov <alexander@chernov.it>

* fix(generators): harden GitLab deploy token generator per review

- reject empty projectID/groupID/name with MinLength=1; the CEL has() rule is
  presence-only, so explicit empty strings slipped past the projectID/groupID
  exclusivity check and were caught only at runtime
- build the Cleanup revoke endpoint from the persisted state instead of the
  live spec, so a later target change cannot orphan the original token
- resolve the auth secret through runtime/esutils/resolvers.SecretKeyRef (the
  shared generator credential contract used by the gcr and grafana generators)
  rather than a raw client.Get
- document projectID/groupID as unescaped paths (the generator URL-escapes
  them) and assert the escaping in tests via EscapedPath

Refs: external-secrets/external-secrets#3714
Signed-off-by: Alexander Chernov <alexander@chernov.it>

* chore(generators): tidy gitlab module go.mod after main merge

Merging main into the branch bumped the apis/runtime sibling modules'
transitive dependencies, but the standalone generators/v1/gitlab go.mod was not
re-tidied. CI lints each module without a go.work workspace, so the stale module
failed package loading (golangci-lint: "no go files to analyze: running go mod
tidy may solve the problem"), breaking check-diff.

Run go mod tidy on the module standalone, which aligns the indirect
golang.org/x/{net,sync,sys,term,text} versions with the rest of the graph.

Signed-off-by: Alexander Chernov <alexander@chernov.it>

* fix(generators): trim trailing slash from GitLab deploy token base URL

deployTokensURL now trims trailing slashes from the base URL so a user-supplied
url like https://gitlab.example.com/ does not produce a double slash before the
API path. Also add unit coverage for expiresAt (RFC3339 UTC normalization,
including a non-Z offset) and for omitted optional fields, per review feedback.

Refs: external-secrets/external-secrets#3714
Signed-off-by: Alexander Chernov <alexander@chernov.it>

---------

Signed-off-by: Alexander Chernov <alexander@chernov.it>
Co-authored-by: Jean-Philippe Evrard <jean-philippe.evrard+rochepub@external.roche.com>
Alexander Chernov 17 часов назад
Родитель
Сommit
78875c18fd
32 измененных файлов с 2211 добавлено и 8 удалено
  1. 1 1
      apis/externalsecrets/v1/externalsecret_types.go
  2. 3 0
      apis/generators/v1alpha1/register.go
  3. 4 1
      apis/generators/v1alpha1/types_cluster.go
  4. 122 0
      apis/generators/v1alpha1/types_gitlab.go
  5. 120 0
      apis/generators/v1alpha1/zz_generated.deepcopy.go
  6. 2 0
      config/crds/bases/external-secrets.io_clusterexternalsecrets.yaml
  7. 1 0
      config/crds/bases/external-secrets.io_clusterpushsecrets.yaml
  8. 2 0
      config/crds/bases/external-secrets.io_externalsecrets.yaml
  9. 1 0
      config/crds/bases/external-secrets.io_pushsecrets.yaml
  10. 108 0
      config/crds/bases/generators.external-secrets.io_clustergenerators.yaml
  11. 153 0
      config/crds/bases/generators.external-secrets.io_gitlabdeploytokens.yaml
  12. 1 0
      config/crds/bases/kustomization.yaml
  13. 3 0
      deploy/charts/external-secrets/templates/rbac.yaml
  14. 257 0
      deploy/crds/bundle.yaml
  15. 45 0
      docs/api/generator/gitlab.md
  16. 395 0
      docs/api/spec.md
  17. 15 0
      docs/snippets/generator-gitlab-example.yaml
  18. 25 0
      docs/snippets/generator-gitlab.yaml
  19. 289 0
      generators/v1/gitlab/gitlab.go
  20. 337 0
      generators/v1/gitlab/gitlab_test.go
  21. 81 0
      generators/v1/gitlab/go.mod
  22. 186 0
      generators/v1/gitlab/go.sum
  23. 2 0
      go.mod
  24. 1 0
      hack/api-docs/mkdocs.yml
  25. 2 0
      pkg/register/generators.go
  26. 11 0
      runtime/esutils/resolvers/generator.go
  27. 2 2
      tests/__snapshot__/clusterexternalsecret-v1.yaml
  28. 15 1
      tests/__snapshot__/clustergenerator-v1alpha1.yaml
  29. 2 2
      tests/__snapshot__/externalsecret-v1.yaml
  30. 17 0
      tests/__snapshot__/gitlabdeploytoken-v1alpha1.yaml
  31. 1 1
      tests/__snapshot__/pushsecret-v1alpha1.yaml
  32. 7 0
      tests/gitlabdeploytoken_test.yaml

+ 1 - 1
apis/externalsecrets/v1/externalsecret_types.go

@@ -646,7 +646,7 @@ type GeneratorRef struct {
 	APIVersion string `json:"apiVersion,omitempty"`
 
 	// Specify the Kind of the generator resource
-	// +kubebuilder:validation:Enum=ACRAccessToken;BeyondtrustWorkloadCredentialsDynamicSecret;ClusterGenerator;CloudsmithAccessToken;ECRAuthorizationToken;Fake;GCRAccessToken;GithubAccessToken;QuayAccessToken;Password;SSHKey;STSSessionToken;UUID;VaultDynamicSecret;Webhook;Grafana;MFA
+	// +kubebuilder:validation:Enum=ACRAccessToken;BeyondtrustWorkloadCredentialsDynamicSecret;ClusterGenerator;CloudsmithAccessToken;ECRAuthorizationToken;Fake;GCRAccessToken;GithubAccessToken;GitlabDeployToken;QuayAccessToken;Password;SSHKey;STSSessionToken;UUID;VaultDynamicSecret;Webhook;Grafana;MFA
 	Kind string `json:"kind"`
 
 	// Specify the name of the generator resource

+ 3 - 0
apis/generators/v1alpha1/register.go

@@ -61,6 +61,8 @@ var (
 	VaultDynamicSecretKind = reflect.TypeFor[VaultDynamicSecret]().Name()
 	// GithubAccessTokenKind is the kind name for GithubAccessToken resource.
 	GithubAccessTokenKind = reflect.TypeFor[GithubAccessToken]().Name()
+	// GitlabDeployTokenKind is the kind name for GitlabDeployToken resource.
+	GitlabDeployTokenKind = reflect.TypeFor[GitlabDeployToken]().Name()
 	// QuayAccessTokenKind is the kind name for QuayAccessToken resource.
 	QuayAccessTokenKind = reflect.TypeFor[QuayAccessToken]().Name()
 	// UUIDKind is the kind name for UUID resource.
@@ -103,6 +105,7 @@ func init() {
 	SchemeBuilder.Register(&Fake{}, &FakeList{})
 	SchemeBuilder.Register(&GCRAccessToken{}, &GCRAccessTokenList{})
 	SchemeBuilder.Register(&GithubAccessToken{}, &GithubAccessTokenList{})
+	SchemeBuilder.Register(&GitlabDeployToken{}, &GitlabDeployTokenList{})
 	SchemeBuilder.Register(&QuayAccessToken{}, &QuayAccessTokenList{})
 	SchemeBuilder.Register(&Password{}, &PasswordList{})
 	SchemeBuilder.Register(&SSHKey{}, &SSHKeyList{})

+ 4 - 1
apis/generators/v1alpha1/types_cluster.go

@@ -30,7 +30,7 @@ type ClusterGeneratorSpec struct {
 }
 
 // GeneratorKind represents a kind of generator.
-// +kubebuilder:validation:Enum=ACRAccessToken;BeyondtrustWorkloadCredentialsDynamicSecret;CloudsmithAccessToken;ECRAuthorizationToken;Fake;GCRAccessToken;GithubAccessToken;QuayAccessToken;Password;SSHKey;STSSessionToken;UUID;VaultDynamicSecret;Webhook;Grafana;MFA
+// +kubebuilder:validation:Enum=ACRAccessToken;BeyondtrustWorkloadCredentialsDynamicSecret;CloudsmithAccessToken;ECRAuthorizationToken;Fake;GCRAccessToken;GithubAccessToken;GitlabDeployToken;QuayAccessToken;Password;SSHKey;STSSessionToken;UUID;VaultDynamicSecret;Webhook;Grafana;MFA
 type GeneratorKind string
 
 const (
@@ -44,6 +44,8 @@ const (
 	GeneratorKindGCRAccessToken GeneratorKind = "GCRAccessToken"
 	// GeneratorKindGithubAccessToken represents a GitHub access token generator.
 	GeneratorKindGithubAccessToken GeneratorKind = "GithubAccessToken"
+	// GeneratorKindGitlabDeployToken represents a GitLab deploy token generator.
+	GeneratorKindGitlabDeployToken GeneratorKind = "GitlabDeployToken"
 	// GeneratorKindQuayAccessToken represents a Quay access token generator.
 	GeneratorKindQuayAccessToken GeneratorKind = "QuayAccessToken"
 	// GeneratorKindPassword represents a password generator.
@@ -79,6 +81,7 @@ type GeneratorSpec struct {
 	FakeSpec                                        *FakeSpec                                        `json:"fakeSpec,omitempty"`
 	GCRAccessTokenSpec                              *GCRAccessTokenSpec                              `json:"gcrAccessTokenSpec,omitempty"`
 	GithubAccessTokenSpec                           *GithubAccessTokenSpec                           `json:"githubAccessTokenSpec,omitempty"`
+	GitlabDeployTokenSpec                           *GitlabDeployTokenSpec                           `json:"gitlabDeployTokenSpec,omitempty"`
 	QuayAccessTokenSpec                             *QuayAccessTokenSpec                             `json:"quayAccessTokenSpec,omitempty"`
 	PasswordSpec                                    *PasswordSpec                                    `json:"passwordSpec,omitempty"`
 	SSHKeySpec                                      *SSHKeySpec                                      `json:"sshKeySpec,omitempty"`

+ 122 - 0
apis/generators/v1alpha1/types_gitlab.go

@@ -0,0 +1,122 @@
+/*
+Copyright © The ESO Authors
+
+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
+
+    https://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 (
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+)
+
+// GitlabDeployTokenScope is a scope that can be granted to a GitLab deploy token.
+// +kubebuilder:validation:Enum=read_repository;read_registry;write_registry;read_package_registry;write_package_registry;read_virtual_registry;write_virtual_registry
+type GitlabDeployTokenScope string
+
+const (
+	// GitlabDeployTokenScopeReadRepository allows read access to the repository.
+	GitlabDeployTokenScopeReadRepository GitlabDeployTokenScope = "read_repository"
+	// GitlabDeployTokenScopeReadRegistry allows read access to the container registry.
+	GitlabDeployTokenScopeReadRegistry GitlabDeployTokenScope = "read_registry"
+	// GitlabDeployTokenScopeWriteRegistry allows write access to the container registry.
+	GitlabDeployTokenScopeWriteRegistry GitlabDeployTokenScope = "write_registry"
+	// GitlabDeployTokenScopeReadPackageRegistry allows read access to the package registry.
+	GitlabDeployTokenScopeReadPackageRegistry GitlabDeployTokenScope = "read_package_registry"
+	// GitlabDeployTokenScopeWritePackageRegistry allows write access to the package registry.
+	GitlabDeployTokenScopeWritePackageRegistry GitlabDeployTokenScope = "write_package_registry"
+	// GitlabDeployTokenScopeReadVirtualRegistry allows read access to the virtual registry (projects only).
+	GitlabDeployTokenScopeReadVirtualRegistry GitlabDeployTokenScope = "read_virtual_registry"
+	// GitlabDeployTokenScopeWriteVirtualRegistry allows write access to the virtual registry (projects only).
+	GitlabDeployTokenScopeWriteVirtualRegistry GitlabDeployTokenScope = "write_virtual_registry"
+)
+
+// GitlabDeployTokenSpec defines the desired state to generate a GitLab deploy token.
+// +kubebuilder:validation:XValidation:rule="has(self.projectID) != has(self.groupID)",message="exactly one of projectID or groupID must be set"
+type GitlabDeployTokenSpec struct {
+	// URL configures the GitLab instance URL. Defaults to https://gitlab.com.
+	// +optional
+	URL string `json:"url,omitempty"`
+
+	// ProjectID is the numeric ID or unescaped path (e.g. group/project) of the
+	// project to create the deploy token in. The generator URL-escapes paths before
+	// calling the GitLab API, so do not pre-encode. Mutually exclusive with groupID.
+	// +optional
+	// +kubebuilder:validation:MinLength=1
+	ProjectID string `json:"projectID,omitempty"`
+
+	// GroupID is the numeric ID or unescaped path (e.g. parent/group) of the group to
+	// create the deploy token in. The generator URL-escapes paths before calling the
+	// GitLab API, so do not pre-encode. Mutually exclusive with projectID.
+	// +optional
+	// +kubebuilder:validation:MinLength=1
+	GroupID string `json:"groupID,omitempty"`
+
+	// Name of the deploy token.
+	// +kubebuilder:validation:MinLength=1
+	Name string `json:"name"`
+
+	// Scopes granted to the deploy token. At least one scope is required.
+	// +kubebuilder:validation:MinItems=1
+	Scopes []GitlabDeployTokenScope `json:"scopes"`
+
+	// ExpiresAt is an optional expiry for the deploy token. If omitted the token does
+	// not expire on the GitLab side and is revoked only when the generator state is
+	// cleaned up (on regeneration or when the consuming ExternalSecret is deleted).
+	// +optional
+	ExpiresAt *metav1.Time `json:"expiresAt,omitempty"`
+
+	// Username is an optional username for the deploy token. GitLab defaults it to
+	// gitlab+deploy-token-{n} when omitted.
+	// +optional
+	Username string `json:"username,omitempty"`
+
+	// Auth configures how ESO authenticates with the GitLab API.
+	Auth GitlabDeployTokenAuth `json:"auth"`
+}
+
+// GitlabDeployTokenAuth defines the authentication configuration for the GitLab API.
+type GitlabDeployTokenAuth struct {
+	// Token references a secret containing a GitLab access token (personal, group, or
+	// project) with the api scope and at least the Maintainer role on the target.
+	Token GitlabDeployTokenSecretRef `json:"token"`
+}
+
+// GitlabDeployTokenSecretRef references a secret containing a GitLab access token.
+type GitlabDeployTokenSecretRef struct {
+	SecretRef esmeta.SecretKeySelector `json:"secretRef"`
+}
+
+// GitlabDeployToken generates a GitLab deploy token.
+// +kubebuilder:object:root=true
+// +kubebuilder:storageversion
+// +kubebuilder:subresource:status
+// +kubebuilder:metadata:labels="external-secrets.io/component=controller"
+// +kubebuilder:resource:scope=Namespaced,categories={external-secrets, external-secrets-generators}
+type GitlabDeployToken struct {
+	metav1.TypeMeta   `json:",inline"`
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+
+	Spec GitlabDeployTokenSpec `json:"spec,omitempty"`
+}
+
+// +kubebuilder:object:root=true
+
+// GitlabDeployTokenList contains a list of GitlabDeployToken resources.
+type GitlabDeployTokenList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata,omitempty"`
+	Items           []GitlabDeployToken `json:"items"`
+}

+ 120 - 0
apis/generators/v1alpha1/zz_generated.deepcopy.go

@@ -862,6 +862,11 @@ func (in *GeneratorSpec) DeepCopyInto(out *GeneratorSpec) {
 		*out = new(GithubAccessTokenSpec)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.GitlabDeployTokenSpec != nil {
+		in, out := &in.GitlabDeployTokenSpec, &out.GitlabDeployTokenSpec
+		*out = new(GitlabDeployTokenSpec)
+		(*in).DeepCopyInto(*out)
+	}
 	if in.QuayAccessTokenSpec != nil {
 		in, out := &in.QuayAccessTokenSpec, &out.QuayAccessTokenSpec
 		*out = new(QuayAccessTokenSpec)
@@ -1163,6 +1168,121 @@ func (in *GithubSecretRef) DeepCopy() *GithubSecretRef {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *GitlabDeployToken) DeepCopyInto(out *GitlabDeployToken) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	in.Spec.DeepCopyInto(&out.Spec)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitlabDeployToken.
+func (in *GitlabDeployToken) DeepCopy() *GitlabDeployToken {
+	if in == nil {
+		return nil
+	}
+	out := new(GitlabDeployToken)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *GitlabDeployToken) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *GitlabDeployTokenAuth) DeepCopyInto(out *GitlabDeployTokenAuth) {
+	*out = *in
+	in.Token.DeepCopyInto(&out.Token)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitlabDeployTokenAuth.
+func (in *GitlabDeployTokenAuth) DeepCopy() *GitlabDeployTokenAuth {
+	if in == nil {
+		return nil
+	}
+	out := new(GitlabDeployTokenAuth)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *GitlabDeployTokenList) DeepCopyInto(out *GitlabDeployTokenList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]GitlabDeployToken, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitlabDeployTokenList.
+func (in *GitlabDeployTokenList) DeepCopy() *GitlabDeployTokenList {
+	if in == nil {
+		return nil
+	}
+	out := new(GitlabDeployTokenList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *GitlabDeployTokenList) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *GitlabDeployTokenSecretRef) DeepCopyInto(out *GitlabDeployTokenSecretRef) {
+	*out = *in
+	in.SecretRef.DeepCopyInto(&out.SecretRef)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitlabDeployTokenSecretRef.
+func (in *GitlabDeployTokenSecretRef) DeepCopy() *GitlabDeployTokenSecretRef {
+	if in == nil {
+		return nil
+	}
+	out := new(GitlabDeployTokenSecretRef)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *GitlabDeployTokenSpec) DeepCopyInto(out *GitlabDeployTokenSpec) {
+	*out = *in
+	if in.Scopes != nil {
+		in, out := &in.Scopes, &out.Scopes
+		*out = make([]GitlabDeployTokenScope, len(*in))
+		copy(*out, *in)
+	}
+	if in.ExpiresAt != nil {
+		in, out := &in.ExpiresAt, &out.ExpiresAt
+		*out = (*in).DeepCopy()
+	}
+	in.Auth.DeepCopyInto(&out.Auth)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitlabDeployTokenSpec.
+func (in *GitlabDeployTokenSpec) DeepCopy() *GitlabDeployTokenSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(GitlabDeployTokenSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *Grafana) DeepCopyInto(out *Grafana) {
 	*out = *in

+ 2 - 0
config/crds/bases/external-secrets.io_clusterexternalsecrets.yaml

@@ -174,6 +174,7 @@ spec:
                                   - Fake
                                   - GCRAccessToken
                                   - GithubAccessToken
+                                  - GitlabDeployToken
                                   - QuayAccessToken
                                   - Password
                                   - SSHKey
@@ -439,6 +440,7 @@ spec:
                                   - Fake
                                   - GCRAccessToken
                                   - GithubAccessToken
+                                  - GitlabDeployToken
                                   - QuayAccessToken
                                   - Password
                                   - SSHKey

+ 1 - 0
config/crds/bases/external-secrets.io_clusterpushsecrets.yaml

@@ -435,6 +435,7 @@ spec:
                             - Fake
                             - GCRAccessToken
                             - GithubAccessToken
+                            - GitlabDeployToken
                             - QuayAccessToken
                             - Password
                             - SSHKey

+ 2 - 0
config/crds/bases/external-secrets.io_externalsecrets.yaml

@@ -159,6 +159,7 @@ spec:
                               - Fake
                               - GCRAccessToken
                               - GithubAccessToken
+                              - GitlabDeployToken
                               - QuayAccessToken
                               - Password
                               - SSHKey
@@ -422,6 +423,7 @@ spec:
                               - Fake
                               - GCRAccessToken
                               - GithubAccessToken
+                              - GitlabDeployToken
                               - QuayAccessToken
                               - Password
                               - SSHKey

+ 1 - 0
config/crds/bases/external-secrets.io_pushsecrets.yaml

@@ -358,6 +358,7 @@ spec:
                         - Fake
                         - GCRAccessToken
                         - GithubAccessToken
+                        - GitlabDeployToken
                         - QuayAccessToken
                         - Password
                         - SSHKey

+ 108 - 0
config/crds/bases/generators.external-secrets.io_clustergenerators.yaml

@@ -893,6 +893,113 @@ spec:
                     - auth
                     - installID
                     type: object
+                  gitlabDeployTokenSpec:
+                    description: GitlabDeployTokenSpec defines the desired state to
+                      generate a GitLab deploy token.
+                    properties:
+                      auth:
+                        description: Auth configures how ESO authenticates with the
+                          GitLab API.
+                        properties:
+                          token:
+                            description: |-
+                              Token references a secret containing a GitLab access token (personal, group, or
+                              project) with the api scope and at least the Maintainer role on the target.
+                            properties:
+                              secretRef:
+                                description: |-
+                                  SecretKeySelector is a reference to a specific 'key' within a Secret resource.
+                                  In some instances, `key` is a required field.
+                                properties:
+                                  key:
+                                    description: |-
+                                      A key in the referenced Secret.
+                                      Some instances of this field may be defaulted, in others it may be required.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[-._a-zA-Z0-9]+$
+                                    type: string
+                                  name:
+                                    description: The name of the Secret resource being
+                                      referred to.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                    type: string
+                                  namespace:
+                                    description: |-
+                                      The namespace of the Secret resource being referred to.
+                                      Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                    maxLength: 63
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                    type: string
+                                type: object
+                            required:
+                            - secretRef
+                            type: object
+                        required:
+                        - token
+                        type: object
+                      expiresAt:
+                        description: |-
+                          ExpiresAt is an optional expiry for the deploy token. If omitted the token does
+                          not expire on the GitLab side and is revoked only when the generator state is
+                          cleaned up (on regeneration or when the consuming ExternalSecret is deleted).
+                        format: date-time
+                        type: string
+                      groupID:
+                        description: |-
+                          GroupID is the numeric ID or unescaped path (e.g. parent/group) of the group to
+                          create the deploy token in. The generator URL-escapes paths before calling the
+                          GitLab API, so do not pre-encode. Mutually exclusive with projectID.
+                        minLength: 1
+                        type: string
+                      name:
+                        description: Name of the deploy token.
+                        minLength: 1
+                        type: string
+                      projectID:
+                        description: |-
+                          ProjectID is the numeric ID or unescaped path (e.g. group/project) of the
+                          project to create the deploy token in. The generator URL-escapes paths before
+                          calling the GitLab API, so do not pre-encode. Mutually exclusive with groupID.
+                        minLength: 1
+                        type: string
+                      scopes:
+                        description: Scopes granted to the deploy token. At least
+                          one scope is required.
+                        items:
+                          description: GitlabDeployTokenScope is a scope that can
+                            be granted to a GitLab deploy token.
+                          enum:
+                          - read_repository
+                          - read_registry
+                          - write_registry
+                          - read_package_registry
+                          - write_package_registry
+                          - read_virtual_registry
+                          - write_virtual_registry
+                          type: string
+                        minItems: 1
+                        type: array
+                      url:
+                        description: URL configures the GitLab instance URL. Defaults
+                          to https://gitlab.com.
+                        type: string
+                      username:
+                        description: |-
+                          Username is an optional username for the deploy token. GitLab defaults it to
+                          gitlab+deploy-token-{n} when omitted.
+                        type: string
+                    required:
+                    - auth
+                    - name
+                    - scopes
+                    type: object
+                    x-kubernetes-validations:
+                    - message: exactly one of projectID or groupID must be set
+                      rule: has(self.projectID) != has(self.groupID)
                   grafanaSpec:
                     description: GrafanaSpec controls the behavior of the grafana
                       generator.
@@ -2553,6 +2660,7 @@ spec:
                 - Fake
                 - GCRAccessToken
                 - GithubAccessToken
+                - GitlabDeployToken
                 - QuayAccessToken
                 - Password
                 - SSHKey

+ 153 - 0
config/crds/bases/generators.external-secrets.io_gitlabdeploytokens.yaml

@@ -0,0 +1,153 @@
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.19.0
+  labels:
+    external-secrets.io/component: controller
+  name: gitlabdeploytokens.generators.external-secrets.io
+spec:
+  group: generators.external-secrets.io
+  names:
+    categories:
+    - external-secrets
+    - external-secrets-generators
+    kind: GitlabDeployToken
+    listKind: GitlabDeployTokenList
+    plural: gitlabdeploytokens
+    singular: gitlabdeploytoken
+  scope: Namespaced
+  versions:
+  - name: v1alpha1
+    schema:
+      openAPIV3Schema:
+        description: GitlabDeployToken generates a GitLab deploy token.
+        properties:
+          apiVersion:
+            description: |-
+              APIVersion defines the versioned schema of this representation of an object.
+              Servers should convert recognized schemas to the latest internal value, and
+              may reject unrecognized values.
+              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
+            type: string
+          kind:
+            description: |-
+              Kind is a string value representing the REST resource this object represents.
+              Servers may infer this from the endpoint the client submits requests to.
+              Cannot be updated.
+              In CamelCase.
+              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: GitlabDeployTokenSpec defines the desired state to generate
+              a GitLab deploy token.
+            properties:
+              auth:
+                description: Auth configures how ESO authenticates with the GitLab
+                  API.
+                properties:
+                  token:
+                    description: |-
+                      Token references a secret containing a GitLab access token (personal, group, or
+                      project) with the api scope and at least the Maintainer role on the target.
+                    properties:
+                      secretRef:
+                        description: |-
+                          SecretKeySelector is a reference to a specific 'key' within a Secret resource.
+                          In some instances, `key` is a required field.
+                        properties:
+                          key:
+                            description: |-
+                              A key in the referenced Secret.
+                              Some instances of this field may be defaulted, in others it may be required.
+                            maxLength: 253
+                            minLength: 1
+                            pattern: ^[-._a-zA-Z0-9]+$
+                            type: string
+                          name:
+                            description: The name of the Secret resource being referred
+                              to.
+                            maxLength: 253
+                            minLength: 1
+                            pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                            type: string
+                          namespace:
+                            description: |-
+                              The namespace of the Secret resource being referred to.
+                              Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                            maxLength: 63
+                            minLength: 1
+                            pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                            type: string
+                        type: object
+                    required:
+                    - secretRef
+                    type: object
+                required:
+                - token
+                type: object
+              expiresAt:
+                description: |-
+                  ExpiresAt is an optional expiry for the deploy token. If omitted the token does
+                  not expire on the GitLab side and is revoked only when the generator state is
+                  cleaned up (on regeneration or when the consuming ExternalSecret is deleted).
+                format: date-time
+                type: string
+              groupID:
+                description: |-
+                  GroupID is the numeric ID or unescaped path (e.g. parent/group) of the group to
+                  create the deploy token in. The generator URL-escapes paths before calling the
+                  GitLab API, so do not pre-encode. Mutually exclusive with projectID.
+                minLength: 1
+                type: string
+              name:
+                description: Name of the deploy token.
+                minLength: 1
+                type: string
+              projectID:
+                description: |-
+                  ProjectID is the numeric ID or unescaped path (e.g. group/project) of the
+                  project to create the deploy token in. The generator URL-escapes paths before
+                  calling the GitLab API, so do not pre-encode. Mutually exclusive with groupID.
+                minLength: 1
+                type: string
+              scopes:
+                description: Scopes granted to the deploy token. At least one scope
+                  is required.
+                items:
+                  description: GitlabDeployTokenScope is a scope that can be granted
+                    to a GitLab deploy token.
+                  enum:
+                  - read_repository
+                  - read_registry
+                  - write_registry
+                  - read_package_registry
+                  - write_package_registry
+                  - read_virtual_registry
+                  - write_virtual_registry
+                  type: string
+                minItems: 1
+                type: array
+              url:
+                description: URL configures the GitLab instance URL. Defaults to https://gitlab.com.
+                type: string
+              username:
+                description: |-
+                  Username is an optional username for the deploy token. GitLab defaults it to
+                  gitlab+deploy-token-{n} when omitted.
+                type: string
+            required:
+            - auth
+            - name
+            - scopes
+            type: object
+            x-kubernetes-validations:
+            - message: exactly one of projectID or groupID must be set
+              rule: has(self.projectID) != has(self.groupID)
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}

+ 1 - 0
config/crds/bases/kustomization.yaml

@@ -17,6 +17,7 @@ resources:
   - generators.external-secrets.io_gcraccesstokens.yaml
   - generators.external-secrets.io_generatorstates.yaml
   - generators.external-secrets.io_githubaccesstokens.yaml
+  - generators.external-secrets.io_gitlabdeploytokens.yaml
   - generators.external-secrets.io_grafanas.yaml
   - generators.external-secrets.io_mfas.yaml
   - generators.external-secrets.io_passwords.yaml

+ 3 - 0
deploy/charts/external-secrets/templates/rbac.yaml

@@ -104,6 +104,7 @@ rules:
     - "fakes"
     - "gcraccesstokens"
     - "githubaccesstokens"
+    - "gitlabdeploytokens"
     - "quayaccesstokens"
     - "passwords"
     - "sshkeys"
@@ -283,6 +284,7 @@ rules:
     - "fakes"
     - "gcraccesstokens"
     - "githubaccesstokens"
+    - "gitlabdeploytokens"
     - "quayaccesstokens"
     - "passwords"
     - "sshkeys"
@@ -347,6 +349,7 @@ rules:
     - "fakes"
     - "gcraccesstokens"
     - "githubaccesstokens"
+    - "gitlabdeploytokens"
     - "quayaccesstokens"
     - "passwords"
     - "sshkeys"

+ 257 - 0
deploy/crds/bundle.yaml

@@ -163,6 +163,7 @@ spec:
                                       - Fake
                                       - GCRAccessToken
                                       - GithubAccessToken
+                                      - GitlabDeployToken
                                       - QuayAccessToken
                                       - Password
                                       - SSHKey
@@ -411,6 +412,7 @@ spec:
                                       - Fake
                                       - GCRAccessToken
                                       - GithubAccessToken
+                                      - GitlabDeployToken
                                       - QuayAccessToken
                                       - Password
                                       - SSHKey
@@ -2038,6 +2040,7 @@ spec:
                                 - Fake
                                 - GCRAccessToken
                                 - GithubAccessToken
+                                - GitlabDeployToken
                                 - QuayAccessToken
                                 - Password
                                 - SSHKey
@@ -13158,6 +13161,7 @@ spec:
                                   - Fake
                                   - GCRAccessToken
                                   - GithubAccessToken
+                                  - GitlabDeployToken
                                   - QuayAccessToken
                                   - Password
                                   - SSHKey
@@ -13406,6 +13410,7 @@ spec:
                                   - Fake
                                   - GCRAccessToken
                                   - GithubAccessToken
+                                  - GitlabDeployToken
                                   - QuayAccessToken
                                   - Password
                                   - SSHKey
@@ -14746,6 +14751,7 @@ spec:
                             - Fake
                             - GCRAccessToken
                             - GithubAccessToken
+                            - GitlabDeployToken
                             - QuayAccessToken
                             - Password
                             - SSHKey
@@ -27100,6 +27106,107 @@ spec:
                         - auth
                         - installID
                       type: object
+                    gitlabDeployTokenSpec:
+                      description: GitlabDeployTokenSpec defines the desired state to generate a GitLab deploy token.
+                      properties:
+                        auth:
+                          description: Auth configures how ESO authenticates with the GitLab API.
+                          properties:
+                            token:
+                              description: |-
+                                Token references a secret containing a GitLab access token (personal, group, or
+                                project) with the api scope and at least the Maintainer role on the target.
+                              properties:
+                                secretRef:
+                                  description: |-
+                                    SecretKeySelector is a reference to a specific 'key' within a Secret resource.
+                                    In some instances, `key` is a required field.
+                                  properties:
+                                    key:
+                                      description: |-
+                                        A key in the referenced Secret.
+                                        Some instances of this field may be defaulted, in others it may be required.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[-._a-zA-Z0-9]+$
+                                      type: string
+                                    name:
+                                      description: The name of the Secret resource being referred to.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                      type: string
+                                    namespace:
+                                      description: |-
+                                        The namespace of the Secret resource being referred to.
+                                        Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                      maxLength: 63
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                      type: string
+                                  type: object
+                              required:
+                                - secretRef
+                              type: object
+                          required:
+                            - token
+                          type: object
+                        expiresAt:
+                          description: |-
+                            ExpiresAt is an optional expiry for the deploy token. If omitted the token does
+                            not expire on the GitLab side and is revoked only when the generator state is
+                            cleaned up (on regeneration or when the consuming ExternalSecret is deleted).
+                          format: date-time
+                          type: string
+                        groupID:
+                          description: |-
+                            GroupID is the numeric ID or unescaped path (e.g. parent/group) of the group to
+                            create the deploy token in. The generator URL-escapes paths before calling the
+                            GitLab API, so do not pre-encode. Mutually exclusive with projectID.
+                          minLength: 1
+                          type: string
+                        name:
+                          description: Name of the deploy token.
+                          minLength: 1
+                          type: string
+                        projectID:
+                          description: |-
+                            ProjectID is the numeric ID or unescaped path (e.g. group/project) of the
+                            project to create the deploy token in. The generator URL-escapes paths before
+                            calling the GitLab API, so do not pre-encode. Mutually exclusive with groupID.
+                          minLength: 1
+                          type: string
+                        scopes:
+                          description: Scopes granted to the deploy token. At least one scope is required.
+                          items:
+                            description: GitlabDeployTokenScope is a scope that can be granted to a GitLab deploy token.
+                            enum:
+                              - read_repository
+                              - read_registry
+                              - write_registry
+                              - read_package_registry
+                              - write_package_registry
+                              - read_virtual_registry
+                              - write_virtual_registry
+                            type: string
+                          minItems: 1
+                          type: array
+                        url:
+                          description: URL configures the GitLab instance URL. Defaults to https://gitlab.com.
+                          type: string
+                        username:
+                          description: |-
+                            Username is an optional username for the deploy token. GitLab defaults it to
+                            gitlab+deploy-token-{n} when omitted.
+                          type: string
+                      required:
+                        - auth
+                        - name
+                        - scopes
+                      type: object
+                      x-kubernetes-validations:
+                        - message: exactly one of projectID or groupID must be set
+                          rule: has(self.projectID) != has(self.groupID)
                     grafanaSpec:
                       description: GrafanaSpec controls the behavior of the grafana generator.
                       properties:
@@ -28664,6 +28771,7 @@ spec:
                     - Fake
                     - GCRAccessToken
                     - GithubAccessToken
+                    - GitlabDeployToken
                     - QuayAccessToken
                     - Password
                     - SSHKey
@@ -29431,6 +29539,155 @@ spec:
 ---
 apiVersion: apiextensions.k8s.io/v1
 kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.19.0
+  labels:
+    external-secrets.io/component: controller
+  name: gitlabdeploytokens.generators.external-secrets.io
+spec:
+  group: generators.external-secrets.io
+  names:
+    categories:
+      - external-secrets
+      - external-secrets-generators
+    kind: GitlabDeployToken
+    listKind: GitlabDeployTokenList
+    plural: gitlabdeploytokens
+    singular: gitlabdeploytoken
+  scope: Namespaced
+  versions:
+    - name: v1alpha1
+      schema:
+        openAPIV3Schema:
+          description: GitlabDeployToken generates a GitLab deploy token.
+          properties:
+            apiVersion:
+              description: |-
+                APIVersion defines the versioned schema of this representation of an object.
+                Servers should convert recognized schemas to the latest internal value, and
+                may reject unrecognized values.
+                More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
+              type: string
+            kind:
+              description: |-
+                Kind is a string value representing the REST resource this object represents.
+                Servers may infer this from the endpoint the client submits requests to.
+                Cannot be updated.
+                In CamelCase.
+                More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
+              type: string
+            metadata:
+              type: object
+            spec:
+              description: GitlabDeployTokenSpec defines the desired state to generate a GitLab deploy token.
+              properties:
+                auth:
+                  description: Auth configures how ESO authenticates with the GitLab API.
+                  properties:
+                    token:
+                      description: |-
+                        Token references a secret containing a GitLab access token (personal, group, or
+                        project) with the api scope and at least the Maintainer role on the target.
+                      properties:
+                        secretRef:
+                          description: |-
+                            SecretKeySelector is a reference to a specific 'key' within a Secret resource.
+                            In some instances, `key` is a required field.
+                          properties:
+                            key:
+                              description: |-
+                                A key in the referenced Secret.
+                                Some instances of this field may be defaulted, in others it may be required.
+                              maxLength: 253
+                              minLength: 1
+                              pattern: ^[-._a-zA-Z0-9]+$
+                              type: string
+                            name:
+                              description: The name of the Secret resource being referred to.
+                              maxLength: 253
+                              minLength: 1
+                              pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                              type: string
+                            namespace:
+                              description: |-
+                                The namespace of the Secret resource being referred to.
+                                Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                              maxLength: 63
+                              minLength: 1
+                              pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                              type: string
+                          type: object
+                      required:
+                        - secretRef
+                      type: object
+                  required:
+                    - token
+                  type: object
+                expiresAt:
+                  description: |-
+                    ExpiresAt is an optional expiry for the deploy token. If omitted the token does
+                    not expire on the GitLab side and is revoked only when the generator state is
+                    cleaned up (on regeneration or when the consuming ExternalSecret is deleted).
+                  format: date-time
+                  type: string
+                groupID:
+                  description: |-
+                    GroupID is the numeric ID or unescaped path (e.g. parent/group) of the group to
+                    create the deploy token in. The generator URL-escapes paths before calling the
+                    GitLab API, so do not pre-encode. Mutually exclusive with projectID.
+                  minLength: 1
+                  type: string
+                name:
+                  description: Name of the deploy token.
+                  minLength: 1
+                  type: string
+                projectID:
+                  description: |-
+                    ProjectID is the numeric ID or unescaped path (e.g. group/project) of the
+                    project to create the deploy token in. The generator URL-escapes paths before
+                    calling the GitLab API, so do not pre-encode. Mutually exclusive with groupID.
+                  minLength: 1
+                  type: string
+                scopes:
+                  description: Scopes granted to the deploy token. At least one scope is required.
+                  items:
+                    description: GitlabDeployTokenScope is a scope that can be granted to a GitLab deploy token.
+                    enum:
+                      - read_repository
+                      - read_registry
+                      - write_registry
+                      - read_package_registry
+                      - write_package_registry
+                      - read_virtual_registry
+                      - write_virtual_registry
+                    type: string
+                  minItems: 1
+                  type: array
+                url:
+                  description: URL configures the GitLab instance URL. Defaults to https://gitlab.com.
+                  type: string
+                username:
+                  description: |-
+                    Username is an optional username for the deploy token. GitLab defaults it to
+                    gitlab+deploy-token-{n} when omitted.
+                  type: string
+              required:
+                - auth
+                - name
+                - scopes
+              type: object
+              x-kubernetes-validations:
+                - message: exactly one of projectID or groupID must be set
+                  rule: has(self.projectID) != has(self.groupID)
+          type: object
+      served: true
+      storage: true
+      subresources:
+        status: {}
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
 metadata:
   annotations:
     controller-gen.kubebuilder.io/version: v0.19.0

+ 45 - 0
docs/api/generator/gitlab.md

@@ -0,0 +1,45 @@
+## GitLab Deploy Token Generator
+
+The GitLab Deploy Token generator creates [GitLab deploy tokens](https://docs.gitlab.com/user/project/deploy_tokens/) for a project or a group. A deploy token gives read or write access to a project's repository, container registry, and package registry, which makes it well suited for pulling images or packages from automation.
+
+The generated secret contains two keys:
+
+- `username`: the deploy token username (the value of `spec.username`, or the `gitlab+deploy-token-{n}` value GitLab assigns when `username` is omitted).
+- `token`: the deploy token value.
+
+### Authentication
+
+The generator authenticates against the GitLab API with an access token (personal, group, or project) that has the `api` scope and at least the **Maintainer** role on the target project (or **Owner** on the target group). Store that token in a Kubernetes secret and reference it from `spec.auth.token.secretRef`.
+
+```bash
+kubectl create secret generic gitlab-api-token --from-literal=token=glpat-xxxxxxxxxxxx
+```
+
+### Target
+
+Set exactly one of `spec.projectID` or `spec.groupID`. Both accept either a numeric ID or an unescaped path such as `group/project`, the generator URL-escapes paths before calling the API, so do not pre-encode them. Setting both, neither, or an empty string is rejected by the CRD.
+
+### Scopes
+
+`spec.scopes` requires at least one of: `read_repository`, `read_registry`, `write_registry`, `read_package_registry`, `write_package_registry`. Projects additionally support `read_virtual_registry` and `write_virtual_registry`.
+
+### Token lifecycle
+
+GitLab deploy tokens are persistent: unlike short-lived tokens they are not garbage-collected by GitLab on their own. This generator therefore records the created token ID in its generator state and **revokes the previous token** whenever the value is regenerated (on refresh) and when the consuming `ExternalSecret` is deleted. Set `spec.expiresAt` if you also want GitLab to expire the token server-side as a backstop.
+
+### Example Manifest
+
+```yaml
+{% include 'generator-gitlab.yaml' %}
+```
+
+Example `ExternalSecret` that references the generator:
+
+```yaml
+{% include 'generator-gitlab-example.yaml' %}
+```
+
+### Notes
+
+- The access token used for authentication is never written to the target secret; only the generated deploy token is.
+- Each refresh creates a new deploy token and revokes the prior one, so the token value rotates on every `refreshInterval`.

+ 395 - 0
docs/api/spec.md

@@ -27418,6 +27418,9 @@ string
 </tr><tr><td><p>&#34;GithubAccessToken&#34;</p></td>
 <td><p>GeneratorKindGithubAccessToken represents a GitHub access token generator.</p>
 </td>
+</tr><tr><td><p>&#34;GitlabDeployToken&#34;</p></td>
+<td><p>GeneratorKindGitlabDeployToken represents a GitLab deploy token generator.</p>
+</td>
 </tr><tr><td><p>&#34;Grafana&#34;</p></td>
 <td><p>GeneratorKindGrafana represents a Grafana token generator.</p>
 </td>
@@ -27555,6 +27558,18 @@ GithubAccessTokenSpec
 </tr>
 <tr>
 <td>
+<code>gitlabDeployTokenSpec</code></br>
+<em>
+<a href="#generators.external-secrets.io/v1alpha1.GitlabDeployTokenSpec">
+GitlabDeployTokenSpec
+</a>
+</em>
+</td>
+<td>
+</td>
+</tr>
+<tr>
+<td>
 <code>quayAccessTokenSpec</code></br>
 <em>
 <a href="#generators.external-secrets.io/v1alpha1.QuayAccessTokenSpec">
@@ -28216,6 +28231,386 @@ External Secrets meta/v1.SecretKeySelector
 </tr>
 </tbody>
 </table>
+<h3 id="generators.external-secrets.io/v1alpha1.GitlabDeployToken">GitlabDeployToken
+</h3>
+<p>
+<p>GitlabDeployToken generates a GitLab deploy token.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>metadata</code></br>
+<em>
+<a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.25/#objectmeta-v1-meta">
+Kubernetes meta/v1.ObjectMeta
+</a>
+</em>
+</td>
+<td>
+Refer to the Kubernetes API documentation for the fields of the
+<code>metadata</code> field.
+</td>
+</tr>
+<tr>
+<td>
+<code>spec</code></br>
+<em>
+<a href="#generators.external-secrets.io/v1alpha1.GitlabDeployTokenSpec">
+GitlabDeployTokenSpec
+</a>
+</em>
+</td>
+<td>
+<br/>
+<br/>
+<table>
+<tr>
+<td>
+<code>url</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>URL configures the GitLab instance URL. Defaults to <a href="https://gitlab.com">https://gitlab.com</a>.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>projectID</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>ProjectID is the numeric ID or unescaped path (e.g. group/project) of the
+project to create the deploy token in. The generator URL-escapes paths before
+calling the GitLab API, so do not pre-encode. Mutually exclusive with groupID.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>groupID</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>GroupID is the numeric ID or unescaped path (e.g. parent/group) of the group to
+create the deploy token in. The generator URL-escapes paths before calling the
+GitLab API, so do not pre-encode. Mutually exclusive with projectID.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>name</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<p>Name of the deploy token.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>scopes</code></br>
+<em>
+<a href="#generators.external-secrets.io/v1alpha1.GitlabDeployTokenScope">
+[]GitlabDeployTokenScope
+</a>
+</em>
+</td>
+<td>
+<p>Scopes granted to the deploy token. At least one scope is required.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>expiresAt</code></br>
+<em>
+<a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.25/#time-v1-meta">
+Kubernetes meta/v1.Time
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>ExpiresAt is an optional expiry for the deploy token. If omitted the token does
+not expire on the GitLab side and is revoked only when the generator state is
+cleaned up (on regeneration or when the consuming ExternalSecret is deleted).</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>username</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Username is an optional username for the deploy token. GitLab defaults it to
+gitlab+deploy-token-{n} when omitted.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>auth</code></br>
+<em>
+<a href="#generators.external-secrets.io/v1alpha1.GitlabDeployTokenAuth">
+GitlabDeployTokenAuth
+</a>
+</em>
+</td>
+<td>
+<p>Auth configures how ESO authenticates with the GitLab API.</p>
+</td>
+</tr>
+</table>
+</td>
+</tr>
+</tbody>
+</table>
+<h3 id="generators.external-secrets.io/v1alpha1.GitlabDeployTokenAuth">GitlabDeployTokenAuth
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#generators.external-secrets.io/v1alpha1.GitlabDeployTokenSpec">GitlabDeployTokenSpec</a>)
+</p>
+<p>
+<p>GitlabDeployTokenAuth defines the authentication configuration for the GitLab API.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>token</code></br>
+<em>
+<a href="#generators.external-secrets.io/v1alpha1.GitlabDeployTokenSecretRef">
+GitlabDeployTokenSecretRef
+</a>
+</em>
+</td>
+<td>
+<p>Token references a secret containing a GitLab access token (personal, group, or
+project) with the api scope and at least the Maintainer role on the target.</p>
+</td>
+</tr>
+</tbody>
+</table>
+<h3 id="generators.external-secrets.io/v1alpha1.GitlabDeployTokenScope">GitlabDeployTokenScope
+(<code>string</code> alias)</p></h3>
+<p>
+(<em>Appears on:</em>
+<a href="#generators.external-secrets.io/v1alpha1.GitlabDeployTokenSpec">GitlabDeployTokenSpec</a>)
+</p>
+<p>
+<p>GitlabDeployTokenScope is a scope that can be granted to a GitLab deploy token.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Value</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody><tr><td><p>&#34;read_package_registry&#34;</p></td>
+<td><p>GitlabDeployTokenScopeReadPackageRegistry allows read access to the package registry.</p>
+</td>
+</tr><tr><td><p>&#34;read_registry&#34;</p></td>
+<td><p>GitlabDeployTokenScopeReadRegistry allows read access to the container registry.</p>
+</td>
+</tr><tr><td><p>&#34;read_repository&#34;</p></td>
+<td><p>GitlabDeployTokenScopeReadRepository allows read access to the repository.</p>
+</td>
+</tr><tr><td><p>&#34;read_virtual_registry&#34;</p></td>
+<td><p>GitlabDeployTokenScopeReadVirtualRegistry allows read access to the virtual registry (projects only).</p>
+</td>
+</tr><tr><td><p>&#34;write_package_registry&#34;</p></td>
+<td><p>GitlabDeployTokenScopeWritePackageRegistry allows write access to the package registry.</p>
+</td>
+</tr><tr><td><p>&#34;write_registry&#34;</p></td>
+<td><p>GitlabDeployTokenScopeWriteRegistry allows write access to the container registry.</p>
+</td>
+</tr><tr><td><p>&#34;write_virtual_registry&#34;</p></td>
+<td><p>GitlabDeployTokenScopeWriteVirtualRegistry allows write access to the virtual registry (projects only).</p>
+</td>
+</tr></tbody>
+</table>
+<h3 id="generators.external-secrets.io/v1alpha1.GitlabDeployTokenSecretRef">GitlabDeployTokenSecretRef
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#generators.external-secrets.io/v1alpha1.GitlabDeployTokenAuth">GitlabDeployTokenAuth</a>)
+</p>
+<p>
+<p>GitlabDeployTokenSecretRef references a secret containing a GitLab access token.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>secretRef</code></br>
+<em>
+<a href="https://pkg.go.dev/github.com/external-secrets/external-secrets/apis/meta/v1#SecretKeySelector">
+External Secrets meta/v1.SecretKeySelector
+</a>
+</em>
+</td>
+<td>
+</td>
+</tr>
+</tbody>
+</table>
+<h3 id="generators.external-secrets.io/v1alpha1.GitlabDeployTokenSpec">GitlabDeployTokenSpec
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#generators.external-secrets.io/v1alpha1.GeneratorSpec">GeneratorSpec</a>, 
+<a href="#generators.external-secrets.io/v1alpha1.GitlabDeployToken">GitlabDeployToken</a>)
+</p>
+<p>
+<p>GitlabDeployTokenSpec defines the desired state to generate a GitLab deploy token.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>url</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>URL configures the GitLab instance URL. Defaults to <a href="https://gitlab.com">https://gitlab.com</a>.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>projectID</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>ProjectID is the numeric ID or unescaped path (e.g. group/project) of the
+project to create the deploy token in. The generator URL-escapes paths before
+calling the GitLab API, so do not pre-encode. Mutually exclusive with groupID.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>groupID</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>GroupID is the numeric ID or unescaped path (e.g. parent/group) of the group to
+create the deploy token in. The generator URL-escapes paths before calling the
+GitLab API, so do not pre-encode. Mutually exclusive with projectID.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>name</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<p>Name of the deploy token.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>scopes</code></br>
+<em>
+<a href="#generators.external-secrets.io/v1alpha1.GitlabDeployTokenScope">
+[]GitlabDeployTokenScope
+</a>
+</em>
+</td>
+<td>
+<p>Scopes granted to the deploy token. At least one scope is required.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>expiresAt</code></br>
+<em>
+<a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.25/#time-v1-meta">
+Kubernetes meta/v1.Time
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>ExpiresAt is an optional expiry for the deploy token. If omitted the token does
+not expire on the GitLab side and is revoked only when the generator state is
+cleaned up (on regeneration or when the consuming ExternalSecret is deleted).</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>username</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Username is an optional username for the deploy token. GitLab defaults it to
+gitlab+deploy-token-{n} when omitted.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>auth</code></br>
+<em>
+<a href="#generators.external-secrets.io/v1alpha1.GitlabDeployTokenAuth">
+GitlabDeployTokenAuth
+</a>
+</em>
+</td>
+<td>
+<p>Auth configures how ESO authenticates with the GitLab API.</p>
+</td>
+</tr>
+</tbody>
+</table>
 <h3 id="generators.external-secrets.io/v1alpha1.Grafana">Grafana
 </h3>
 <p>

+ 15 - 0
docs/snippets/generator-gitlab-example.yaml

@@ -0,0 +1,15 @@
+---
+apiVersion: external-secrets.io/v1
+kind: ExternalSecret
+metadata:
+  name: gitlab-deploy-token
+spec:
+  refreshInterval: "1h"
+  target:
+    name: gitlab-deploy-token # Name for the secret to be created on the cluster
+  dataFrom:
+    - sourceRef:
+        generatorRef:
+          apiVersion: generators.external-secrets.io/v1alpha1
+          kind: GitlabDeployToken
+          name: gitlab-deploy-token

+ 25 - 0
docs/snippets/generator-gitlab.yaml

@@ -0,0 +1,25 @@
+# 1. Create a GitLab access token (personal, group, or project) with the `api`
+#    scope and at least the Maintainer role on the target project / group.
+#    https://docs.gitlab.com/api/deploy_tokens/
+# 2. Store it in a Kubernetes secret, e.g.
+#    kubectl create secret generic gitlab-api-token --from-literal=token=glpat-xxxx
+---
+apiVersion: generators.external-secrets.io/v1alpha1
+kind: GitlabDeployToken
+metadata:
+  name: gitlab-deploy-token
+spec:
+  url: "" # Optional, defaults to https://gitlab.com
+  projectID: "42" # Numeric ID or unescaped path e.g. group/project. Mutually exclusive with groupID.
+  # groupID: "7"   # Use instead of projectID to create a group deploy token.
+  name: "eso-managed"
+  scopes:
+    - read_repository
+    - read_registry
+  expiresAt: "2027-01-01T00:00:00Z" # Optional
+  username: "eso" # Optional, GitLab defaults to gitlab+deploy-token-{n}
+  auth:
+    token:
+      secretRef:
+        name: gitlab-api-token
+        key: token

+ 289 - 0
generators/v1/gitlab/gitlab.go

@@ -0,0 +1,289 @@
+/*
+Copyright © The ESO Authors
+
+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
+
+    https://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 provides a generator for GitLab project and group deploy tokens.
+package gitlab
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"net/http"
+	"net/url"
+	"strconv"
+	"strings"
+	"time"
+
+	apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/yaml"
+
+	genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1"
+	"github.com/external-secrets/external-secrets/runtime/esutils/resolvers"
+)
+
+const (
+	defaultGitlabAPI = "https://gitlab.com"
+	apiPath          = "/api/v4"
+
+	errNoSpec    = "no config spec provided"
+	errParseSpec = "unable to parse spec: %w"
+
+	// requestTimeout bounds a single Generate or Cleanup. Each makes exactly one
+	// HTTP call, governed by this context deadline; the default client uses the
+	// same value so a shorter transport timeout cannot preempt it and abandon an
+	// in-flight create (which would orphan a deploy token GitLab already minted).
+	requestTimeout = 30 * time.Second
+)
+
+// Generator implements GitLab deploy token generation.
+type Generator struct {
+	httpClient *http.Client
+}
+
+// deployTokenState is persisted as the generator state so that Cleanup can revoke
+// the deploy token that Generate created. GitLab deploy tokens are persistent, so
+// without revoking them every refresh would leave a dangling token behind.
+type deployTokenState struct {
+	URL       string `json:"url"`
+	ProjectID string `json:"projectID,omitempty"`
+	GroupID   string `json:"groupID,omitempty"`
+	TokenID   int    `json:"tokenID"`
+}
+
+// createTokenResponse mirrors the fields returned by the GitLab deploy token API.
+type createTokenResponse struct {
+	ID       int    `json:"id"`
+	Name     string `json:"name"`
+	Username string `json:"username"`
+	Token    string `json:"token"`
+}
+
+// Generate creates a new GitLab deploy token and returns its username and token.
+func (g *Generator) Generate(ctx context.Context, jsonSpec *apiextensions.JSON, kube client.Client, namespace string) (map[string][]byte, genv1alpha1.GeneratorProviderState, error) {
+	return g.generate(ctx, jsonSpec, kube, namespace)
+}
+
+// Cleanup revokes the deploy token created during Generate. It is idempotent: a
+// token that has already been deleted (HTTP 404) is treated as success.
+func (g *Generator) Cleanup(ctx context.Context, jsonSpec *apiextensions.JSON, state genv1alpha1.GeneratorProviderState, kube client.Client, namespace string) error {
+	return g.cleanup(ctx, jsonSpec, state, kube, namespace)
+}
+
+func (g *Generator) generate(ctx context.Context, jsonSpec *apiextensions.JSON, kube client.Client, namespace string) (map[string][]byte, genv1alpha1.GeneratorProviderState, error) {
+	if jsonSpec == nil {
+		return nil, nil, errors.New(errNoSpec)
+	}
+	ctx, cancel := context.WithTimeout(ctx, requestTimeout)
+	defer cancel()
+
+	spec, err := parseSpec(jsonSpec.Raw)
+	if err != nil {
+		return nil, nil, fmt.Errorf(errParseSpec, err)
+	}
+	token, err := g.fetchAuthToken(ctx, kube, namespace, &spec.Spec)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	payload := map[string]any{
+		"name":   spec.Spec.Name,
+		"scopes": spec.Spec.Scopes,
+	}
+	if spec.Spec.Username != "" {
+		payload["username"] = spec.Spec.Username
+	}
+	if spec.Spec.ExpiresAt != nil {
+		payload["expires_at"] = spec.Spec.ExpiresAt.UTC().Format(time.RFC3339)
+	}
+	body, err := json.Marshal(payload)
+	if err != nil {
+		return nil, nil, fmt.Errorf("error marshaling payload: %w", err)
+	}
+
+	endpoint, err := deployTokensURL(&spec.Spec)
+	if err != nil {
+		return nil, nil, err
+	}
+	req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body))
+	if err != nil {
+		return nil, nil, fmt.Errorf("error creating request: %w", err)
+	}
+	req.Header.Set("PRIVATE-TOKEN", token)
+	req.Header.Set("Content-Type", "application/json")
+
+	resp, err := g.client().Do(req)
+	if err != nil {
+		return nil, nil, fmt.Errorf("error performing request: %w", err)
+	}
+	defer func() { _ = resp.Body.Close() }()
+
+	raw, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return nil, nil, fmt.Errorf("error reading response: %w", err)
+	}
+	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+		return nil, nil, fmt.Errorf("error generating deploy token: response code: %d, response: %s", resp.StatusCode, gitlabError(raw))
+	}
+
+	var parsed createTokenResponse
+	if err := json.Unmarshal(raw, &parsed); err != nil {
+		return nil, nil, fmt.Errorf("error decoding response: %w", err)
+	}
+	if parsed.Token == "" {
+		return nil, nil, errors.New("deploy token missing from GitLab response")
+	}
+
+	state, err := json.Marshal(deployTokenState{
+		URL:       spec.Spec.URL,
+		ProjectID: spec.Spec.ProjectID,
+		GroupID:   spec.Spec.GroupID,
+		TokenID:   parsed.ID,
+	})
+	if err != nil {
+		return nil, nil, fmt.Errorf("error marshaling state: %w", err)
+	}
+
+	return map[string][]byte{
+		"username": []byte(parsed.Username),
+		"token":    []byte(parsed.Token),
+	}, &apiextensions.JSON{Raw: state}, nil
+}
+
+func (g *Generator) cleanup(ctx context.Context, jsonSpec *apiextensions.JSON, rawState genv1alpha1.GeneratorProviderState, kube client.Client, namespace string) error {
+	if jsonSpec == nil || rawState == nil {
+		return nil
+	}
+	ctx, cancel := context.WithTimeout(ctx, requestTimeout)
+	defer cancel()
+
+	spec, err := parseSpec(jsonSpec.Raw)
+	if err != nil {
+		return fmt.Errorf(errParseSpec, err)
+	}
+	var state deployTokenState
+	if err := json.Unmarshal(rawState.Raw, &state); err != nil {
+		return fmt.Errorf("error parsing generator state: %w", err)
+	}
+	if state.TokenID == 0 {
+		return nil
+	}
+
+	authToken, err := g.fetchAuthToken(ctx, kube, namespace, &spec.Spec)
+	if err != nil {
+		return err
+	}
+
+	// Build the revoke endpoint from the persisted state, not the (possibly
+	// changed) current spec, so cleanup always targets where the token was made.
+	base, err := deployTokensURL(&genv1alpha1.GitlabDeployTokenSpec{
+		URL:       state.URL,
+		ProjectID: state.ProjectID,
+		GroupID:   state.GroupID,
+	})
+	if err != nil {
+		return err
+	}
+	endpoint := base + "/" + strconv.Itoa(state.TokenID)
+	req, err := http.NewRequestWithContext(ctx, http.MethodDelete, endpoint, http.NoBody)
+	if err != nil {
+		return fmt.Errorf("error creating request: %w", err)
+	}
+	req.Header.Set("PRIVATE-TOKEN", authToken)
+
+	resp, err := g.client().Do(req)
+	if err != nil {
+		return fmt.Errorf("error performing request: %w", err)
+	}
+	defer func() { _ = resp.Body.Close() }()
+
+	// 204 No Content on success; 404 means the token is already gone.
+	if resp.StatusCode == http.StatusNotFound || (resp.StatusCode >= 200 && resp.StatusCode < 300) {
+		return nil
+	}
+	raw, _ := io.ReadAll(resp.Body)
+	return fmt.Errorf("error revoking deploy token: response code: %d, response: %s", resp.StatusCode, gitlabError(raw))
+}
+
+func (g *Generator) client() *http.Client {
+	if g.httpClient != nil {
+		return g.httpClient
+	}
+	return &http.Client{Timeout: requestTimeout}
+}
+
+func (g *Generator) fetchAuthToken(ctx context.Context, kube client.Client, namespace string, spec *genv1alpha1.GitlabDeployTokenSpec) (string, error) {
+	token, err := resolvers.SecretKeyRef(ctx, kube, resolvers.EmptyStoreKind, namespace, &spec.Auth.Token.SecretRef)
+	if err != nil {
+		return "", fmt.Errorf("error getting GitLab token from secret: %w", err)
+	}
+	return token, nil
+}
+
+// deployTokensURL builds the deploy-tokens collection URL for the configured
+// project or group. Exactly one of projectID / groupID must be set.
+func deployTokensURL(spec *genv1alpha1.GitlabDeployTokenSpec) (string, error) {
+	base := spec.URL
+	if base == "" {
+		base = defaultGitlabAPI
+	}
+	// Trim trailing slashes so a user-supplied url such as
+	// "https://gitlab.example.com/" does not yield a double slash before the
+	// API path.
+	base = strings.TrimRight(base, "/")
+	switch {
+	case spec.ProjectID != "" && spec.GroupID == "":
+		return base + apiPath + "/projects/" + url.PathEscape(spec.ProjectID) + "/deploy_tokens", nil
+	case spec.GroupID != "" && spec.ProjectID == "":
+		return base + apiPath + "/groups/" + url.PathEscape(spec.GroupID) + "/deploy_tokens", nil
+	default:
+		return "", errors.New("exactly one of projectID or groupID must be set")
+	}
+}
+
+// gitlabError extracts a human-readable message from a GitLab error body, which
+// uses either a "message" or an "error" field.
+func gitlabError(raw []byte) string {
+	var body map[string]any
+	if err := json.Unmarshal(raw, &body); err == nil {
+		if msg, ok := body["message"]; ok {
+			return fmt.Sprintf("%v", msg)
+		}
+		if msg, ok := body["error"]; ok {
+			return fmt.Sprintf("%v", msg)
+		}
+	}
+	return string(raw)
+}
+
+func parseSpec(data []byte) (*genv1alpha1.GitlabDeployToken, error) {
+	var spec genv1alpha1.GitlabDeployToken
+	err := yaml.Unmarshal(data, &spec)
+	return &spec, err
+}
+
+// NewGenerator creates a new Generator instance.
+func NewGenerator() genv1alpha1.Generator {
+	return &Generator{}
+}
+
+// Kind returns the generator kind.
+func Kind() string {
+	return string(genv1alpha1.GeneratorKindGitlabDeployToken)
+}

+ 337 - 0
generators/v1/gitlab/gitlab_test.go

@@ -0,0 +1,337 @@
+/*
+Copyright © The ESO Authors
+
+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
+
+    https://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"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	corev1 "k8s.io/api/core/v1"
+	apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
+
+	genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1"
+)
+
+const (
+	testNamespace = "foo"
+	testSecret    = "gitlab-token"
+	testKey       = "token"
+)
+
+func newKube() client.Client {
+	return clientfake.NewClientBuilder().WithObjects(&corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      testSecret,
+			Namespace: testNamespace,
+		},
+		Data: map[string][]byte{
+			testKey: []byte("glpat-secret-access-token"),
+		},
+	}).Build()
+}
+
+// captured records the create requests the mock GitLab server received.
+type captured struct {
+	method      string
+	path        string
+	privateTok  string
+	contentType string
+	body        map[string]any
+}
+
+func newServer(t *testing.T, status int, response []byte, sink *captured) *httptest.Server {
+	return httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+		sink.method = req.Method
+		// EscapedPath() preserves the on-the-wire encoding (e.g. group%2Fproject),
+		// whereas req.URL.Path would be decoded back and hide double/non-encoding.
+		sink.path = req.URL.EscapedPath()
+		sink.privateTok = req.Header.Get("PRIVATE-TOKEN")
+		sink.contentType = req.Header.Get("Content-Type")
+		if req.Body != nil && req.Method == http.MethodPost {
+			_ = json.NewDecoder(req.Body).Decode(&sink.body)
+		}
+		rw.WriteHeader(status)
+		_, _ = rw.Write(response)
+	}))
+}
+
+func specJSON(t *testing.T, url, projectID, groupID string) *apiextensions.JSON {
+	t.Helper()
+	target := ""
+	if projectID != "" {
+		target = fmt.Sprintf("  projectID: %q\n", projectID)
+	}
+	if groupID != "" {
+		target += fmt.Sprintf("  groupID: %q\n", groupID)
+	}
+	raw := fmt.Sprintf(`apiVersion: generators.external-secrets.io/v1alpha1
+kind: GitlabDeployToken
+spec:
+  url: %q
+%s  name: "eso-token"
+  scopes:
+  - read_repository
+  - read_registry
+  username: "custom-user"
+  auth:
+    token:
+      secretRef:
+        name: %q
+        key: %q
+`, url, target, testSecret, testKey)
+	return &apiextensions.JSON{Raw: []byte(raw)}
+}
+
+func TestGenerate(t *testing.T) {
+	createResp := []byte(`{
+		"id": 42,
+		"name": "eso-token",
+		"username": "custom-user",
+		"expires_at": null,
+		"token": "gitlab-deploy-token-value",
+		"revoked": false,
+		"expired": false,
+		"scopes": ["read_repository", "read_registry"]
+	}`)
+
+	t.Run("nil spec", func(t *testing.T) {
+		g := &Generator{}
+		_, _, err := g.generate(context.Background(), nil, newKube(), testNamespace)
+		require.Error(t, err)
+	})
+
+	t.Run("project deploy token", func(t *testing.T) {
+		sink := &captured{}
+		srv := newServer(t, http.StatusCreated, createResp, sink)
+		defer srv.Close()
+
+		g := &Generator{httpClient: srv.Client()}
+		got, state, err := g.generate(context.Background(), specJSON(t, srv.URL, "group/project", ""), newKube(), testNamespace)
+		require.NoError(t, err)
+
+		assert.Equal(t, http.MethodPost, sink.method)
+		// Unescaped path input is URL-escaped on the wire.
+		assert.Equal(t, "/api/v4/projects/group%2Fproject/deploy_tokens", sink.path)
+		assert.Equal(t, "glpat-secret-access-token", sink.privateTok)
+		assert.Equal(t, "application/json", sink.contentType)
+		assert.Equal(t, "eso-token", sink.body["name"])
+		assert.Equal(t, "custom-user", sink.body["username"])
+		assert.ElementsMatch(t, []any{"read_repository", "read_registry"}, sink.body["scopes"])
+
+		assert.Equal(t, map[string][]byte{
+			"username": []byte("custom-user"),
+			"token":    []byte("gitlab-deploy-token-value"),
+		}, got)
+
+		require.NotNil(t, state)
+		var st deployTokenState
+		require.NoError(t, json.Unmarshal(state.Raw, &st))
+		assert.Equal(t, 42, st.TokenID)
+		assert.Equal(t, "group/project", st.ProjectID)
+	})
+
+	t.Run("group deploy token", func(t *testing.T) {
+		sink := &captured{}
+		srv := newServer(t, http.StatusCreated, createResp, sink)
+		defer srv.Close()
+
+		g := &Generator{httpClient: srv.Client()}
+		_, state, err := g.generate(context.Background(), specJSON(t, srv.URL, "", "42"), newKube(), testNamespace)
+		require.NoError(t, err)
+		assert.Equal(t, "/api/v4/groups/42/deploy_tokens", sink.path)
+
+		var st deployTokenState
+		require.NoError(t, json.Unmarshal(state.Raw, &st))
+		assert.Equal(t, "42", st.GroupID)
+	})
+
+	t.Run("error when both project and group set", func(t *testing.T) {
+		g := &Generator{}
+		_, _, err := g.generate(context.Background(), specJSON(t, "https://gitlab.com", "1", "2"), newKube(), testNamespace)
+		require.ErrorContains(t, err, "exactly one of projectID or groupID")
+	})
+
+	t.Run("error on non-2xx response", func(t *testing.T) {
+		sink := &captured{}
+		srv := newServer(t, http.StatusForbidden, []byte(`{"message":"403 Forbidden"}`), sink)
+		defer srv.Close()
+
+		g := &Generator{httpClient: srv.Client()}
+		_, _, err := g.generate(context.Background(), specJSON(t, srv.URL, "1", ""), newKube(), testNamespace)
+		require.ErrorContains(t, err, "403 Forbidden")
+	})
+
+	t.Run("error when token missing in secret", func(t *testing.T) {
+		kube := clientfake.NewClientBuilder().WithObjects(&corev1.Secret{
+			ObjectMeta: metav1.ObjectMeta{Name: testSecret, Namespace: testNamespace},
+			Data:       map[string][]byte{},
+		}).Build()
+		g := &Generator{}
+		_, _, err := g.generate(context.Background(), specJSON(t, "https://gitlab.com", "1", ""), kube, testNamespace)
+		require.ErrorContains(t, err, "cannot find secret data for key")
+	})
+
+	t.Run("expires_at is normalized to RFC3339 UTC", func(t *testing.T) {
+		// GitLab's deploy-token API documents an ISO8601 expiry; RFC3339 is a
+		// profile of ISO8601 and the generator always emits a UTC datetime,
+		// regardless of the offset the user supplied. username is left unset
+		// here so this also covers "expiresAt set, other optional field unset".
+		cases := []struct{ in, want string }{
+			{"2025-12-31T23:59:59Z", "2025-12-31T23:59:59Z"},
+			{"2030-01-02T03:04:05Z", "2030-01-02T03:04:05Z"},
+			{"2025-06-30T12:00:00+02:00", "2025-06-30T10:00:00Z"},
+		}
+		for _, tc := range cases {
+			sink := &captured{}
+			srv := newServer(t, http.StatusCreated, createResp, sink)
+
+			raw := fmt.Sprintf(`apiVersion: generators.external-secrets.io/v1alpha1
+kind: GitlabDeployToken
+spec:
+  url: %q
+  projectID: "1"
+  name: "eso-token"
+  scopes:
+  - read_repository
+  expiresAt: %q
+  auth:
+    token:
+      secretRef:
+        name: %q
+        key: %q
+`, srv.URL, tc.in, testSecret, testKey)
+
+			g := &Generator{httpClient: srv.Client()}
+			_, _, err := g.generate(context.Background(), &apiextensions.JSON{Raw: []byte(raw)}, newKube(), testNamespace)
+			require.NoError(t, err)
+			assert.Equal(t, tc.want, sink.body["expires_at"])
+			srv.Close()
+		}
+	})
+
+	t.Run("optional fields omitted are not sent", func(t *testing.T) {
+		sink := &captured{}
+		srv := newServer(t, http.StatusCreated, createResp, sink)
+		defer srv.Close()
+
+		raw := fmt.Sprintf(`apiVersion: generators.external-secrets.io/v1alpha1
+kind: GitlabDeployToken
+spec:
+  url: %q
+  projectID: "1"
+  name: "eso-token"
+  scopes:
+  - read_repository
+  auth:
+    token:
+      secretRef:
+        name: %q
+        key: %q
+`, srv.URL, testSecret, testKey)
+
+		g := &Generator{httpClient: srv.Client()}
+		_, _, err := g.generate(context.Background(), &apiextensions.JSON{Raw: []byte(raw)}, newKube(), testNamespace)
+		require.NoError(t, err)
+
+		_, hasUser := sink.body["username"]
+		assert.False(t, hasUser, "username must be omitted from the request when unset")
+		_, hasExp := sink.body["expires_at"]
+		assert.False(t, hasExp, "expires_at must be omitted from the request when unset")
+	})
+}
+
+func TestCleanup(t *testing.T) {
+	t.Run("nil state is a no-op", func(t *testing.T) {
+		g := &Generator{}
+		require.NoError(t, g.cleanup(context.Background(), specJSON(t, "https://gitlab.com", "1", ""), nil, newKube(), testNamespace))
+	})
+
+	t.Run("revokes project deploy token", func(t *testing.T) {
+		sink := &captured{}
+		srv := newServer(t, http.StatusNoContent, nil, sink)
+		defer srv.Close()
+
+		state := &apiextensions.JSON{Raw: []byte(`{"url":"` + srv.URL + `","projectID":"1","tokenID":42}`)}
+		g := &Generator{httpClient: srv.Client()}
+		require.NoError(t, g.cleanup(context.Background(), specJSON(t, srv.URL, "1", ""), state, newKube(), testNamespace))
+		assert.Equal(t, http.MethodDelete, sink.method)
+		assert.Equal(t, "/api/v4/projects/1/deploy_tokens/42", sink.path)
+		assert.Equal(t, "glpat-secret-access-token", sink.privateTok)
+	})
+
+	t.Run("idempotent on 404", func(t *testing.T) {
+		sink := &captured{}
+		srv := newServer(t, http.StatusNotFound, []byte(`{"message":"404 Not Found"}`), sink)
+		defer srv.Close()
+
+		state := &apiextensions.JSON{Raw: []byte(`{"url":"` + srv.URL + `","groupID":"7","tokenID":99}`)}
+		g := &Generator{httpClient: srv.Client()}
+		require.NoError(t, g.cleanup(context.Background(), specJSON(t, srv.URL, "", "7"), state, newKube(), testNamespace))
+		assert.Equal(t, "/api/v4/groups/7/deploy_tokens/99", sink.path)
+	})
+
+	t.Run("error on 500", func(t *testing.T) {
+		sink := &captured{}
+		srv := newServer(t, http.StatusInternalServerError, []byte(`{"message":"boom"}`), sink)
+		defer srv.Close()
+
+		state := &apiextensions.JSON{Raw: []byte(`{"url":"` + srv.URL + `","projectID":"1","tokenID":42}`)}
+		g := &Generator{httpClient: srv.Client()}
+		require.ErrorContains(t, g.cleanup(context.Background(), specJSON(t, srv.URL, "1", ""), state, newKube(), testNamespace), "boom")
+	})
+}
+
+func TestDeployTokensURL(t *testing.T) {
+	tests := []struct {
+		name    string
+		spec    genv1alpha1.GitlabDeployTokenSpec
+		want    string
+		wantErr bool
+	}{
+		{name: "project default url", spec: genv1alpha1.GitlabDeployTokenSpec{ProjectID: "10"}, want: "https://gitlab.com/api/v4/projects/10/deploy_tokens"},
+		{name: "group custom url", spec: genv1alpha1.GitlabDeployTokenSpec{URL: "https://gl.example.com", GroupID: "5"}, want: "https://gl.example.com/api/v4/groups/5/deploy_tokens"},
+		{name: "path project encoded", spec: genv1alpha1.GitlabDeployTokenSpec{ProjectID: "grp/proj"}, want: "https://gitlab.com/api/v4/projects/grp%2Fproj/deploy_tokens"},
+		{name: "trailing slash trimmed", spec: genv1alpha1.GitlabDeployTokenSpec{URL: "https://gl.example.com/", ProjectID: "10"}, want: "https://gl.example.com/api/v4/projects/10/deploy_tokens"},
+		{
+			name: "multiple trailing slashes trimmed",
+			spec: genv1alpha1.GitlabDeployTokenSpec{URL: "https://gl.example.com///", GroupID: "5"},
+			want: "https://gl.example.com/api/v4/groups/5/deploy_tokens",
+		},
+		{name: "both set", spec: genv1alpha1.GitlabDeployTokenSpec{ProjectID: "1", GroupID: "2"}, wantErr: true},
+		{name: "neither set", spec: genv1alpha1.GitlabDeployTokenSpec{}, wantErr: true},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := deployTokensURL(&tt.spec)
+			if tt.wantErr {
+				require.Error(t, err)
+				return
+			}
+			require.NoError(t, err)
+			assert.Equal(t, tt.want, got)
+		})
+	}
+}

+ 81 - 0
generators/v1/gitlab/go.mod

@@ -0,0 +1,81 @@
+module github.com/external-secrets/external-secrets/generators/v1/gitlab
+
+go 1.26.4
+
+require (
+	github.com/external-secrets/external-secrets/apis v0.0.0
+	github.com/external-secrets/external-secrets/runtime v0.0.0-00010101000000-000000000000
+	github.com/stretchr/testify v1.11.1
+	k8s.io/api v0.35.2
+	k8s.io/apiextensions-apiserver v0.35.2
+	k8s.io/apimachinery v0.35.2
+	sigs.k8s.io/controller-runtime v0.23.3
+	sigs.k8s.io/yaml v1.6.0
+)
+
+require (
+	github.com/beorn7/perks v1.0.1 // indirect
+	github.com/cespare/xxhash/v2 v2.3.0 // indirect
+	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
+	github.com/emicklei/go-restful/v3 v3.13.0 // indirect
+	github.com/evanphx/json-patch/v5 v5.9.11 // indirect
+	github.com/fsnotify/fsnotify v1.9.0 // indirect
+	github.com/fxamacker/cbor/v2 v2.9.0 // indirect
+	github.com/go-logr/logr v1.4.3 // indirect
+	github.com/go-openapi/jsonpointer v0.22.5 // indirect
+	github.com/go-openapi/jsonreference v0.21.5 // indirect
+	github.com/go-openapi/swag v0.25.5 // indirect
+	github.com/go-openapi/swag/cmdutils v0.25.5 // indirect
+	github.com/go-openapi/swag/conv v0.25.5 // indirect
+	github.com/go-openapi/swag/fileutils v0.25.5 // indirect
+	github.com/go-openapi/swag/jsonname v0.25.5 // indirect
+	github.com/go-openapi/swag/jsonutils v0.25.5 // indirect
+	github.com/go-openapi/swag/loading v0.25.5 // indirect
+	github.com/go-openapi/swag/mangling v0.25.5 // indirect
+	github.com/go-openapi/swag/netutils v0.25.5 // indirect
+	github.com/go-openapi/swag/stringutils v0.25.5 // indirect
+	github.com/go-openapi/swag/typeutils v0.25.5 // indirect
+	github.com/go-openapi/swag/yamlutils v0.25.5 // indirect
+	github.com/google/btree v1.1.3 // indirect
+	github.com/google/gnostic-models v0.7.1 // indirect
+	github.com/google/go-cmp v0.7.0 // indirect
+	github.com/google/uuid v1.6.0 // indirect
+	github.com/json-iterator/go v1.1.12 // indirect
+	github.com/kr/text v0.2.0 // indirect
+	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+	github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
+	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
+	github.com/prometheus/client_golang v1.23.2 // indirect
+	github.com/prometheus/client_model v0.6.2 // indirect
+	github.com/prometheus/common v0.67.5 // indirect
+	github.com/prometheus/procfs v0.20.1 // indirect
+	github.com/spf13/pflag v1.0.10 // indirect
+	github.com/x448/float16 v0.8.4 // indirect
+	go.yaml.in/yaml/v2 v2.4.4 // indirect
+	go.yaml.in/yaml/v3 v3.0.4 // indirect
+	golang.org/x/net v0.56.0 // indirect
+	golang.org/x/oauth2 v0.36.0 // indirect
+	golang.org/x/sync v0.21.0 // indirect
+	golang.org/x/sys v0.46.0 // indirect
+	golang.org/x/term v0.44.0 // indirect
+	golang.org/x/text v0.38.0 // indirect
+	golang.org/x/time v0.15.0 // indirect
+	gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
+	google.golang.org/protobuf v1.36.11 // indirect
+	gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
+	gopkg.in/inf.v0 v0.9.1 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
+	k8s.io/client-go v0.35.2 // indirect
+	k8s.io/klog/v2 v2.140.0 // indirect
+	k8s.io/kube-openapi v0.0.0-20260304202019-5b3e3fdb0acf // indirect
+	k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect
+	sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
+	sigs.k8s.io/randfill v1.0.0 // indirect
+	sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect
+)
+
+replace (
+	github.com/external-secrets/external-secrets/apis => ../../../apis
+	github.com/external-secrets/external-secrets/runtime => ../../../runtime
+)

+ 186 - 0
generators/v1/gitlab/go.sum

@@ -0,0 +1,186 @@
+github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
+github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes=
+github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
+github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k=
+github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
+github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
+github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
+github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
+github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
+github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
+github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
+github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
+github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA=
+github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0=
+github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE=
+github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw=
+github.com/go-openapi/swag v0.25.5 h1:pNkwbUEeGwMtcgxDr+2GBPAk4kT+kJ+AaB+TMKAg+TU=
+github.com/go-openapi/swag v0.25.5/go.mod h1:B3RT6l8q7X803JRxa2e59tHOiZlX1t8viplOcs9CwTA=
+github.com/go-openapi/swag/cmdutils v0.25.5 h1:yh5hHrpgsw4NwM9KAEtaDTXILYzdXh/I8Whhx9hKj7c=
+github.com/go-openapi/swag/cmdutils v0.25.5/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0=
+github.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g=
+github.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k=
+github.com/go-openapi/swag/fileutils v0.25.5 h1:B6JTdOcs2c0dBIs9HnkyTW+5gC+8NIhVBUwERkFhMWk=
+github.com/go-openapi/swag/fileutils v0.25.5/go.mod h1:V3cT9UdMQIaH4WiTrUc9EPtVA4txS0TOmRURmhGF4kc=
+github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo=
+github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU=
+github.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo=
+github.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4=
+github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5 h1:SX6sE4FrGb4sEnnxbFL/25yZBb5Hcg1inLeErd86Y1U=
+github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5/go.mod h1:/2KvOTrKWjVA5Xli3DZWdMCZDzz3uV/T7bXwrKWPquo=
+github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU=
+github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g=
+github.com/go-openapi/swag/mangling v0.25.5 h1:hyrnvbQRS7vKePQPHHDso+k6CGn5ZBs5232UqWZmJZw=
+github.com/go-openapi/swag/mangling v0.25.5/go.mod h1:6hadXM/o312N/h98RwByLg088U61TPGiltQn71Iw0NY=
+github.com/go-openapi/swag/netutils v0.25.5 h1:LZq2Xc2QI8+7838elRAaPCeqJnHODfSyOa7ZGfxDKlU=
+github.com/go-openapi/swag/netutils v0.25.5/go.mod h1:lHbtmj4m57APG/8H7ZcMMSWzNqIQcu0RFiXrPUara14=
+github.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M=
+github.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII=
+github.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E=
+github.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc=
+github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ=
+github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ=
+github.com/go-openapi/testify/enable/yaml/v2 v2.4.0 h1:7SgOMTvJkM8yWrQlU8Jm18VeDPuAvB/xWrdxFJkoFag=
+github.com/go-openapi/testify/enable/yaml/v2 v2.4.0/go.mod h1:14iV8jyyQlinc9StD7w1xVPW3CO3q1Gj04Jy//Kw4VM=
+github.com/go-openapi/testify/v2 v2.4.0 h1:8nsPrHVCWkQ4p8h1EsRVymA2XABB4OT40gcvAu+voFM=
+github.com/go-openapi/testify/v2 v2.4.0/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
+github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
+github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
+github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
+github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
+github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c=
+github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
+github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc=
+github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
+github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
+github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
+github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/onsi/ginkgo/v2 v2.28.0 h1:Rrf+lVLmtlBIKv6KrIGJCjyY8N36vDVcutbGJkyqjJc=
+github.com/onsi/ginkgo/v2 v2.28.0/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
+github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
+github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
+github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
+github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
+github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
+github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
+github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
+github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
+github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
+github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
+github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
+github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
+github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
+github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
+go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
+go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
+go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo=
+go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
+go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
+go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
+go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
+go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
+golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
+golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
+golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
+golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
+golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
+golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
+golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
+golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
+golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
+golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
+golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
+golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
+golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
+golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
+golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
+golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
+golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
+gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0=
+gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
+google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
+google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
+gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
+gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
+gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+k8s.io/api v0.35.2 h1:tW7mWc2RpxW7HS4CoRXhtYHSzme1PN1UjGHJ1bdrtdw=
+k8s.io/api v0.35.2/go.mod h1:7AJfqGoAZcwSFhOjcGM7WV05QxMMgUaChNfLTXDRE60=
+k8s.io/apiextensions-apiserver v0.35.2 h1:iyStXHoJZsUXPh/nFAsjC29rjJWdSgUmG1XpApE29c0=
+k8s.io/apiextensions-apiserver v0.35.2/go.mod h1:OdyGvcO1FtMDWQ+rRh/Ei3b6X3g2+ZDHd0MSRGeS8rU=
+k8s.io/apimachinery v0.35.2 h1:NqsM/mmZA7sHW02JZ9RTtk3wInRgbVxL8MPfzSANAK8=
+k8s.io/apimachinery v0.35.2/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
+k8s.io/client-go v0.35.2 h1:YUfPefdGJA4aljDdayAXkc98DnPkIetMl4PrKX97W9o=
+k8s.io/client-go v0.35.2/go.mod h1:4QqEwh4oQpeK8AaefZ0jwTFJw/9kIjdQi0jpKeYvz7g=
+k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
+k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
+k8s.io/kube-openapi v0.0.0-20260304202019-5b3e3fdb0acf h1:btPscg4cMql0XdYK2jLsJcNEKmACJz8l+U7geC06FiM=
+k8s.io/kube-openapi v0.0.0-20260304202019-5b3e3fdb0acf/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
+k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU=
+k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
+sigs.k8s.io/controller-runtime v0.23.3 h1:VjB/vhoPoA9l1kEKZHBMnQF33tdCLQKJtydy4iqwZ80=
+sigs.k8s.io/controller-runtime v0.23.3/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0=
+sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
+sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
+sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
+sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
+sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8=
+sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
+sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
+sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=

+ 2 - 0
go.mod

@@ -11,6 +11,7 @@ replace (
 	github.com/external-secrets/external-secrets/generators/v1/fake => ./generators/v1/fake
 	github.com/external-secrets/external-secrets/generators/v1/gcr => ./generators/v1/gcr
 	github.com/external-secrets/external-secrets/generators/v1/github => ./generators/v1/github
+	github.com/external-secrets/external-secrets/generators/v1/gitlab => ./generators/v1/gitlab
 	github.com/external-secrets/external-secrets/generators/v1/grafana => ./generators/v1/grafana
 	github.com/external-secrets/external-secrets/generators/v1/mfa => ./generators/v1/mfa
 	github.com/external-secrets/external-secrets/generators/v1/password => ./generators/v1/password
@@ -127,6 +128,7 @@ require (
 	github.com/external-secrets/external-secrets/generators/v1/fake v0.0.0-00010101000000-000000000000
 	github.com/external-secrets/external-secrets/generators/v1/gcr v0.0.0-00010101000000-000000000000
 	github.com/external-secrets/external-secrets/generators/v1/github v0.0.0-00010101000000-000000000000
+	github.com/external-secrets/external-secrets/generators/v1/gitlab v0.0.0-00010101000000-000000000000
 	github.com/external-secrets/external-secrets/generators/v1/grafana v0.0.0-00010101000000-000000000000
 	github.com/external-secrets/external-secrets/generators/v1/mfa v0.0.0-00010101000000-000000000000
 	github.com/external-secrets/external-secrets/generators/v1/password v0.0.0-00010101000000-000000000000

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

@@ -85,6 +85,7 @@ nav:
           - Fake: api/generator/fake.md
           - Webhook: api/generator/webhook.md
           - Github: api/generator/github.md
+          - Gitlab: api/generator/gitlab.md
           - UUID: api/generator/uuid.md
           - MFA: api/generator/mfa.md
           - SSHKey: api/generator/sshkey.md

+ 2 - 0
pkg/register/generators.go

@@ -25,6 +25,7 @@ import (
 	fakegen "github.com/external-secrets/external-secrets/generators/v1/fake"
 	gcr "github.com/external-secrets/external-secrets/generators/v1/gcr"
 	githubgen "github.com/external-secrets/external-secrets/generators/v1/github"
+	gitlabgen "github.com/external-secrets/external-secrets/generators/v1/gitlab"
 	grafana "github.com/external-secrets/external-secrets/generators/v1/grafana"
 	mfa "github.com/external-secrets/external-secrets/generators/v1/mfa"
 	password "github.com/external-secrets/external-secrets/generators/v1/password"
@@ -45,6 +46,7 @@ func init() {
 	genv1alpha1.Register(fakegen.Kind(), fakegen.NewGenerator())
 	genv1alpha1.Register(gcr.Kind(), gcr.NewGenerator())
 	genv1alpha1.Register(githubgen.Kind(), githubgen.NewGenerator())
+	genv1alpha1.Register(gitlabgen.Kind(), gitlabgen.NewGenerator())
 	genv1alpha1.Register(grafana.Kind(), grafana.NewGenerator())
 	genv1alpha1.Register(mfa.Kind(), mfa.NewGenerator())
 	genv1alpha1.Register(password.Kind(), password.NewGenerator())

+ 11 - 0
runtime/esutils/resolvers/generator.go

@@ -218,6 +218,17 @@ func clusterGeneratorToVirtual(gen *genv1alpha1.ClusterGenerator) (client.Object
 			},
 			Spec: *gen.Spec.Generator.GithubAccessTokenSpec,
 		}, nil
+	case genv1alpha1.GeneratorKindGitlabDeployToken:
+		if gen.Spec.Generator.GitlabDeployTokenSpec == nil {
+			return nil, fmt.Errorf("when kind is %s, GitlabDeployTokenSpec must be set", gen.Spec.Kind)
+		}
+		return &genv1alpha1.GitlabDeployToken{
+			TypeMeta: metav1.TypeMeta{
+				APIVersion: genv1alpha1.SchemeGroupVersion.String(),
+				Kind:       genv1alpha1.GitlabDeployTokenKind,
+			},
+			Spec: *gen.Spec.Generator.GitlabDeployTokenSpec,
+		}, nil
 	case genv1alpha1.GeneratorKindQuayAccessToken:
 		if gen.Spec.Generator.QuayAccessTokenSpec == nil {
 			return nil, fmt.Errorf("when kind is %s, QuayAccessTokenSpec must be set", gen.Spec.Kind)

+ 2 - 2
tests/__snapshot__/clusterexternalsecret-v1.yaml

@@ -20,7 +20,7 @@ spec:
       sourceRef:
         generatorRef:
           apiVersion: external-secrets.io/v1
-          kind: "ACRAccessToken" # "ACRAccessToken", "BeyondtrustWorkloadCredentialsDynamicSecret", "ClusterGenerator", "CloudsmithAccessToken", "ECRAuthorizationToken", "Fake", "GCRAccessToken", "GithubAccessToken", "QuayAccessToken", "Password", "SSHKey", "STSSessionToken", "UUID", "VaultDynamicSecret", "Webhook", "Grafana", "MFA"
+          kind: "ACRAccessToken" # "ACRAccessToken", "BeyondtrustWorkloadCredentialsDynamicSecret", "ClusterGenerator", "CloudsmithAccessToken", "ECRAuthorizationToken", "Fake", "GCRAccessToken", "GithubAccessToken", "GitlabDeployToken", "QuayAccessToken", "Password", "SSHKey", "STSSessionToken", "UUID", "VaultDynamicSecret", "Webhook", "Grafana", "MFA"
           name: string
         storeRef:
           kind: "SecretStore" # "SecretStore", "ClusterSecretStore"
@@ -57,7 +57,7 @@ spec:
       sourceRef:
         generatorRef:
           apiVersion: external-secrets.io/v1
-          kind: "ACRAccessToken" # "ACRAccessToken", "BeyondtrustWorkloadCredentialsDynamicSecret", "ClusterGenerator", "CloudsmithAccessToken", "ECRAuthorizationToken", "Fake", "GCRAccessToken", "GithubAccessToken", "QuayAccessToken", "Password", "SSHKey", "STSSessionToken", "UUID", "VaultDynamicSecret", "Webhook", "Grafana", "MFA"
+          kind: "ACRAccessToken" # "ACRAccessToken", "BeyondtrustWorkloadCredentialsDynamicSecret", "ClusterGenerator", "CloudsmithAccessToken", "ECRAuthorizationToken", "Fake", "GCRAccessToken", "GithubAccessToken", "GitlabDeployToken", "QuayAccessToken", "Password", "SSHKey", "STSSessionToken", "UUID", "VaultDynamicSecret", "Webhook", "Grafana", "MFA"
           name: string
         storeRef:
           kind: "SecretStore" # "SecretStore", "ClusterSecretStore"

+ 15 - 1
tests/__snapshot__/clustergenerator-v1alpha1.yaml

@@ -127,6 +127,20 @@ spec:
       permissions: {}
       repositories: [] # minItems 0 of type string
       url: string
+    gitlabDeployTokenSpec:
+      auth:
+        token:
+          secretRef:
+            key: string
+            name: string
+            namespace: string
+      expiresAt: 2024-10-11T12:48:44Z
+      groupID: string
+      name: string
+      projectID: string
+      scopes: [string] # minItems 1 of type string
+      url: string
+      username: string
     grafanaSpec:
       auth:
         basic:
@@ -376,4 +390,4 @@ spec:
           name: string
       timeout: string
       url: string
-  kind: "ACRAccessToken" # "ACRAccessToken", "BeyondtrustWorkloadCredentialsDynamicSecret", "CloudsmithAccessToken", "ECRAuthorizationToken", "Fake", "GCRAccessToken", "GithubAccessToken", "QuayAccessToken", "Password", "SSHKey", "STSSessionToken", "UUID", "VaultDynamicSecret", "Webhook", "Grafana", "MFA"
+  kind: "ACRAccessToken" # "ACRAccessToken", "BeyondtrustWorkloadCredentialsDynamicSecret", "CloudsmithAccessToken", "ECRAuthorizationToken", "Fake", "GCRAccessToken", "GithubAccessToken", "GitlabDeployToken", "QuayAccessToken", "Password", "SSHKey", "STSSessionToken", "UUID", "VaultDynamicSecret", "Webhook", "Grafana", "MFA"

+ 2 - 2
tests/__snapshot__/externalsecret-v1.yaml

@@ -15,7 +15,7 @@ spec:
     sourceRef:
       generatorRef:
         apiVersion: external-secrets.io/v1
-        kind: "ACRAccessToken" # "ACRAccessToken", "BeyondtrustWorkloadCredentialsDynamicSecret", "ClusterGenerator", "CloudsmithAccessToken", "ECRAuthorizationToken", "Fake", "GCRAccessToken", "GithubAccessToken", "QuayAccessToken", "Password", "SSHKey", "STSSessionToken", "UUID", "VaultDynamicSecret", "Webhook", "Grafana", "MFA"
+        kind: "ACRAccessToken" # "ACRAccessToken", "BeyondtrustWorkloadCredentialsDynamicSecret", "ClusterGenerator", "CloudsmithAccessToken", "ECRAuthorizationToken", "Fake", "GCRAccessToken", "GithubAccessToken", "GitlabDeployToken", "QuayAccessToken", "Password", "SSHKey", "STSSessionToken", "UUID", "VaultDynamicSecret", "Webhook", "Grafana", "MFA"
         name: string
       storeRef:
         kind: "SecretStore" # "SecretStore", "ClusterSecretStore"
@@ -52,7 +52,7 @@ spec:
     sourceRef:
       generatorRef:
         apiVersion: external-secrets.io/v1
-        kind: "ACRAccessToken" # "ACRAccessToken", "BeyondtrustWorkloadCredentialsDynamicSecret", "ClusterGenerator", "CloudsmithAccessToken", "ECRAuthorizationToken", "Fake", "GCRAccessToken", "GithubAccessToken", "QuayAccessToken", "Password", "SSHKey", "STSSessionToken", "UUID", "VaultDynamicSecret", "Webhook", "Grafana", "MFA"
+        kind: "ACRAccessToken" # "ACRAccessToken", "BeyondtrustWorkloadCredentialsDynamicSecret", "ClusterGenerator", "CloudsmithAccessToken", "ECRAuthorizationToken", "Fake", "GCRAccessToken", "GithubAccessToken", "GitlabDeployToken", "QuayAccessToken", "Password", "SSHKey", "STSSessionToken", "UUID", "VaultDynamicSecret", "Webhook", "Grafana", "MFA"
         name: string
       storeRef:
         kind: "SecretStore" # "SecretStore", "ClusterSecretStore"

+ 17 - 0
tests/__snapshot__/gitlabdeploytoken-v1alpha1.yaml

@@ -0,0 +1,17 @@
+apiVersion: generators.external-secrets.io/v1alpha1
+kind: GitlabDeployToken
+metadata: {}
+spec:
+  auth:
+    token:
+      secretRef:
+        key: string
+        name: string
+        namespace: string
+  expiresAt: 2024-10-11T12:48:44Z
+  groupID: string
+  name: string
+  projectID: string
+  scopes: [string] # minItems 1 of type string
+  url: string
+  username: string

+ 1 - 1
tests/__snapshot__/pushsecret-v1alpha1.yaml

@@ -45,7 +45,7 @@ spec:
   selector:
     generatorRef:
       apiVersion: external-secrets.io/v1alpha1
-      kind: "ACRAccessToken" # "ACRAccessToken", "BeyondtrustWorkloadCredentialsDynamicSecret", "ClusterGenerator", "CloudsmithAccessToken", "ECRAuthorizationToken", "Fake", "GCRAccessToken", "GithubAccessToken", "QuayAccessToken", "Password", "SSHKey", "STSSessionToken", "UUID", "VaultDynamicSecret", "Webhook", "Grafana", "MFA"
+      kind: "ACRAccessToken" # "ACRAccessToken", "BeyondtrustWorkloadCredentialsDynamicSecret", "ClusterGenerator", "CloudsmithAccessToken", "ECRAuthorizationToken", "Fake", "GCRAccessToken", "GithubAccessToken", "GitlabDeployToken", "QuayAccessToken", "Password", "SSHKey", "STSSessionToken", "UUID", "VaultDynamicSecret", "Webhook", "Grafana", "MFA"
       name: string
     secret:
       name: string

+ 7 - 0
tests/gitlabdeploytoken_test.yaml

@@ -0,0 +1,7 @@
+suite: test GitlabDeployToken
+template: tests/crds/gitlabdeploytoken.yml
+tests:
+  - it: matches GitlabDeployToken correctly
+    asserts:
+      - matchSnapshot:
+          path: tests/__snapshot__