From 1e2d0919f739e3e9fbc8f9fe65568ef7bfdf36cf Mon Sep 17 00:00:00 2001 From: Nic Cope Date: Tue, 14 Sep 2021 06:24:33 +0000 Subject: [PATCH 1/2] Support GKE OAuth Authorization This commit adds an optional set of 'identity' credentials to the Helm provider config. Currently only the GoogleApplicationCredentials type of identity is supported. If supplied, the kubeconfig (fetched from the regular 'credentials') is used to create an API Server REST client, which is wrapped such that it will use a Google Application Credentials file to fetch OAuth tokens which are then used as the Bearer token to authorize requests. Signed-off-by: Nic Cope --- apis/v1alpha1/types.go | 26 ++- apis/v1alpha1/zz_generated.deepcopy.go | 21 +++ apis/v1beta1/types.go | 26 ++- apis/v1beta1/zz_generated.deepcopy.go | 21 +++ .../provider-config-with-secret.yaml | 11 +- go.mod | 1 + .../helm.crossplane.io_providerconfigs.yaml | 128 +++++++++++++- pkg/clients/client.go | 4 +- pkg/clients/gke/gke.go | 50 ++++++ pkg/controller/release/release.go | 100 ++++++----- pkg/controller/release/release_test.go | 158 ++++++++++++------ 11 files changed, 442 insertions(+), 104 deletions(-) create mode 100644 pkg/clients/gke/gke.go diff --git a/apis/v1alpha1/types.go b/apis/v1alpha1/types.go index fe2b8cd..919089b 100644 --- a/apis/v1alpha1/types.go +++ b/apis/v1alpha1/types.go @@ -24,8 +24,15 @@ import ( // A ProviderConfigSpec defines the desired state of a Provider. type ProviderConfigSpec struct { - // Credentials required to authenticate to this provider. + // Credentials used to connect to the Kubernetes API. Typically a + // kubeconfig file. Use InjectedIdentity for in-cluster config. Credentials ProviderCredentials `json:"credentials"` + + // Identity used to authenticate to the Kubernetes API. The identity + // credentials can be used to supplement kubeconfig 'credentials', for + // example by configuring a bearer token source such as OAuth. + // +optional + Identity *Identity `json:"identity,omitempty"` } // ProviderCredentials required to authenticate. @@ -37,6 +44,23 @@ type ProviderCredentials struct { xpv1.CommonCredentialSelectors `json:",inline"` } +// IdentityType used to authenticate to the Kubernetes API. +type IdentityType string + +// Supported identity types. +const ( + IdentityTypeGoogleApplicationCredentials = "GoogleApplicationCredentials" +) + +// Identity used to authenticate. +type Identity struct { + // Type of identity. + // +kubebuilder:validation:Enum=GoogleApplicationCredentials + Type IdentityType `json:"type"` + + ProviderCredentials `json:",inline"` +} + // A ProviderConfigStatus defines the status of a Provider. type ProviderConfigStatus struct { xpv1.ProviderConfigStatus `json:",inline"` diff --git a/apis/v1alpha1/zz_generated.deepcopy.go b/apis/v1alpha1/zz_generated.deepcopy.go index 9bc0ec6..4442f14 100644 --- a/apis/v1alpha1/zz_generated.deepcopy.go +++ b/apis/v1alpha1/zz_generated.deepcopy.go @@ -24,6 +24,22 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Identity) DeepCopyInto(out *Identity) { + *out = *in + in.ProviderCredentials.DeepCopyInto(&out.ProviderCredentials) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Identity. +func (in *Identity) DeepCopy() *Identity { + if in == nil { + return nil + } + out := new(Identity) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ProviderConfig) DeepCopyInto(out *ProviderConfig) { *out = *in @@ -87,6 +103,11 @@ func (in *ProviderConfigList) DeepCopyObject() runtime.Object { func (in *ProviderConfigSpec) DeepCopyInto(out *ProviderConfigSpec) { *out = *in in.Credentials.DeepCopyInto(&out.Credentials) + if in.Identity != nil { + in, out := &in.Identity, &out.Identity + *out = new(Identity) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderConfigSpec. diff --git a/apis/v1beta1/types.go b/apis/v1beta1/types.go index 30aa6c5..426e8ff 100644 --- a/apis/v1beta1/types.go +++ b/apis/v1beta1/types.go @@ -24,8 +24,15 @@ import ( // A ProviderConfigSpec defines the desired state of a Provider. type ProviderConfigSpec struct { - // Credentials required to authenticate to this provider. + // Credentials used to connect to the Kubernetes API. Typically a + // kubeconfig file. Use InjectedIdentity for in-cluster config. Credentials ProviderCredentials `json:"credentials"` + + // Identity used to authenticate to the Kubernetes API. The identity + // credentials can be used to supplement kubeconfig 'credentials', for + // example by configuring a bearer token source such as OAuth. + // +optional + Identity *Identity `json:"identity,omitempty"` } // ProviderCredentials required to authenticate. @@ -37,6 +44,23 @@ type ProviderCredentials struct { xpv1.CommonCredentialSelectors `json:",inline"` } +// IdentityType used to authenticate to the Kubernetes API. +type IdentityType string + +// Supported identity types. +const ( + IdentityTypeGoogleApplicationCredentials = "GoogleApplicationCredentials" +) + +// Identity used to authenticate. +type Identity struct { + // Type of identity. + // +kubebuilder:validation:Enum=GoogleApplicationCredentials + Type IdentityType `json:"type"` + + ProviderCredentials `json:",inline"` +} + // A ProviderConfigStatus defines the status of a Provider. type ProviderConfigStatus struct { xpv1.ProviderConfigStatus `json:",inline"` diff --git a/apis/v1beta1/zz_generated.deepcopy.go b/apis/v1beta1/zz_generated.deepcopy.go index e2fec11..2683ff9 100644 --- a/apis/v1beta1/zz_generated.deepcopy.go +++ b/apis/v1beta1/zz_generated.deepcopy.go @@ -24,6 +24,22 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Identity) DeepCopyInto(out *Identity) { + *out = *in + in.ProviderCredentials.DeepCopyInto(&out.ProviderCredentials) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Identity. +func (in *Identity) DeepCopy() *Identity { + if in == nil { + return nil + } + out := new(Identity) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ProviderConfig) DeepCopyInto(out *ProviderConfig) { *out = *in @@ -87,6 +103,11 @@ func (in *ProviderConfigList) DeepCopyObject() runtime.Object { func (in *ProviderConfigSpec) DeepCopyInto(out *ProviderConfigSpec) { *out = *in in.Credentials.DeepCopyInto(&out.Credentials) + if in.Identity != nil { + in, out := &in.Identity, &out.Identity + *out = new(Identity) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderConfigSpec. diff --git a/examples/provider-config/provider-config-with-secret.yaml b/examples/provider-config/provider-config-with-secret.yaml index 0e20641..c6a2da4 100644 --- a/examples/provider-config/provider-config-with-secret.yaml +++ b/examples/provider-config/provider-config-with-secret.yaml @@ -1,11 +1,18 @@ apiVersion: helm.crossplane.io/v1beta1 kind: ProviderConfig metadata: - name: helm-provider + name: default spec: credentials: source: Secret secretRef: - name: cluster-config + name: cluster-credentials namespace: crossplane-system key: kubeconfig +# identity: +# type: GoogleApplicationCredentials +# source: Secret +# secretRef: +# name: gcp-credentials +# namespace: crossplane-system +# key: credentials.json diff --git a/go.mod b/go.mod index 61529e9..3f020e9 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/crossplane/crossplane-tools v0.0.0-20210320162312-1baca298c527 github.com/google/go-cmp v0.5.6 github.com/pkg/errors v0.9.1 + golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d gopkg.in/alecthomas/kingpin.v2 v2.2.6 helm.sh/helm/v3 v3.6.3 k8s.io/api v0.21.2 diff --git a/package/crds/helm.crossplane.io_providerconfigs.yaml b/package/crds/helm.crossplane.io_providerconfigs.yaml index 7e93d9d..bfe75b4 100644 --- a/package/crds/helm.crossplane.io_providerconfigs.yaml +++ b/package/crds/helm.crossplane.io_providerconfigs.yaml @@ -48,7 +48,8 @@ spec: description: A ProviderConfigSpec defines the desired state of a Provider. properties: credentials: - description: Credentials required to authenticate to this provider. + description: Credentials used to connect to the Kubernetes API. Typically + a kubeconfig file. Use InjectedIdentity for in-cluster config. properties: env: description: Env is a reference to an environment variable that @@ -100,6 +101,67 @@ spec: required: - source type: object + identity: + description: Identity used to authenticate to the Kubernetes API. + The identity credentials can be used to supplement kubeconfig 'credentials', + for example by configuring a bearer token source such as OAuth. + properties: + env: + description: Env is a reference to an environment variable that + contains credentials that must be used to connect to the provider. + properties: + name: + description: Name is the name of an environment variable. + type: string + required: + - name + type: object + fs: + description: Fs is a reference to a filesystem location that contains + credentials that must be used to connect to the provider. + properties: + path: + description: Path is a filesystem path. + type: string + required: + - path + type: object + secretRef: + description: A SecretRef is a reference to a secret key that contains + the credentials that must be used to connect to the provider. + properties: + key: + description: The key to select. + type: string + name: + description: Name of the secret. + type: string + namespace: + description: Namespace of the secret. + type: string + required: + - key + - name + - namespace + type: object + source: + description: Source of the provider credentials. + enum: + - None + - Secret + - InjectedIdentity + - Environment + - Filesystem + type: string + type: + description: Type of identity. + enum: + - GoogleApplicationCredentials + type: string + required: + - source + - type + type: object required: - credentials type: object @@ -181,7 +243,8 @@ spec: description: A ProviderConfigSpec defines the desired state of a Provider. properties: credentials: - description: Credentials required to authenticate to this provider. + description: Credentials used to connect to the Kubernetes API. Typically + a kubeconfig file. Use InjectedIdentity for in-cluster config. properties: env: description: Env is a reference to an environment variable that @@ -233,6 +296,67 @@ spec: required: - source type: object + identity: + description: Identity used to authenticate to the Kubernetes API. + The identity credentials can be used to supplement kubeconfig 'credentials', + for example by configuring a bearer token source such as OAuth. + properties: + env: + description: Env is a reference to an environment variable that + contains credentials that must be used to connect to the provider. + properties: + name: + description: Name is the name of an environment variable. + type: string + required: + - name + type: object + fs: + description: Fs is a reference to a filesystem location that contains + credentials that must be used to connect to the provider. + properties: + path: + description: Path is a filesystem path. + type: string + required: + - path + type: object + secretRef: + description: A SecretRef is a reference to a secret key that contains + the credentials that must be used to connect to the provider. + properties: + key: + description: The key to select. + type: string + name: + description: Name of the secret. + type: string + namespace: + description: Namespace of the secret. + type: string + required: + - key + - name + - namespace + type: object + source: + description: Source of the provider credentials. + enum: + - None + - Secret + - InjectedIdentity + - Environment + - Filesystem + type: string + type: + description: Type of identity. + enum: + - GoogleApplicationCredentials + type: string + required: + - source + - type + type: object required: - credentials type: object diff --git a/pkg/clients/client.go b/pkg/clients/client.go index 2ac4ad8..6306ca1 100644 --- a/pkg/clients/client.go +++ b/pkg/clients/client.go @@ -26,8 +26,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) -// NewRestConfig returns a rest config given a secret with connection information. -func NewRestConfig(kubeconfig []byte) (*rest.Config, error) { +// NewRESTConfig returns a REST config given a secret with connection information. +func NewRESTConfig(kubeconfig []byte) (*rest.Config, error) { ac, err := clientcmd.Load(kubeconfig) if err != nil { return nil, errors.Wrap(err, "failed to load kubeconfig") diff --git a/pkg/clients/gke/gke.go b/pkg/clients/gke/gke.go new file mode 100644 index 0000000..93c5107 --- /dev/null +++ b/pkg/clients/gke/gke.go @@ -0,0 +1,50 @@ +/* +Copyright 2021 The Crossplane Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + 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 gke contains utilities for authenticating to GKE clusters. +package gke + +import ( + "context" + "net/http" + + "github.com/pkg/errors" + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" + "k8s.io/client-go/rest" +) + +// DefaultScopes for GKE authentication. +var DefaultScopes []string = []string{ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/userinfo.email", +} + +// WrapRESTConfig configures the supplied REST config to use OAuth2 bearer +// tokens fetched using the supplied Google Application Credentials. +func WrapRESTConfig(ctx context.Context, rc *rest.Config, credentials []byte, scopes ...string) error { + creds, err := google.CredentialsFromJSON(ctx, credentials, scopes...) + if err != nil { + return errors.Wrap(err, "cannot load Google Application Credentials from JSON") + } + + // CredentialsFromJSON creates a TokenSource that handles token caching. + rc.Wrap(func(rt http.RoundTripper) http.RoundTripper { + return &oauth2.Transport{Source: creds.TokenSource, Base: rt} + }) + + return nil +} diff --git a/pkg/controller/release/release.go b/pkg/controller/release/release.go index e829cea..e4caa93 100644 --- a/pkg/controller/release/release.go +++ b/pkg/controller/release/release.go @@ -44,6 +44,7 @@ import ( "github.com/crossplane-contrib/provider-helm/apis/release/v1beta1" helmv1beta1 "github.com/crossplane-contrib/provider-helm/apis/v1beta1" "github.com/crossplane-contrib/provider-helm/pkg/clients" + "github.com/crossplane-contrib/provider-helm/pkg/clients/gke" helmClient "github.com/crossplane-contrib/provider-helm/pkg/clients/helm" ) @@ -62,30 +63,28 @@ const ( ) const ( - errNotRelease = "managed resource is not a Release custom resource" - errProviderConfigNotSet = "provider config is not set" - errProviderNotRetrieved = "provider could not be retrieved" - errCredSecretNotSet = "provider credentials secret is not set" - errNewKubernetesClient = "cannot create new Kubernetes client" - errProviderSecretNotRetrieved = "secret referred in provider could not be retrieved" - errProviderSecretValueForKeyNotFound = "value for key \"%s\" not found in provider credentials secret" - errFailedToGetLastRelease = "failed to get last helm release" - errLastReleaseIsNil = "last helm release is nil" - errFailedToCheckIfUpToDate = "failed to check if release is up to date" - errFailedToInstall = "failed to install release" - errFailedToUpgrade = "failed to upgrade release" - errFailedToUninstall = "failed to uninstall release" - errFailedToGetRepoCreds = "failed to get user name and password from secret reference" - errFailedToComposeValues = "failed to compose values" - errFailedToCreateRestConfig = "cannot create new rest config using provider secret" - errFailedToTrackUsage = "cannot track provider config usage" - errFailedToLoadPatches = "failed to load patches" - errFailedToUpdatePatchSha = "failed to update patch sha" - errFailedToSetName = "failed to update chart spec with the name from URL" - errFailedToSetVersion = "failed to update chart spec with the latest version" - errFailedToCreateNamespace = "failed to create namespace for release" - - errFmtUnsupportedCredSource = "unsupported credentials source %q" + errNotRelease = "managed resource is not a Release custom resource" + errProviderConfigNotSet = "provider config is not set" + errProviderNotRetrieved = "provider could not be retrieved" + errNewKubernetesClient = "cannot create new Kubernetes client" + errFailedToGetLastRelease = "failed to get last helm release" + errLastReleaseIsNil = "last helm release is nil" + errFailedToCheckIfUpToDate = "failed to check if release is up to date" + errFailedToInstall = "failed to install release" + errFailedToUpgrade = "failed to upgrade release" + errFailedToUninstall = "failed to uninstall release" + errFailedToGetRepoCreds = "failed to get user name and password from secret reference" + errFailedToComposeValues = "failed to compose values" + errFailedToExtractKubeconfig = "failed to extract kubeconfig" + errFailedToExtractGoogleCredentials = "failed to extract Google Application Credentials" + errFailedToInjectGoogleCredentials = "failed to wrap REST client with Google Application Credentials" + errFailedToCreateRESTConfig = "cannot create new rest config using provider secret" + errFailedToTrackUsage = "cannot track provider config usage" + errFailedToLoadPatches = "failed to load patches" + errFailedToUpdatePatchSha = "failed to update patch sha" + errFailedToSetName = "failed to update chart spec with the name from URL" + errFailedToSetVersion = "failed to update chart spec with the latest version" + errFailedToCreateNamespace = "failed to create namespace for release" ) // Setup adds a controller that reconciles Release managed resources. @@ -99,7 +98,10 @@ func Setup(mgr ctrl.Manager, l logging.Logger) error { logger: logger, client: mgr.GetClient(), usage: resource.NewProviderConfigUsageTracker(mgr.GetClient(), &helmv1beta1.ProviderConfigUsage{}), - newRestConfigFn: clients.NewRestConfig, + kcfgExtractorFn: resource.CommonCredentialExtractor, + gcpExtractorFn: resource.CommonCredentialExtractor, + gcpInjectorFn: gke.WrapRESTConfig, + newRestConfigFn: clients.NewRESTConfig, newKubeClientFn: clients.NewKubeClient, newHelmClientFn: helmClient.NewClient, }), @@ -116,9 +118,13 @@ func Setup(mgr ctrl.Manager, l logging.Logger) error { } type connector struct { - logger logging.Logger - client client.Client - usage resource.Tracker + logger logging.Logger + client client.Client + usage resource.Tracker + + kcfgExtractorFn func(ctx context.Context, src xpv1.CredentialsSource, c client.Client, ccs xpv1.CommonCredentialSelectors) ([]byte, error) + gcpExtractorFn func(ctx context.Context, src xpv1.CredentialsSource, c client.Client, ccs xpv1.CommonCredentialSelectors) ([]byte, error) + gcpInjectorFn func(ctx context.Context, rc *rest.Config, credentials []byte, scopes ...string) error newRestConfigFn func(kubeconfig []byte) (*rest.Config, error) newKubeClientFn func(config *rest.Config) (client.Client, error) newHelmClientFn func(log logging.Logger, config *rest.Config, namespace string, wait bool, timeout time.Duration) (helmClient.Client, error) @@ -151,34 +157,36 @@ func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.E var rc *rest.Config var err error - s := p.Spec.Credentials.Source - switch s { //nolint:exhaustive + switch pc := p.Spec.Credentials; pc.Source { //nolint:exhaustive case xpv1.CredentialsSourceInjectedIdentity: rc, err = rest.InClusterConfig() if err != nil { - return nil, errors.Wrap(err, errFailedToCreateRestConfig) + return nil, errors.Wrap(err, errFailedToCreateRESTConfig) } - case xpv1.CredentialsSourceSecret: - ref := p.Spec.Credentials.SecretRef - if ref == nil { - return nil, errors.New(errCredSecretNotSet) + default: + kc, err := c.kcfgExtractorFn(ctx, pc.Source, c.client, pc.CommonCredentialSelectors) + if err != nil { + return nil, errors.Wrap(err, errFailedToExtractKubeconfig) } - key := types.NamespacedName{Namespace: ref.Namespace, Name: ref.Name} - d, err := getSecretData(ctx, c.client, key) + rc, err = c.newRestConfigFn(kc) if err != nil { - return nil, errors.Wrap(err, errProviderSecretNotRetrieved) - } - kc, f := d[ref.Key] - if !f { - return nil, errors.Errorf(errProviderSecretValueForKeyNotFound, ref.Key) + return nil, errors.Wrap(err, errFailedToCreateRESTConfig) } - rc, err = c.newRestConfigFn(kc) + } + + // NOTE(negz): We don't currently check the identity type because at the + // time of writing there's only one valid value (Google App Creds), and + // that value is required. + if id := p.Spec.Identity; id != nil { + creds, err := c.gcpExtractorFn(ctx, id.Source, c.client, id.CommonCredentialSelectors) if err != nil { - return nil, errors.Wrap(err, errFailedToCreateRestConfig) + return nil, errors.Wrap(err, errFailedToExtractGoogleCredentials) + } + + if err := c.gcpInjectorFn(ctx, rc, creds, gke.DefaultScopes...); err != nil { + return nil, errors.Wrap(err, errFailedToInjectGoogleCredentials) } - default: - return nil, errors.Errorf(errFmtUnsupportedCredSource, s) } k, err := c.newKubeClientFn(rc) diff --git a/pkg/controller/release/release_test.go b/pkg/controller/release/release_test.go index 96b04a5..9308344 100644 --- a/pkg/controller/release/release_test.go +++ b/pkg/controller/release/release_test.go @@ -2,7 +2,6 @@ package release import ( "context" - "fmt" "testing" "time" @@ -30,13 +29,7 @@ import ( ) const ( - providerName = "helm-test" - providerSecretName = "helm-test-secret" - providerSecretNamespace = "helm-test-secret-namespace" - - providerSecretKey = "kubeconfig" - providerSecretData = "somethingsecret" - + providerName = "helm-test" testReleaseName = "test-release" ) @@ -123,27 +116,22 @@ func Test_connector_Connect(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Name: providerName}, Spec: helmv1beta1.ProviderConfigSpec{ Credentials: helmv1beta1.ProviderCredentials{ - Source: xpv1.CredentialsSourceSecret, - CommonCredentialSelectors: xpv1.CommonCredentialSelectors{ - SecretRef: &xpv1.SecretKeySelector{ - SecretReference: xpv1.SecretReference{ - Name: providerSecretName, - Namespace: providerSecretNamespace, - }, - Key: providerSecretKey, - }, + Source: xpv1.CredentialsSourceNone, + }, + Identity: &helmv1beta1.Identity{ + Type: helmv1beta1.IdentityTypeGoogleApplicationCredentials, + ProviderCredentials: helmv1beta1.ProviderCredentials{ + Source: xpv1.CredentialsSourceNone, }, }, }, } - secret := corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{Namespace: providerSecretNamespace, Name: providerSecretName}, - Data: map[string][]byte{providerSecretKey: []byte(providerSecretData)}, - } - type args struct { client client.Client + kcfgExtractorFn func(ctx context.Context, src xpv1.CredentialsSource, c client.Client, ccs xpv1.CommonCredentialSelectors) ([]byte, error) + gcpExtractorFn func(ctx context.Context, src xpv1.CredentialsSource, c client.Client, ccs xpv1.CommonCredentialSelectors) ([]byte, error) + gcpInjectorFn func(ctx context.Context, rc *rest.Config, credentials []byte, scopes ...string) error newRestConfigFn func(kubeconfig []byte) (*rest.Config, error) newKubeClientFn func(config *rest.Config) (client.Client, error) newHelmClientFn func(log logging.Logger, config *rest.Config, namespace string, wait bool, timeout time.Duration) (helmClient.Client, error) @@ -192,47 +180,52 @@ func Test_connector_Connect(t *testing.T) { err: errors.Wrap(errBoom, errProviderNotRetrieved), }, }, - "UnsupportedCredentialSource": { + "FailedToExtractKubeconfig": { args: args{ client: &test.MockClient{ MockGet: func(ctx context.Context, key client.ObjectKey, obj client.Object) error { if key.Name == providerName { - pc := providerConfig.DeepCopy() - pc.Spec.Credentials.Source = xpv1.CredentialsSource("wat") - *obj.(*helmv1beta1.ProviderConfig) = *pc + *obj.(*helmv1beta1.ProviderConfig) = providerConfig return nil } - return nil + return errBoom }, }, + kcfgExtractorFn: func(ctx context.Context, src xpv1.CredentialsSource, c client.Client, ccs xpv1.CommonCredentialSelectors) ([]byte, error) { + return nil, errBoom + }, usage: resource.TrackerFn(func(ctx context.Context, mg resource.Managed) error { return nil }), mg: helmRelease(), }, want: want{ - err: errors.Errorf(errFmtUnsupportedCredSource, "wat"), + err: errors.Wrap(errBoom, errFailedToExtractKubeconfig), }, }, - "NoSecretRef": { + "FailedToCreateRestConfig": { args: args{ client: &test.MockClient{ MockGet: func(ctx context.Context, key client.ObjectKey, obj client.Object) error { if key.Name == providerName { - pc := providerConfig.DeepCopy() - pc.Spec.Credentials.SecretRef = nil - *obj.(*helmv1beta1.ProviderConfig) = *pc + *obj.(*helmv1beta1.ProviderConfig) = providerConfig return nil } - return nil + return errBoom }, }, + kcfgExtractorFn: func(ctx context.Context, src xpv1.CredentialsSource, c client.Client, ccs xpv1.CommonCredentialSelectors) ([]byte, error) { + return nil, nil + }, + newRestConfigFn: func(kubeconfig []byte) (config *rest.Config, err error) { + return nil, errBoom + }, usage: resource.TrackerFn(func(ctx context.Context, mg resource.Managed) error { return nil }), mg: helmRelease(), }, want: want{ - err: errors.New(errCredSecretNotSet), + err: errors.Wrap(errBoom, errFailedToCreateRESTConfig), }, }, - "FailedToGetProviderSecret": { + "FailedToExtractGoogleCredentials": { args: args{ client: &test.MockClient{ MockGet: func(ctx context.Context, key client.ObjectKey, obj client.Object) error { @@ -240,20 +233,26 @@ func Test_connector_Connect(t *testing.T) { *obj.(*helmv1beta1.ProviderConfig) = providerConfig return nil } - if key.Name == providerSecretName && key.Namespace == providerSecretNamespace { - return errBoom - } return errBoom }, }, + kcfgExtractorFn: func(ctx context.Context, src xpv1.CredentialsSource, c client.Client, ccs xpv1.CommonCredentialSelectors) ([]byte, error) { + return nil, nil + }, + newRestConfigFn: func(kubeconfig []byte) (config *rest.Config, err error) { + return nil, nil + }, + gcpExtractorFn: func(ctx context.Context, src xpv1.CredentialsSource, c client.Client, ccs xpv1.CommonCredentialSelectors) ([]byte, error) { + return nil, errBoom + }, usage: resource.TrackerFn(func(ctx context.Context, mg resource.Managed) error { return nil }), mg: helmRelease(), }, want: want{ - err: errors.Wrap(errors.Wrap(errBoom, fmt.Sprintf(errFailedToGetSecret, providerSecretNamespace)), errProviderSecretNotRetrieved), + err: errors.Wrap(errBoom, errFailedToExtractGoogleCredentials), }, }, - "FailedToCreateRestConfig": { + "FailedToInjectGoogleCredentials": { args: args{ client: &test.MockClient{ MockGet: func(ctx context.Context, key client.ObjectKey, obj client.Object) error { @@ -261,21 +260,26 @@ func Test_connector_Connect(t *testing.T) { *obj.(*helmv1beta1.ProviderConfig) = providerConfig return nil } - if key.Name == providerSecretName && key.Namespace == providerSecretNamespace { - *obj.(*corev1.Secret) = secret - return nil - } return errBoom }, }, + kcfgExtractorFn: func(ctx context.Context, src xpv1.CredentialsSource, c client.Client, ccs xpv1.CommonCredentialSelectors) ([]byte, error) { + return nil, nil + }, newRestConfigFn: func(kubeconfig []byte) (config *rest.Config, err error) { - return nil, errBoom + return nil, nil + }, + gcpExtractorFn: func(ctx context.Context, src xpv1.CredentialsSource, c client.Client, ccs xpv1.CommonCredentialSelectors) ([]byte, error) { + return nil, nil + }, + gcpInjectorFn: func(ctx context.Context, rc *rest.Config, credentials []byte, scopes ...string) error { + return errBoom }, usage: resource.TrackerFn(func(ctx context.Context, mg resource.Managed) error { return nil }), mg: helmRelease(), }, want: want{ - err: errors.Wrap(errBoom, errFailedToCreateRestConfig), + err: errors.Wrap(errBoom, errFailedToInjectGoogleCredentials), }, }, "FailedToCreateNewKubernetesClient": { @@ -286,8 +290,40 @@ func Test_connector_Connect(t *testing.T) { *obj.(*helmv1beta1.ProviderConfig) = providerConfig return nil } - if key.Name == providerSecretName && key.Namespace == providerSecretNamespace { - *obj.(*corev1.Secret) = secret + return errBoom + }, + MockStatusUpdate: func(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error { + return nil + }, + }, + kcfgExtractorFn: func(ctx context.Context, src xpv1.CredentialsSource, c client.Client, ccs xpv1.CommonCredentialSelectors) ([]byte, error) { + return nil, nil + }, + gcpExtractorFn: func(ctx context.Context, src xpv1.CredentialsSource, c client.Client, ccs xpv1.CommonCredentialSelectors) ([]byte, error) { + return nil, nil + }, + gcpInjectorFn: func(ctx context.Context, rc *rest.Config, credentials []byte, scopes ...string) error { + return nil + }, + newRestConfigFn: func(kubeconfig []byte) (config *rest.Config, err error) { + return &rest.Config{}, nil + }, + newKubeClientFn: func(config *rest.Config) (c client.Client, err error) { + return nil, errBoom + }, + usage: resource.TrackerFn(func(ctx context.Context, mg resource.Managed) error { return nil }), + mg: helmRelease(), + }, + want: want{ + err: errors.Wrap(errBoom, errNewKubernetesClient), + }, + }, + "FailedToCreateNewHelmClient": { + args: args{ + client: &test.MockClient{ + MockGet: func(ctx context.Context, key client.ObjectKey, obj client.Object) error { + if key.Name == providerName { + *obj.(*helmv1beta1.ProviderConfig) = providerConfig return nil } return errBoom @@ -296,10 +332,22 @@ func Test_connector_Connect(t *testing.T) { return nil }, }, + kcfgExtractorFn: func(ctx context.Context, src xpv1.CredentialsSource, c client.Client, ccs xpv1.CommonCredentialSelectors) ([]byte, error) { + return nil, nil + }, + gcpExtractorFn: func(ctx context.Context, src xpv1.CredentialsSource, c client.Client, ccs xpv1.CommonCredentialSelectors) ([]byte, error) { + return nil, nil + }, + gcpInjectorFn: func(ctx context.Context, rc *rest.Config, credentials []byte, scopes ...string) error { + return nil + }, newRestConfigFn: func(kubeconfig []byte) (config *rest.Config, err error) { return &rest.Config{}, nil }, newKubeClientFn: func(config *rest.Config) (c client.Client, err error) { + return nil, nil + }, + newHelmClientFn: func(log logging.Logger, config *rest.Config, namespace string, wait bool, timeout time.Duration) (helmClient.Client, error) { return nil, errBoom }, usage: resource.TrackerFn(func(ctx context.Context, mg resource.Managed) error { return nil }), @@ -316,8 +364,6 @@ func Test_connector_Connect(t *testing.T) { switch t := obj.(type) { case *helmv1beta1.ProviderConfig: *t = providerConfig - case *corev1.Secret: - *t = secret default: return errBoom } @@ -327,6 +373,15 @@ func Test_connector_Connect(t *testing.T) { return nil }, }, + kcfgExtractorFn: func(ctx context.Context, src xpv1.CredentialsSource, c client.Client, ccs xpv1.CommonCredentialSelectors) ([]byte, error) { + return nil, nil + }, + gcpExtractorFn: func(ctx context.Context, src xpv1.CredentialsSource, c client.Client, ccs xpv1.CommonCredentialSelectors) ([]byte, error) { + return nil, nil + }, + gcpInjectorFn: func(ctx context.Context, rc *rest.Config, credentials []byte, scopes ...string) error { + return nil + }, newRestConfigFn: func(kubeconfig []byte) (config *rest.Config, err error) { return &rest.Config{}, nil }, @@ -349,6 +404,9 @@ func Test_connector_Connect(t *testing.T) { c := &connector{ logger: logging.NewNopLogger(), client: tc.args.client, + kcfgExtractorFn: tc.args.kcfgExtractorFn, + gcpExtractorFn: tc.args.gcpExtractorFn, + gcpInjectorFn: tc.args.gcpInjectorFn, newRestConfigFn: tc.args.newRestConfigFn, newKubeClientFn: tc.args.newKubeClientFn, newHelmClientFn: tc.args.newHelmClientFn, From 771e91978f4a7c5601836d05934de95f8a390c2c Mon Sep 17 00:00:00 2001 From: Nic Cope Date: Tue, 14 Sep 2021 06:59:09 +0000 Subject: [PATCH 2/2] Don't require a kubeconfig user Per https://github.com/crossplane/provider-gcp/issues/343 the GKECluster managed resource doesn't write a user to its kubeconfig when no masterAuth is supplied. This is fine in cases where user authorization is loaded from another source. Signed-off-by: Nic Cope --- pkg/clients/client.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pkg/clients/client.go b/pkg/clients/client.go index 6306ca1..ab1e9ad 100644 --- a/pkg/clients/client.go +++ b/pkg/clients/client.go @@ -17,8 +17,6 @@ limitations under the License. package clients import ( - "fmt" - "github.com/pkg/errors" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" @@ -53,11 +51,14 @@ func restConfigFromAPIConfig(c *api.Config) (*rest.Config, error) { ctx := c.Contexts[c.CurrentContext] cluster := c.Clusters[ctx.Cluster] if cluster == nil { - return nil, errors.New(fmt.Sprintf("cluster for currentContext (%s) not found", c.CurrentContext)) + return nil, errors.Errorf("cluster for currentContext (%s) not found", c.CurrentContext) } user := c.AuthInfos[ctx.AuthInfo] if user == nil { - return nil, errors.New(fmt.Sprintf("auth info for currentContext (%s) not found", c.CurrentContext)) + // We don't require a user because it's possible user + // authorization configuration will be loaded from a separate + // set of identity credentials (e.g. Google Application Creds). + user = &api.AuthInfo{} } return &rest.Config{ Host: cluster.Server,