mirror of
https://github.com/external-secrets/external-secrets.git
synced 2024-12-14 11:57:59 +00:00
* 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>
This commit is contained in:
parent
ecbbdf4b5b
commit
84731616f4
13 changed files with 764 additions and 2 deletions
59
apis/generators/v1alpha1/generator_github.go
Normal file
59
apis/generators/v1alpha1/generator_github.go
Normal file
|
@ -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"`
|
||||
}
|
|
@ -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{})
|
||||
|
|
|
@ -566,6 +566,112 @@ func (in *GCRAccessTokenSpec) DeepCopy() *GCRAccessTokenSpec {
|
|||
return out
|
||||
}
|
||||
|
||||
// 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
|
||||
|
|
|
@ -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: {}
|
|
@ -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:
|
||||
|
|
|
@ -10512,6 +10512,107 @@ spec:
|
|||
---
|
||||
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: {}
|
||||
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
|
||||
|
|
14
docs/snippets/generator-github-example.yaml
Normal file
14
docs/snippets/generator-github-example.yaml
Normal file
|
@ -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
docs/snippets/generator-github.yaml
Normal file
20
docs/snippets/generator-github.yaml
Normal file
|
@ -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
|
|
@ -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
pkg/generator/github/github.go
Normal file
169
pkg/generator/github/github.go
Normal file
|
@ -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
pkg/generator/github/github_test.go
Normal file
138
pkg/generator/github/github_test.go
Normal file
|
@ -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
pkg/generator/github/github_test.pem
Normal file
51
pkg/generator/github/github_test.pem
Normal file
|
@ -0,0 +1,51 @@
|
|||
-----BEGIN RSA PRIVATE KEY-----
|
||||
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
|
||||
-----END RSA PRIVATE KEY-----
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue