Browse Source

feat(generators): add Quay generator support (#4252)

* feat(generators): add Quay generator support

Signed-off-by: Ben Dronen <dronenb@users.noreply.github.com>

* fix(quay): better logic for default URL

Signed-off-by: Ben Dronen <dronenb@users.noreply.github.com>

* fix(quay): make CRD spec show the correct default URL

Signed-off-by: Ben Dronen <dronenb@users.noreply.github.com>

* docs: add Quay docs + add expiry to generator

Signed-off-by: Ben Dronen <dronenb@users.noreply.github.com>

* fix(quay-generator): better error handling, use context in http request, strip https:// prefix from registry

Signed-off-by: Ben Dronen <dronenb@users.noreply.github.com>

---------

Signed-off-by: Ben Dronen <dronenb@users.noreply.github.com>
dronenb 1 year ago
parent
commit
a0386badf8

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

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

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

@@ -108,6 +108,14 @@ var (
 	GithubAccessTokenGroupVersionKind = SchemeGroupVersion.WithKind(GithubAccessTokenKind)
 )
 
+// QuayAccessToken type metadata.
+var (
+	QuayAccessTokenKind             = reflect.TypeOf(QuayAccessToken{}).Name()
+	QuayAccessTokenGroupKind        = schema.GroupKind{Group: Group, Kind: QuayAccessTokenKind}.String()
+	QuayAccessTokenKindAPIVersion   = QuayAccessTokenKind + "." + SchemeGroupVersion.String()
+	QuayAccessTokenGroupVersionKind = SchemeGroupVersion.WithKind(QuayAccessTokenKind)
+)
+
 // Uuid type metadata.
 var (
 	UUIDKind             = reflect.TypeOf(UUID{}).Name()
@@ -146,6 +154,7 @@ func init() {
 	SchemeBuilder.Register(&Fake{}, &FakeList{})
 	SchemeBuilder.Register(&GCRAccessToken{}, &GCRAccessTokenList{})
 	SchemeBuilder.Register(&GithubAccessToken{}, &GithubAccessTokenList{})
+	SchemeBuilder.Register(&QuayAccessToken{}, &QuayAccessTokenList{})
 	SchemeBuilder.Register(&Password{}, &PasswordList{})
 	SchemeBuilder.Register(&STSSessionToken{}, &STSSessionTokenList{})
 	SchemeBuilder.Register(&UUID{}, &UUIDList{})

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

@@ -27,7 +27,7 @@ type ClusterGeneratorSpec struct {
 }
 
 // GeneratorKind represents a kind of generator.
-// +kubebuilder:validation:Enum=ACRAccessToken;ECRAuthorizationToken;Fake;GCRAccessToken;GithubAccessToken;Password;STSSessionToken;UUID;VaultDynamicSecret;Webhook
+// +kubebuilder:validation:Enum=ACRAccessToken;ECRAuthorizationToken;Fake;GCRAccessToken;GithubAccessToken;QuayAccessToken;Password;STSSessionToken;UUID;VaultDynamicSecret;Webhook
 type GeneratorKind string
 
 const (
@@ -36,6 +36,7 @@ const (
 	GeneratorKindFake                  GeneratorKind = "Fake"
 	GeneratorKindGCRAccessToken        GeneratorKind = "GCRAccessToken"
 	GeneratorKindGithubAccessToken     GeneratorKind = "GithubAccessToken"
+	GeneratorKindQuayAccessToken       GeneratorKind = "QuayAccessToken"
 	GeneratorKindPassword              GeneratorKind = "Password"
 	GeneratorKindSTSSessionToken       GeneratorKind = "STSSessionToken"
 	GeneratorKindUUID                  GeneratorKind = "UUID"
@@ -51,6 +52,7 @@ type GeneratorSpec struct {
 	FakeSpec                  *FakeSpec                  `json:"fakeSpec,omitempty"`
 	GCRAccessTokenSpec        *GCRAccessTokenSpec        `json:"gcrAccessTokenSpec,omitempty"`
 	GithubAccessTokenSpec     *GithubAccessTokenSpec     `json:"githubAccessTokenSpec,omitempty"`
+	QuayAccessTokenSpec       *QuayAccessTokenSpec       `json:"quayAccessTokenSpec,omitempty"`
 	PasswordSpec              *PasswordSpec              `json:"passwordSpec,omitempty"`
 	STSSessionTokenSpec       *STSSessionTokenSpec       `json:"stsSessionTokenSpec,omitempty"`
 	UUIDSpec                  *UUIDSpec                  `json:"uuidSpec,omitempty"`

+ 52 - 0
apis/generators/v1alpha1/types_quay.go

@@ -0,0 +1,52 @@
+/*
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v1alpha1
+
+import (
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+)
+
+type QuayAccessTokenSpec struct {
+	// URL configures the Quay instance URL. Defaults to quay.io.
+	URL string `json:"url,omitempty"`
+	// Name of the robot account you are federating with
+	RobotAccount string `json:"robotAccount"`
+	// Name of the service account you are federating with
+	ServiceAccountRef esmeta.ServiceAccountSelector `json:"serviceAccountRef"`
+}
+
+// QuayAccessToken generates Quay oauth token for pulling/pushing images
+// +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 QuayAccessToken struct {
+	metav1.TypeMeta   `json:",inline"`
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+
+	Spec QuayAccessTokenSpec `json:"spec,omitempty"`
+}
+
+// +kubebuilder:object:root=true
+
+// QuayAccessTokenList contains a list of ExternalSecret resources.
+type QuayAccessTokenList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata,omitempty"`
+	Items           []QuayAccessToken `json:"items"`
+}

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

@@ -668,6 +668,11 @@ func (in *GeneratorSpec) DeepCopyInto(out *GeneratorSpec) {
 		*out = new(GithubAccessTokenSpec)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.QuayAccessTokenSpec != nil {
+		in, out := &in.QuayAccessTokenSpec, &out.QuayAccessTokenSpec
+		*out = new(QuayAccessTokenSpec)
+		(*in).DeepCopyInto(*out)
+	}
 	if in.PasswordSpec != nil {
 		in, out := &in.PasswordSpec, &out.PasswordSpec
 		*out = new(PasswordSpec)
@@ -912,6 +917,80 @@ func (in *PasswordSpec) DeepCopy() *PasswordSpec {
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *QuayAccessToken) DeepCopyInto(out *QuayAccessToken) {
+	*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 QuayAccessToken.
+func (in *QuayAccessToken) DeepCopy() *QuayAccessToken {
+	if in == nil {
+		return nil
+	}
+	out := new(QuayAccessToken)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *QuayAccessToken) 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 *QuayAccessTokenList) DeepCopyInto(out *QuayAccessTokenList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]QuayAccessToken, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new QuayAccessTokenList.
+func (in *QuayAccessTokenList) DeepCopy() *QuayAccessTokenList {
+	if in == nil {
+		return nil
+	}
+	out := new(QuayAccessTokenList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *QuayAccessTokenList) 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 *QuayAccessTokenSpec) DeepCopyInto(out *QuayAccessTokenSpec) {
+	*out = *in
+	in.ServiceAccountRef.DeepCopyInto(&out.ServiceAccountRef)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new QuayAccessTokenSpec.
+func (in *QuayAccessTokenSpec) DeepCopy() *QuayAccessTokenSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(QuayAccessTokenSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *RequestParameters) DeepCopyInto(out *RequestParameters) {
 	*out = *in
 	if in.SessionDuration != nil {

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

@@ -165,6 +165,7 @@ spec:
                                   - Fake
                                   - GCRAccessToken
                                   - GithubAccessToken
+                                  - QuayAccessToken
                                   - Password
                                   - STSSessionToken
                                   - UUID
@@ -360,6 +361,7 @@ spec:
                                   - Fake
                                   - GCRAccessToken
                                   - GithubAccessToken
+                                  - QuayAccessToken
                                   - Password
                                   - STSSessionToken
                                   - UUID

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

@@ -455,6 +455,7 @@ spec:
                               - Fake
                               - GCRAccessToken
                               - GithubAccessToken
+                              - QuayAccessToken
                               - Password
                               - STSSessionToken
                               - UUID
@@ -649,6 +650,7 @@ spec:
                               - Fake
                               - GCRAccessToken
                               - GithubAccessToken
+                              - QuayAccessToken
                               - Password
                               - STSSessionToken
                               - UUID

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

@@ -188,6 +188,7 @@ spec:
                         - Fake
                         - GCRAccessToken
                         - GithubAccessToken
+                        - QuayAccessToken
                         - Password
                         - STSSessionToken
                         - UUID

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

@@ -566,6 +566,50 @@ spec:
                     - length
                     - noUpper
                     type: object
+                  quayAccessTokenSpec:
+                    properties:
+                      robotAccount:
+                        description: Name of the robot account you are federating
+                          with
+                        type: string
+                      serviceAccountRef:
+                        description: Name of the service account you are federating
+                          with
+                        properties:
+                          audiences:
+                            description: |-
+                              Audience specifies the `aud` claim for the service account token
+                              If the service account uses a well-known annotation for e.g. IRSA or GCP Workload Identity
+                              then this audiences will be appended to the list
+                            items:
+                              type: string
+                            type: array
+                          name:
+                            description: The name of the ServiceAccount 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: |-
+                              Namespace of the 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
+                        required:
+                        - name
+                        type: object
+                      url:
+                        description: URL configures the Quay instance URL. Defaults
+                          to quay.io.
+                        type: string
+                    required:
+                    - robotAccount
+                    - serviceAccountRef
+                    type: object
                   stsSessionTokenSpec:
                     properties:
                       auth:
@@ -1695,6 +1739,7 @@ spec:
                 - Fake
                 - GCRAccessToken
                 - GithubAccessToken
+                - QuayAccessToken
                 - Password
                 - STSSessionToken
                 - UUID

+ 89 - 0
config/crds/bases/generators.external-secrets.io_quayaccesstokens.yaml

@@ -0,0 +1,89 @@
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.16.5
+  labels:
+    external-secrets.io/component: controller
+  name: quayaccesstokens.generators.external-secrets.io
+spec:
+  group: generators.external-secrets.io
+  names:
+    categories:
+    - external-secrets
+    - external-secrets-generators
+    kind: QuayAccessToken
+    listKind: QuayAccessTokenList
+    plural: quayaccesstokens
+    singular: quayaccesstoken
+  scope: Namespaced
+  versions:
+  - name: v1alpha1
+    schema:
+      openAPIV3Schema:
+        description: QuayAccessToken generates Quay oauth token for pulling/pushing
+          images
+        properties:
+          apiVersion:
+            description: |-
+              APIVersion defines the versioned schema of this representation of an object.
+              Servers should convert recognized schemas to the latest internal value, and
+              may reject unrecognized values.
+              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
+            type: string
+          kind:
+            description: |-
+              Kind is a string value representing the REST resource this object represents.
+              Servers may infer this from the endpoint the client submits requests to.
+              Cannot be updated.
+              In CamelCase.
+              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
+            type: string
+          metadata:
+            type: object
+          spec:
+            properties:
+              robotAccount:
+                description: Name of the robot account you are federating with
+                type: string
+              serviceAccountRef:
+                description: Name of the service account you are federating with
+                properties:
+                  audiences:
+                    description: |-
+                      Audience specifies the `aud` claim for the service account token
+                      If the service account uses a well-known annotation for e.g. IRSA or GCP Workload Identity
+                      then this audiences will be appended to the list
+                    items:
+                      type: string
+                    type: array
+                  name:
+                    description: The name of the ServiceAccount 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: |-
+                      Namespace of the 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
+                required:
+                - name
+                type: object
+              url:
+                description: URL configures the Quay instance URL. Defaults to quay.io.
+                type: string
+            required:
+            - robotAccount
+            - serviceAccountRef
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}

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

@@ -14,6 +14,7 @@ resources:
   - generators.external-secrets.io_gcraccesstokens.yaml
   - generators.external-secrets.io_githubaccesstokens.yaml
   - generators.external-secrets.io_passwords.yaml
+  - generators.external-secrets.io_quayaccesstokens.yaml
   - generators.external-secrets.io_stssessiontokens.yaml
   - generators.external-secrets.io_uuids.yaml
   - generators.external-secrets.io_vaultdynamicsecrets.yaml

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

@@ -56,6 +56,7 @@ rules:
     - "fakes"
     - "gcraccesstokens"
     - "githubaccesstokens"
+    - "quayaccesstokens"
     - "passwords"
     - "stssessiontokens"
     - "uuids"
@@ -153,6 +154,7 @@ rules:
     - "fakes"
     - "gcraccesstokens"
     - "githubaccesstokens"
+    - "quayaccesstokens"
     - "passwords"
     - "vaultdynamicsecrets"
     - "webhooks"
@@ -199,6 +201,7 @@ rules:
     - "fakes"
     - "gcraccesstokens"
     - "githubaccesstokens"
+    - "quayaccesstokens"
     - "passwords"
     - "vaultdynamicsecrets"
     - "webhooks"

+ 144 - 0
deploy/crds/bundle.yaml

@@ -155,6 +155,7 @@ spec:
                                       - Fake
                                       - GCRAccessToken
                                       - GithubAccessToken
+                                      - QuayAccessToken
                                       - Password
                                       - STSSessionToken
                                       - UUID
@@ -341,6 +342,7 @@ spec:
                                       - Fake
                                       - GCRAccessToken
                                       - GithubAccessToken
+                                      - QuayAccessToken
                                       - Password
                                       - STSSessionToken
                                       - UUID
@@ -6939,6 +6941,7 @@ spec:
                                   - Fake
                                   - GCRAccessToken
                                   - GithubAccessToken
+                                  - QuayAccessToken
                                   - Password
                                   - STSSessionToken
                                   - UUID
@@ -7125,6 +7128,7 @@ spec:
                                   - Fake
                                   - GCRAccessToken
                                   - GithubAccessToken
+                                  - QuayAccessToken
                                   - Password
                                   - STSSessionToken
                                   - UUID
@@ -7593,6 +7597,7 @@ spec:
                             - Fake
                             - GCRAccessToken
                             - GithubAccessToken
+                            - QuayAccessToken
                             - Password
                             - STSSessionToken
                             - UUID
@@ -14379,6 +14384,46 @@ spec:
                         - length
                         - noUpper
                       type: object
+                    quayAccessTokenSpec:
+                      properties:
+                        robotAccount:
+                          description: Name of the robot account you are federating with
+                          type: string
+                        serviceAccountRef:
+                          description: Name of the service account you are federating with
+                          properties:
+                            audiences:
+                              description: |-
+                                Audience specifies the `aud` claim for the service account token
+                                If the service account uses a well-known annotation for e.g. IRSA or GCP Workload Identity
+                                then this audiences will be appended to the list
+                              items:
+                                type: string
+                              type: array
+                            name:
+                              description: The name of the ServiceAccount 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: |-
+                                Namespace of the 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
+                          required:
+                            - name
+                          type: object
+                        url:
+                          description: URL configures the Quay instance URL. Defaults to quay.io.
+                          type: string
+                      required:
+                        - robotAccount
+                        - serviceAccountRef
+                      type: object
                     stsSessionTokenSpec:
                       properties:
                         auth:
@@ -15454,6 +15499,7 @@ spec:
                     - Fake
                     - GCRAccessToken
                     - GithubAccessToken
+                    - QuayAccessToken
                     - Password
                     - STSSessionToken
                     - UUID
@@ -16132,6 +16178,104 @@ metadata:
     controller-gen.kubebuilder.io/version: v0.16.5
   labels:
     external-secrets.io/component: controller
+  name: quayaccesstokens.generators.external-secrets.io
+spec:
+  group: generators.external-secrets.io
+  names:
+    categories:
+      - external-secrets
+      - external-secrets-generators
+    kind: QuayAccessToken
+    listKind: QuayAccessTokenList
+    plural: quayaccesstokens
+    singular: quayaccesstoken
+  scope: Namespaced
+  versions:
+    - name: v1alpha1
+      schema:
+        openAPIV3Schema:
+          description: QuayAccessToken generates Quay oauth token for pulling/pushing images
+          properties:
+            apiVersion:
+              description: |-
+                APIVersion defines the versioned schema of this representation of an object.
+                Servers should convert recognized schemas to the latest internal value, and
+                may reject unrecognized values.
+                More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
+              type: string
+            kind:
+              description: |-
+                Kind is a string value representing the REST resource this object represents.
+                Servers may infer this from the endpoint the client submits requests to.
+                Cannot be updated.
+                In CamelCase.
+                More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
+              type: string
+            metadata:
+              type: object
+            spec:
+              properties:
+                robotAccount:
+                  description: Name of the robot account you are federating with
+                  type: string
+                serviceAccountRef:
+                  description: Name of the service account you are federating with
+                  properties:
+                    audiences:
+                      description: |-
+                        Audience specifies the `aud` claim for the service account token
+                        If the service account uses a well-known annotation for e.g. IRSA or GCP Workload Identity
+                        then this audiences will be appended to the list
+                      items:
+                        type: string
+                      type: array
+                    name:
+                      description: The name of the ServiceAccount 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: |-
+                        Namespace of the 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
+                  required:
+                    - name
+                  type: object
+                url:
+                  description: URL configures the Quay instance URL. Defaults to quay.io.
+                  type: string
+              required:
+                - robotAccount
+                - serviceAccountRef
+              type: object
+          type: object
+      served: true
+      storage: true
+      subresources:
+        status: {}
+  conversion:
+    strategy: Webhook
+    webhook:
+      conversionReviewVersions:
+        - v1
+      clientConfig:
+        service:
+          name: kubernetes
+          namespace: default
+          path: /convert
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.16.5
+  labels:
+    external-secrets.io/component: controller
   name: stssessiontokens.generators.external-secrets.io
 spec:
   group: generators.external-secrets.io

File diff suppressed because it is too large
+ 39 - 0
docs/api/generator/quay.md


+ 29 - 0
docs/snippets/generator-quay-example.yaml

@@ -0,0 +1,29 @@
+{% raw %}
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+  name: quay-credentials
+  namespace: default
+spec:
+  dataFrom:
+    - sourceRef:
+        generatorRef:
+          apiVersion: generators.external-secrets.io/v1alpha1
+          kind: QuayAccessToken
+          name: my-quay-token
+  refreshInterval: 55m # Tokens are good for 1 hour
+  target:
+    name: quay-credentials
+    template:
+      type: kubernetes.io/dockerconfigjson
+      data:
+        .dockerconfigjson: |
+          {
+            "auths": {
+              "{{ .registry }}": {
+                "auth": "{{ .auth }}"
+              }
+            }
+          }
+
+{% endraw %}

+ 11 - 0
docs/snippets/generator-quay.yaml

@@ -0,0 +1,11 @@
+apiVersion: generators.external-secrets.io/v1alpha1
+kind: QuayAccessToken
+metadata:
+  name: my-quay-token
+  namespace: default
+spec:
+  url: "quay.io"
+  robotAccount: "quay_user_or_org+robot_account_name"
+  serviceAccountRef:
+    name: "default"
+    namespace: "default"

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

@@ -71,6 +71,7 @@ nav:
       - AWS STS Session Token: api/generator/sts.md
       - Cluster Generator: api/generator/cluster.md
       - Google Container Registry: api/generator/gcr.md
+      - Quay: api/generator/quay.md
       - Vault Dynamic Secret: api/generator/vault.md
       - Password: api/generator/password.md
       - Fake: api/generator/fake.md

+ 211 - 0
pkg/generator/quay/quay.go

@@ -0,0 +1,211 @@
+/*
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package quay
+
+import (
+	"context"
+	b64 "encoding/base64"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"net/http"
+	"strconv"
+	"strings"
+	"time"
+
+	authv1 "k8s.io/api/authentication/v1"
+	apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/kubernetes"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	ctrlcfg "sigs.k8s.io/controller-runtime/pkg/client/config"
+	"sigs.k8s.io/yaml"
+
+	genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1"
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+)
+
+type Generator struct {
+	httpClient *http.Client
+}
+
+const (
+	defaultQuayURL = "quay.io"
+
+	errNoSpec    = "no config spec provided"
+	errParseSpec = "unable to parse spec: %w"
+	errGetToken  = "unable to get authorization token: %w"
+
+	httpClientTimeout = 5 * time.Second
+)
+
+func (g *Generator) Generate(ctx context.Context, jsonSpec *apiextensions.JSON, kube client.Client, namespace string) (map[string][]byte, error) {
+	return g.generate(
+		ctx,
+		jsonSpec,
+		kube,
+		namespace,
+	)
+}
+
+func (g *Generator) generate(
+	ctx context.Context,
+	jsonSpec *apiextensions.JSON,
+	_ client.Client,
+	namespace string) (map[string][]byte, error) {
+	if jsonSpec == nil {
+		return nil, errors.New(errNoSpec)
+	}
+	res, err := parseSpec(jsonSpec.Raw)
+	if err != nil {
+		return nil, fmt.Errorf(errParseSpec, err)
+	}
+
+	// Fetch the service account token
+	token, err := fetchServiceAccountToken(ctx, res.Spec.ServiceAccountRef, namespace)
+	if err != nil {
+		return nil, fmt.Errorf("failed to fetch service account token: %w", err)
+	}
+	url := res.Spec.URL
+	if url == "" {
+		url = defaultQuayURL
+	}
+	url = strings.TrimPrefix(url, "https://")
+
+	accessToken, err := getQuayRobotToken(ctx, token, res.Spec.RobotAccount, url, g.httpClient)
+	if err != nil {
+		return nil, err
+	}
+	exp, err := tokenExpiration(accessToken)
+	if err != nil {
+		return nil, err
+	}
+	return map[string][]byte{
+		"registry": []byte(url),
+		"auth":     []byte(b64.StdEncoding.EncodeToString([]byte(res.Spec.RobotAccount + ":" + accessToken))),
+		"expiry":   []byte(exp),
+	}, nil
+}
+
+func getClaims(tokenString string) (map[string]interface{}, error) {
+	// Split the token into its three parts
+	parts := strings.Split(tokenString, ".")
+	if len(parts) != 3 {
+		return nil, fmt.Errorf("invalid token format")
+	}
+
+	// Decode the payload (the second part of the token)
+	payload, err := b64.RawURLEncoding.DecodeString(parts[1])
+	if err != nil {
+		return nil, fmt.Errorf("error decoding payload: %w", err)
+	}
+
+	var claims map[string]interface{}
+	if err := json.Unmarshal(payload, &claims); err != nil {
+		return nil, fmt.Errorf("error un-marshaling claims: %w", err)
+	}
+	return claims, nil
+}
+
+func tokenExpiration(tokenString string) (string, error) {
+	claims, err := getClaims(tokenString)
+	if err != nil {
+		return "", fmt.Errorf("error getting claims: %w", err)
+	}
+	exp, ok := claims["exp"].(float64)
+	if ok {
+		return strconv.FormatFloat(exp, 'f', -1, 64), nil
+	}
+
+	return "", fmt.Errorf("exp claim not found or wrong type")
+}
+
+// https://docs.projectquay.io/manage_quay.html#exchanging-oauth2-robot-account-token
+func getQuayRobotToken(ctx context.Context, fedToken, robotAccount, url string, hc *http.Client) (string, error) {
+	if hc == nil {
+		hc = &http.Client{
+			Timeout: httpClientTimeout,
+		}
+	}
+
+	req, err := http.NewRequestWithContext(ctx, "GET", "https://"+url+"/oauth2/federation/robot/token", http.NoBody)
+	if err != nil {
+		return "", err
+	}
+	req.SetBasicAuth(robotAccount, fedToken)
+	resp, err := hc.Do(req)
+	if err != nil {
+		return "", err
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != 200 {
+		return "", fmt.Errorf("request failed do to unexpected status: %s", resp.Status)
+	}
+
+	body, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return "", err
+	}
+
+	var result map[string]interface{}
+
+	err = json.Unmarshal(body, &result)
+	if err != nil {
+		return "", err
+	}
+	token, ok := result["token"]
+	if !ok {
+		return "", fmt.Errorf("token not found in response")
+	}
+	tokenString, ok := token.(string)
+	if !ok {
+		return "", fmt.Errorf("error when typecasting token to string")
+	}
+	return tokenString, nil
+}
+
+func fetchServiceAccountToken(ctx context.Context, saRef esmeta.ServiceAccountSelector, namespace string) (string, error) {
+	cfg, err := ctrlcfg.GetConfig()
+	if err != nil {
+		return "", err
+	}
+	kubeClient, err := kubernetes.NewForConfig(cfg)
+	if err != nil {
+		return "", fmt.Errorf("failed to create kubernetes client: %w", err)
+	}
+
+	tokenRequest := &authv1.TokenRequest{
+		Spec: authv1.TokenRequestSpec{
+			Audiences: saRef.Audiences,
+		},
+	}
+	tokenResponse, err := kubeClient.CoreV1().ServiceAccounts(namespace).CreateToken(ctx, saRef.Name, tokenRequest, metav1.CreateOptions{})
+	if err != nil {
+		return "", fmt.Errorf("failed to create token: %w", err)
+	}
+	return tokenResponse.Status.Token, nil
+}
+
+func parseSpec(data []byte) (*genv1alpha1.QuayAccessToken, error) {
+	var spec genv1alpha1.QuayAccessToken
+	err := yaml.Unmarshal(data, &spec)
+	return &spec, err
+}
+
+func init() {
+	genv1alpha1.Register(genv1alpha1.QuayAccessTokenKind, &Generator{})
+}

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

@@ -23,6 +23,7 @@ import (
 	_ "github.com/external-secrets/external-secrets/pkg/generator/gcr"
 	_ "github.com/external-secrets/external-secrets/pkg/generator/github"
 	_ "github.com/external-secrets/external-secrets/pkg/generator/password"
+	_ "github.com/external-secrets/external-secrets/pkg/generator/quay"
 	_ "github.com/external-secrets/external-secrets/pkg/generator/sts"
 	_ "github.com/external-secrets/external-secrets/pkg/generator/uuid"
 	_ "github.com/external-secrets/external-secrets/pkg/generator/vault"

+ 7 - 0
pkg/utils/resolvers/generator.go

@@ -173,6 +173,13 @@ func clusterGeneratorToVirtual(gen *genv1alpha1.ClusterGenerator) (client.Object
 		return &genv1alpha1.GithubAccessToken{
 			Spec: *gen.Spec.Generator.GithubAccessTokenSpec,
 		}, 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)
+		}
+		return &genv1alpha1.QuayAccessToken{
+			Spec: *gen.Spec.Generator.QuayAccessTokenSpec,
+		}, nil
 	case genv1alpha1.GeneratorKindPassword:
 		if gen.Spec.Generator.PasswordSpec == nil {
 			return nil, fmt.Errorf("when kind is %s, PasswordSpec must be set", gen.Spec.Kind)