Browse Source

GitHub provider (supersedes #3014) (#3115)

* github provider signed, supersedes #3014

Signed-off-by: Mike Serchenia <michael_serchenia@epam.com>

* tests pass, + crd + docs

Signed-off-by: Mike Serchenia <michael_serchenia@epam.com>

* fix sonarLint alert

Signed-off-by: Mike Serchenia <michael_serchenia@epam.com>

* refactoring, replace secretStore with generator

Signed-off-by: Mike Serchenia <michael_serchenia@epam.com>

* cosmetics + tst + lint pass

Signed-off-by: Mike Serchenia <michael_serchenia@epam.com>

* docs

Signed-off-by: Mike Serchenia <michael_serchenia@epam.com>

* clean-up + lint + test

Signed-off-by: Mike Serchenia <michael_serchenia@epam.com>

* small refactor, fix issues left in comments

Signed-off-by: Mike Serchenia <michael_serchenia@epam.com>

---------

Signed-off-by: Mike Serchenia <michael_serchenia@epam.com>
Michael Serchenia 2 years ago
parent
commit
84731616f4

+ 59 - 0
apis/generators/v1alpha1/generator_github.go

@@ -0,0 +1,59 @@
+/*
+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 GithubAccessTokenSpec struct {
+	// URL configures the Github instance URL. Defaults to https://github.com/.
+	URL       string `json:"url,omitempty"`
+	AppID     string `json:"appID"`
+	InstallID string `json:"installID"`
+	// Auth configures how ESO authenticates with a Github instance.
+	Auth GithubAuth `json:"auth"`
+}
+
+type GithubAuth struct {
+	PrivatKey GithubSecretRef `json:"privatKey"`
+}
+
+type GithubSecretRef struct {
+	SecretRef esmeta.SecretKeySelector `json:"secretRef"`
+}
+
+// GithubAccessToken generates ghs_ accessToken
+// +kubebuilder:object:root=true
+// +kubebuilder:storageversion
+// +kubebuilder:subresource:status
+// +kubebuilder:resource:scope=Namespaced,categories={githubaccesstoken},shortName=githubaccesstoken
+type GithubAccessToken struct {
+	metav1.TypeMeta   `json:",inline"`
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+
+	Spec GithubAccessTokenSpec `json:"spec,omitempty"`
+}
+
+// +kubebuilder:object:root=true
+
+// GithubAccessToken contains a list of ExternalSecret resources.
+type GithubAccessTokenList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata,omitempty"`
+	Items           []GithubAccessToken `json:"items"`
+}

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

@@ -92,9 +92,18 @@ var (
 	VaultDynamicSecretGroupVersionKind = SchemeGroupVersion.WithKind(VaultDynamicSecretKind)
 )
 
+// GithubAccessToken type metadata.
+var (
+	GithubAccessTokenKind             = reflect.TypeOf(GithubAccessToken{}).Name()
+	GithubAccessTokenGroupKind        = schema.GroupKind{Group: Group, Kind: GithubAccessTokenKind}.String()
+	GithubAccessTokenKindAPIVersion   = GithubAccessTokenKind + "." + SchemeGroupVersion.String()
+	GithubAccessTokenGroupVersionKind = SchemeGroupVersion.WithKind(GithubAccessTokenKind)
+)
+
 func init() {
 	SchemeBuilder.Register(&ECRAuthorizationToken{}, &ECRAuthorizationToken{})
 	SchemeBuilder.Register(&GCRAccessToken{}, &GCRAccessTokenList{})
+	SchemeBuilder.Register(&GithubAccessToken{}, &GithubAccessTokenList{})
 	SchemeBuilder.Register(&ACRAccessToken{}, &ACRAccessTokenList{})
 	SchemeBuilder.Register(&Fake{}, &FakeList{})
 	SchemeBuilder.Register(&VaultDynamicSecret{}, &VaultDynamicSecretList{})

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

@@ -567,6 +567,112 @@ func (in *GCRAccessTokenSpec) DeepCopy() *GCRAccessTokenSpec {
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *GithubAccessToken) DeepCopyInto(out *GithubAccessToken) {
+	*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 GithubAccessToken.
+func (in *GithubAccessToken) DeepCopy() *GithubAccessToken {
+	if in == nil {
+		return nil
+	}
+	out := new(GithubAccessToken)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *GithubAccessToken) 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 *GithubAccessTokenList) DeepCopyInto(out *GithubAccessTokenList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]GithubAccessToken, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GithubAccessTokenList.
+func (in *GithubAccessTokenList) DeepCopy() *GithubAccessTokenList {
+	if in == nil {
+		return nil
+	}
+	out := new(GithubAccessTokenList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *GithubAccessTokenList) 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 *GithubAccessTokenSpec) DeepCopyInto(out *GithubAccessTokenSpec) {
+	*out = *in
+	in.Auth.DeepCopyInto(&out.Auth)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GithubAccessTokenSpec.
+func (in *GithubAccessTokenSpec) DeepCopy() *GithubAccessTokenSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(GithubAccessTokenSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *GithubAuth) DeepCopyInto(out *GithubAuth) {
+	*out = *in
+	in.PrivatKey.DeepCopyInto(&out.PrivatKey)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GithubAuth.
+func (in *GithubAuth) DeepCopy() *GithubAuth {
+	if in == nil {
+		return nil
+	}
+	out := new(GithubAuth)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *GithubSecretRef) DeepCopyInto(out *GithubSecretRef) {
+	*out = *in
+	in.SecretRef.DeepCopyInto(&out.SecretRef)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GithubSecretRef.
+func (in *GithubSecretRef) DeepCopy() *GithubSecretRef {
+	if in == nil {
+		return nil
+	}
+	out := new(GithubSecretRef)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *Password) DeepCopyInto(out *Password) {
 	*out = *in
 	out.TypeMeta = in.TypeMeta

+ 91 - 0
config/crds/bases/generators.external-secrets.io_githubaccesstokens.yaml

@@ -0,0 +1,91 @@
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.14.0
+  name: githubaccesstokens.generators.external-secrets.io
+spec:
+  group: generators.external-secrets.io
+  names:
+    categories:
+    - githubaccesstoken
+    kind: GithubAccessToken
+    listKind: GithubAccessTokenList
+    plural: githubaccesstokens
+    shortNames:
+    - githubaccesstoken
+    singular: githubaccesstoken
+  scope: Namespaced
+  versions:
+  - name: v1alpha1
+    schema:
+      openAPIV3Schema:
+        description: GithubAccessToken generates ghs_ accessToken
+        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:
+              appID:
+                type: string
+              auth:
+                description: Auth configures how ESO authenticates with a Github instance.
+                properties:
+                  privatKey:
+                    properties:
+                      secretRef:
+                        description: |-
+                          A reference to a specific 'key' within a Secret resource,
+                          In some instances, `key` is a required field.
+                        properties:
+                          key:
+                            description: |-
+                              The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be
+                              defaulted, in others it may be required.
+                            type: string
+                          name:
+                            description: The name of the Secret resource being referred
+                              to.
+                            type: string
+                          namespace:
+                            description: |-
+                              Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults
+                              to the namespace of the referent.
+                            type: string
+                        type: object
+                    required:
+                    - secretRef
+                    type: object
+                required:
+                - privatKey
+                type: object
+              installID:
+                type: string
+              url:
+                description: URL configures the Github instance URL. Defaults to https://github.com/.
+                type: string
+            required:
+            - appID
+            - auth
+            - installID
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}

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

@@ -53,6 +53,7 @@ rules:
     - "ecrauthorizationtokens"
     - "fakes"
     - "gcraccesstokens"
+    - "githubaccesstokens"
     - "passwords"
     - "vaultdynamicsecrets"
     verbs:
@@ -145,6 +146,7 @@ rules:
     - "ecrauthorizationtokens"
     - "fakes"
     - "gcraccesstokens"
+    - "githubaccesstokens"
     - "passwords"
     - "vaultdynamicsecrets"
     verbs:
@@ -188,6 +190,7 @@ rules:
     - "ecrauthorizationtokens"
     - "fakes"
     - "gcraccesstokens"
+    - "githubaccesstokens"
     - "passwords"
     - "vaultdynamicsecrets"
     verbs:

+ 101 - 0
deploy/crds/bundle.yaml

@@ -10515,6 +10515,107 @@ kind: CustomResourceDefinition
 metadata:
   annotations:
     controller-gen.kubebuilder.io/version: v0.14.0
+  name: githubaccesstokens.generators.external-secrets.io
+spec:
+  group: generators.external-secrets.io
+  names:
+    categories:
+      - githubaccesstoken
+    kind: GithubAccessToken
+    listKind: GithubAccessTokenList
+    plural: githubaccesstokens
+    shortNames:
+      - githubaccesstoken
+    singular: githubaccesstoken
+  scope: Namespaced
+  versions:
+    - name: v1alpha1
+      schema:
+        openAPIV3Schema:
+          description: GithubAccessToken generates ghs_ accessToken
+          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:
+                appID:
+                  type: string
+                auth:
+                  description: Auth configures how ESO authenticates with a Github instance.
+                  properties:
+                    privatKey:
+                      properties:
+                        secretRef:
+                          description: |-
+                            A reference to a specific 'key' within a Secret resource,
+                            In some instances, `key` is a required field.
+                          properties:
+                            key:
+                              description: |-
+                                The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be
+                                defaulted, in others it may be required.
+                              type: string
+                            name:
+                              description: The name of the Secret resource being referred to.
+                              type: string
+                            namespace:
+                              description: |-
+                                Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults
+                                to the namespace of the referent.
+                              type: string
+                          type: object
+                      required:
+                        - secretRef
+                      type: object
+                  required:
+                    - privatKey
+                  type: object
+                installID:
+                  type: string
+                url:
+                  description: URL configures the Github instance URL. Defaults to https://github.com/.
+                  type: string
+              required:
+                - appID
+                - auth
+                - installID
+              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.14.0
   name: passwords.generators.external-secrets.io
 spec:
   group: generators.external-secrets.io

+ 14 - 0
docs/snippets/generator-github-example.yaml

@@ -0,0 +1,14 @@
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+  name: github-auth-token
+spec:
+  refreshInterval: "30m"
+  target:
+    name: github-auth-token # Name for the secret to be created on the cluster
+  dataFrom:
+  - sourceRef:
+      generatorRef:
+        apiVersion: generators.external-secrets.io/v1alpha1
+        kind: GithubAccessToken
+        name: github-auth-token

+ 20 - 0
docs/snippets/generator-github.yaml

@@ -0,0 +1,20 @@
+# 1. Register Github app https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app#registering-a-github-app
+#   `App ID: 123456` will be displayed after you create an app. Next on the bottom of the page, you'll find `Generate a private key` button.
+# 2. Get privateKey https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/managing-private-keys-for-github-apps#generating-private-keys put it in e.g `github-app-pem` k8s secret
+# 3. Set permissions for the app, e.g if you want to push OCI images to ghr set RW for packages https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/choosing-permissions-for-a-github-app#choosing-permissions-for-rest-api-access
+# 4. Install your Github app https://docs.github.com/en/apps/using-github-apps/installing-your-own-github-app
+# 5. Get `installID` https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-an-installation-access-token-for-a-github-app#generating-an-installation-access-token (2)
+---
+apiVersion: generators.external-secrets.io/v1alpha1
+kind: GithubAccessToken
+metadata:
+  name: github-auth-token
+spec:
+  appID: "0000000" # (1)
+  installID: "00000000" # (5)
+  url: "" # (Default https://api.github.com.)
+  auth:
+    privatKey:
+      secretRef:
+        name: github-app-pem # (2)
+        key: key

+ 2 - 2
docs/snippets/getallsecrets-find-by-name.yaml

@@ -1,4 +1,4 @@
-apiVersion: external-secrets.io/v1beta1 
+apiVersion: external-secrets.io/v1beta1
 kind: ExternalSecret
 metadata:
   name: find-by-tags
@@ -12,4 +12,4 @@ spec:
   dataFrom:
   - find:
       name:
-        regexp: "key"
+        regexp: "key"

+ 169 - 0
pkg/generator/github/github.go

@@ -0,0 +1,169 @@
+/*
+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 github
+
+import (
+	"context"
+	"crypto/rsa"
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"time"
+
+	"github.com/golang-jwt/jwt/v5"
+	corev1 "k8s.io/api/core/v1"
+	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"
+)
+
+type Generator struct {
+	httpClient *http.Client
+}
+
+type Github struct {
+	HTTP       *http.Client
+	Kube       client.Client
+	Namespace  string
+	URL        string
+	InstallTkn string
+}
+
+const (
+	defaultLoginUsername = "token"
+	defaultGithubAPI     = "https://api.github.com"
+
+	errNoSpec    = "no config spec provided"
+	errParseSpec = "unable to parse spec: %w"
+	errGetToken  = "unable to get authorization token: %w"
+
+	contextTimeout    = 30 * time.Second
+	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,
+	kube client.Client,
+	namespace string) (map[string][]byte, error) {
+	if jsonSpec == nil {
+		return nil, fmt.Errorf(errNoSpec)
+	}
+	ctx, cancel := context.WithTimeout(ctx, contextTimeout)
+	defer cancel()
+
+	gh, err := newGHClient(ctx, kube, namespace, g.httpClient, jsonSpec)
+	if err != nil {
+		return nil, fmt.Errorf("error creating request: %w", err)
+	}
+	// Github api expects POST request
+	req, err := http.NewRequestWithContext(ctx, http.MethodPost, gh.URL, http.NoBody)
+	if err != nil {
+		return nil, fmt.Errorf("error creating request: %w", err)
+	}
+	req.Header.Add("Authorization", "Bearer "+gh.InstallTkn)
+	req.Header.Add("Accept", "application/vnd.github.v3+json")
+
+	resp, err := gh.HTTP.Do(req)
+	if err != nil {
+		return nil, fmt.Errorf("error performing request: %w", err)
+	}
+	defer resp.Body.Close()
+
+	// git access token
+	var gat map[string]interface{}
+	if err := json.NewDecoder(resp.Body).Decode(&gat); err != nil && resp.StatusCode >= 200 && resp.StatusCode < 300 {
+		return nil, fmt.Errorf("error decoding response: %w", err)
+	}
+
+	accessToken, ok := gat["token"].(string)
+	if !ok {
+		return nil, fmt.Errorf("token isn't a string or token key doesn't exist")
+	}
+	return map[string][]byte{
+		defaultLoginUsername: []byte(accessToken),
+	}, nil
+}
+
+func newGHClient(ctx context.Context, k client.Client, n string, hc *http.Client,
+	js *apiextensions.JSON) (*Github, error) {
+	if hc == nil {
+		hc = &http.Client{
+			Timeout: httpClientTimeout,
+		}
+	}
+	res, err := parseSpec(js.Raw)
+	if err != nil {
+		return nil, fmt.Errorf(errParseSpec, err)
+	}
+	gh := &Github{Kube: k, Namespace: n, HTTP: hc}
+
+	ghPath := fmt.Sprintf("/app/installations/%s/access_tokens", res.Spec.InstallID)
+	gh.URL = defaultGithubAPI + ghPath
+	if res.Spec.URL != "" {
+		gh.URL = res.Spec.URL + ghPath
+	}
+	secret := &corev1.Secret{}
+	if err := gh.Kube.Get(ctx, client.ObjectKey{Name: res.Spec.Auth.PrivatKey.SecretRef.Name, Namespace: n}, secret); err != nil {
+		return nil, fmt.Errorf("error getting GH pem from secret:%w", err)
+	}
+
+	pk, err := jwt.ParseRSAPrivateKeyFromPEM(secret.Data[res.Spec.Auth.PrivatKey.SecretRef.Key])
+	if err != nil {
+		return nil, fmt.Errorf("error parsing RSA private key: %w", err)
+	}
+	if gh.InstallTkn, err = GetInstallationToken(pk, res.Spec.AppID); err != nil {
+		return nil, fmt.Errorf("can't get InstallationToken: %w", err)
+	}
+	return gh, nil
+}
+
+// Get github installation token.
+func GetInstallationToken(key *rsa.PrivateKey, aid string) (string, error) {
+	claims := jwt.RegisteredClaims{
+		Issuer:    aid,
+		IssuedAt:  jwt.NewNumericDate(time.Now().Add(-time.Second * 10)),
+		ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Second * 300)),
+	}
+
+	token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
+	signedToken, err := token.SignedString(key)
+	if err != nil {
+		return "", fmt.Errorf("error signing token: %w", err)
+	}
+
+	return signedToken, nil
+}
+
+func parseSpec(data []byte) (*genv1alpha1.GithubAccessToken, error) {
+	var spec genv1alpha1.GithubAccessToken
+	err := yaml.Unmarshal(data, &spec)
+	return &spec, err
+}
+
+func init() {
+	genv1alpha1.Register(genv1alpha1.GithubAccessTokenKind, &Generator{})
+}

+ 138 - 0
pkg/generator/github/github_test.go

@@ -0,0 +1,138 @@
+/*
+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 github
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"net/http/httptest"
+	"os"
+	"reflect"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	v1 "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"
+)
+
+const (
+	tstCrtName = "github_test.pem"
+)
+
+func testHTTPSrv(t *testing.T, r []byte) *httptest.Server {
+	return httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+		assert.Equal(t, "POST", req.Method, "Expected POST request")
+		assert.Empty(t, req.Body)
+		assert.NotEmpty(t, req.Header.Get("Authorization"))
+		assert.Equal(t, "application/vnd.github.v3+json", req.Header.Get("Accept"))
+
+		// Send response to be tested
+		rw.Write(r)
+	}))
+}
+func TestGenerate(t *testing.T) {
+	type args struct {
+		ctx       context.Context
+		jsonSpec  *apiextensions.JSON
+		kube      client.Client
+		namespace string
+	}
+	pem, err := os.ReadFile(tstCrtName)
+	assert.NoError(t, err, "Should not error when reading privatKey")
+
+	validResponce := []byte(`{
+		"token": "ghs_16C7e42F292c6912E7710c838347Ae178B4a",
+		"expires_at": "2016-07-11T22:14:10Z",
+		"permissions": {
+		  "issues": "write",
+		  "contents": "read"
+		},
+		"repository_selection": "selected"
+	  }`)
+
+	server := testHTTPSrv(t, validResponce)
+
+	tests := []struct {
+		name    string
+		g       *Generator
+		args    args
+		want    map[string][]byte
+		wantErr bool
+	}{
+		{
+			name: "nil spec",
+			args: args{
+				jsonSpec: nil,
+			},
+			wantErr: true,
+		},
+		{
+			name: "full spec",
+			args: args{
+				ctx:       context.TODO(),
+				namespace: "foo",
+				kube: clientfake.NewClientBuilder().WithObjects(&v1.Secret{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "testName",
+						Namespace: "foo",
+					},
+					Data: map[string][]byte{
+						"privatKey": pem,
+					},
+				}).Build(),
+				jsonSpec: &apiextensions.JSON{
+					Raw: []byte(fmt.Sprintf(`apiVersion: generators.external-secrets.io/v1alpha1
+kind: GithubToken
+spec:
+  appID: "0000000"
+  installID: "00000000"
+  URL: %q
+  auth:
+    privatKey:
+      secretRef:
+        name: "testName"
+        namespace: "foo"
+        key: "privatKey"`, server.URL)),
+				},
+			},
+			want: map[string][]byte{
+				"token": []byte("ghs_16C7e42F292c6912E7710c838347Ae178B4a"),
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			g := &Generator{httpClient: server.Client()}
+			got, err := g.generate(
+				tt.args.ctx,
+				tt.args.jsonSpec,
+				tt.args.kube,
+				tt.args.namespace,
+			)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("Generator.Generate() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("Generator.Generate() = %s, want %s", got, tt.want)
+			}
+		})
+	}
+}

+ 51 - 0
pkg/generator/github/github_test.pem

@@ -0,0 +1,51 @@
+-----BEGIN RSA PRIVATE KEY-----
+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
+-----END RSA PRIVATE KEY-----

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

@@ -22,6 +22,7 @@ import (
 	_ "github.com/external-secrets/external-secrets/pkg/generator/ecr"
 	_ "github.com/external-secrets/external-secrets/pkg/generator/fake"
 	_ "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/vault"
 	_ "github.com/external-secrets/external-secrets/pkg/generator/webhook"