Merge pull request #240 from hasheddan/creds-flex

Support additional ProviderConfig cred sources and provide tooling for extraction
This commit is contained in:
Daniel Mangum 2021-02-01 16:19:09 -06:00 committed by GitHub
commit 45bc6d5035
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 355 additions and 84 deletions

View File

@ -161,25 +161,6 @@ type ResourceStatus struct {
ConditionedStatus `json:",inline"`
}
// A ProviderSpec defines the common way to get to the necessary objects to
// connect to the provider.
// Deprecated: Please use ProviderConfigSpec.
type ProviderSpec struct {
// CredentialsSecretRef references a specific secret's key that contains
// the credentials that are used to connect to the provider.
// +optional
CredentialsSecretRef *SecretKeySelector `json:"credentialsSecretRef,omitempty"`
}
// A ProviderConfigSpec defines the desired state of a provider config. A
// provider config may embed this type in its spec in order to support standard
// fields. Provider configs may choose to avoid embedding this type as
// appropriate, but are encouraged to follow its conventions.
type ProviderConfigSpec struct {
// Credentials required to authenticate to this provider.
Credentials ProviderCredentials `json:"credentials"`
}
// A CredentialsSource is a source from which provider credentials may be
// acquired.
type CredentialsSource string
@ -198,20 +179,47 @@ const (
// Workload Identity for GCP, Pod Identity for Azure, or in-cluster
// authentication for the Kubernetes API.
CredentialsSourceInjectedIdentity CredentialsSource = "InjectedIdentity"
// CredentialsSourceEnvironment indicates that a provider should acquire
// credentials from an environment variable.
CredentialsSourceEnvironment CredentialsSource = "Environment"
// CredentialsSourceFilesystem indicates that a provider should acquire
// credentials from the filesystem.
CredentialsSourceFilesystem CredentialsSource = "Filesystem"
)
// ProviderCredentials required to authenticate.
type ProviderCredentials struct {
// Source of the provider credentials.
// +kubebuilder:validation:Enum=None;Secret;InjectedIdentity
Source CredentialsSource `json:"source"`
// CommonCredentialSelectors provides common selectors for extracting
// credentials.
type CommonCredentialSelectors struct {
// Fs is a reference to a filesystem location that contains credentials that
// must be used to connect to the provider.
// +optional
Fs *FsSelector `json:"fs,omitempty"`
// A CredentialsSecretRef is a reference to a secret key that contains the
// credentials that must be used to connect to the provider.
// Env is a reference to an environment variable that contains credentials
// that must be used to connect to the provider.
// +optional
Env *EnvSelector `json:"env,omitempty"`
// A SecretRef is a reference to a secret key that contains the credentials
// that must be used to connect to the provider.
// +optional
SecretRef *SecretKeySelector `json:"secretRef,omitempty"`
}
// EnvSelector selects an environment variable.
type EnvSelector struct {
// Name is the name of an environment variable.
Name string `json:"name"`
}
// FsSelector selects a filesystem location.
type FsSelector struct {
// Path is a filesystem path.
Path string `json:"path"`
}
// A ProviderConfigStatus defines the observed status of a ProviderConfig.
type ProviderConfigStatus struct {
ConditionedStatus `json:",inline"`

View File

@ -24,6 +24,36 @@ import (
corev1 "k8s.io/api/core/v1"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CommonCredentialSelectors) DeepCopyInto(out *CommonCredentialSelectors) {
*out = *in
if in.Fs != nil {
in, out := &in.Fs, &out.Fs
*out = new(FsSelector)
**out = **in
}
if in.Env != nil {
in, out := &in.Env, &out.Env
*out = new(EnvSelector)
**out = **in
}
if in.SecretRef != nil {
in, out := &in.SecretRef, &out.SecretRef
*out = new(SecretKeySelector)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CommonCredentialSelectors.
func (in *CommonCredentialSelectors) DeepCopy() *CommonCredentialSelectors {
if in == nil {
return nil
}
out := new(CommonCredentialSelectors)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Condition) DeepCopyInto(out *Condition) {
*out = *in
@ -62,6 +92,36 @@ func (in *ConditionedStatus) DeepCopy() *ConditionedStatus {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *EnvSelector) DeepCopyInto(out *EnvSelector) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EnvSelector.
func (in *EnvSelector) DeepCopy() *EnvSelector {
if in == nil {
return nil
}
out := new(EnvSelector)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *FsSelector) DeepCopyInto(out *FsSelector) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FsSelector.
func (in *FsSelector) DeepCopy() *FsSelector {
if in == nil {
return nil
}
out := new(FsSelector)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *LocalSecretReference) DeepCopyInto(out *LocalSecretReference) {
*out = *in
@ -77,22 +137,6 @@ func (in *LocalSecretReference) DeepCopy() *LocalSecretReference {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ProviderConfigSpec) DeepCopyInto(out *ProviderConfigSpec) {
*out = *in
in.Credentials.DeepCopyInto(&out.Credentials)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderConfigSpec.
func (in *ProviderConfigSpec) DeepCopy() *ProviderConfigSpec {
if in == nil {
return nil
}
out := new(ProviderConfigSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ProviderConfigStatus) DeepCopyInto(out *ProviderConfigStatus) {
*out = *in
@ -126,46 +170,6 @@ func (in *ProviderConfigUsage) DeepCopy() *ProviderConfigUsage {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ProviderCredentials) DeepCopyInto(out *ProviderCredentials) {
*out = *in
if in.SecretRef != nil {
in, out := &in.SecretRef, &out.SecretRef
*out = new(SecretKeySelector)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderCredentials.
func (in *ProviderCredentials) DeepCopy() *ProviderCredentials {
if in == nil {
return nil
}
out := new(ProviderCredentials)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ProviderSpec) DeepCopyInto(out *ProviderSpec) {
*out = *in
if in.CredentialsSecretRef != nil {
in, out := &in.CredentialsSecretRef, &out.CredentialsSecretRef
*out = new(SecretKeySelector)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderSpec.
func (in *ProviderSpec) DeepCopy() *ProviderSpec {
if in == nil {
return nil
}
out := new(ProviderSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Reference) DeepCopyInto(out *Reference) {
*out = *in

View File

@ -18,10 +18,14 @@ package resource
import (
"context"
"os"
"github.com/pkg/errors"
"github.com/spf13/afero"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
@ -29,8 +33,13 @@ import (
)
const (
errMissingPCRef = "managed resource does not reference a ProviderConfig"
errApplyPCU = "cannot apply ProviderConfigUsage"
errExtractEnv = "cannot extract from environment variable when none specified"
errExtractFs = "cannot extract from filesystem when no path specified"
errExtractSecretKey = "cannot extract from secret key when none specified"
errGetCredentialsSecret = "cannot get credentials secret"
errNoHandlerForSourceFmt = "no extraction handler registered for source: %s"
errMissingPCRef = "managed resource does not reference a ProviderConfig"
errApplyPCU = "cannot apply ProviderConfigUsage"
)
type errMissingRef struct{ error }
@ -46,6 +55,52 @@ func IsMissingReference(err error) bool {
return ok
}
// EnvLookupFn looks up an environment variable.
type EnvLookupFn func(string) string
// ExtractEnv extracts credentials from an environment variable.
func ExtractEnv(ctx context.Context, e EnvLookupFn, s xpv1.CommonCredentialSelectors) ([]byte, error) {
if s.Env == nil {
return nil, errors.New(errExtractEnv)
}
return []byte(e(s.Env.Name)), nil
}
// ExtractFs extracts credentials from the filesystem.
func ExtractFs(ctx context.Context, fs afero.Fs, s xpv1.CommonCredentialSelectors) ([]byte, error) {
if s.Fs == nil {
return nil, errors.New(errExtractFs)
}
return afero.ReadFile(fs, s.Fs.Path)
}
// ExtractSecret extracts credentials from a Kubernetes secret.
func ExtractSecret(ctx context.Context, client client.Client, s xpv1.CommonCredentialSelectors) ([]byte, error) {
if s.SecretRef == nil {
return nil, errors.New(errExtractSecretKey)
}
secret := &corev1.Secret{}
if err := client.Get(ctx, types.NamespacedName{Namespace: s.SecretRef.Namespace, Name: s.SecretRef.Name}, secret); err != nil {
return nil, errors.Wrap(err, errGetCredentialsSecret)
}
return secret.Data[s.SecretRef.Key], nil
}
// CommonCredentialExtractor extracts credentials from common sources.
func CommonCredentialExtractor(ctx context.Context, source xpv1.CredentialsSource, client client.Client, selector xpv1.CommonCredentialSelectors) ([]byte, error) {
switch source { // nolint:exhaustive
case xpv1.CredentialsSourceEnvironment:
return ExtractEnv(ctx, os.Getenv, selector)
case xpv1.CredentialsSourceFilesystem:
return ExtractFs(ctx, afero.NewOsFs(), selector)
case xpv1.CredentialsSourceSecret:
return ExtractSecret(ctx, client, selector)
case xpv1.CredentialsSourceNone:
return nil, nil
}
return nil, errors.Errorf(errNoHandlerForSourceFmt, source)
}
// A Tracker tracks managed resources.
type Tracker interface {
// Track the supplied managed resource.

View File

@ -22,6 +22,8 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/pkg/errors"
"github.com/spf13/afero"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
@ -29,6 +31,208 @@ import (
"github.com/crossplane/crossplane-runtime/pkg/test"
)
func TestExtractEnv(t *testing.T) {
credentials := []byte("supersecretcreds")
type args struct {
e EnvLookupFn
creds xpv1.CommonCredentialSelectors
}
type want struct {
b []byte
err error
}
cases := map[string]struct {
reason string
args args
want want
}{
"EnvVarSuccess": {
reason: "Successful extraction of credentials from environment variable",
args: args{
e: func(string) string { return string(credentials) },
creds: xpv1.CommonCredentialSelectors{
Env: &xpv1.EnvSelector{
Name: "SECRET_CREDS",
},
},
},
want: want{
b: credentials,
},
},
"EnvVarFail": {
reason: "Failed extraction of credentials from environment variable",
args: args{
e: func(string) string { return string(credentials) },
},
want: want{
err: errors.New(errExtractEnv),
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
got, err := ExtractEnv(context.TODO(), tc.args.e, tc.args.creds)
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
t.Errorf("\n%s\npc.ExtractEnv(...): -want error, +got error:\n%s\n", tc.reason, diff)
}
if diff := cmp.Diff(tc.want.b, got); diff != "" {
t.Errorf("\n%s\npc.ExtractEnv(...): -want, +got:\n%s\n", tc.reason, diff)
}
})
}
}
func TestExtractFs(t *testing.T) {
credentials := []byte("supersecretcreds")
mockFs := afero.NewMemMapFs()
f, _ := mockFs.Create("credentials.txt")
f.Write(credentials)
f.Close()
type args struct {
fs afero.Fs
creds xpv1.CommonCredentialSelectors
}
type want struct {
b []byte
err error
}
cases := map[string]struct {
reason string
args args
want want
}{
"FsSuccess": {
reason: "Successful extraction of credentials from filesystem",
args: args{
fs: mockFs,
creds: xpv1.CommonCredentialSelectors{
Fs: &xpv1.FsSelector{
Path: "credentials.txt",
},
},
},
want: want{
b: credentials,
},
},
"FsFailure": {
reason: "Failed extraction of credentials from filesystem",
args: args{
fs: mockFs,
},
want: want{
err: errors.New(errExtractFs),
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
got, err := ExtractFs(context.TODO(), tc.args.fs, tc.args.creds)
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
t.Errorf("\n%s\npc.ExtractFs(...): -want error, +got error:\n%s\n", tc.reason, diff)
}
if diff := cmp.Diff(tc.want.b, got); diff != "" {
t.Errorf("\n%s\npc.ExtractFs(...): -want, +got:\n%s\n", tc.reason, diff)
}
})
}
}
func TestExtractSecret(t *testing.T) {
errBoom := errors.New("boom")
credentials := []byte("supersecretcreds")
type args struct {
client client.Client
creds xpv1.CommonCredentialSelectors
}
type want struct {
b []byte
err error
}
cases := map[string]struct {
reason string
args args
want want
}{
"SecretSuccess": {
reason: "Successful extraction of credentials from Secret",
args: args{
client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(o client.Object) error {
s, _ := o.(*corev1.Secret)
s.Data = map[string][]byte{
"creds": credentials,
}
return nil
}),
},
creds: xpv1.CommonCredentialSelectors{
SecretRef: &xpv1.SecretKeySelector{
SecretReference: xpv1.SecretReference{
Name: "super",
Namespace: "secret",
},
Key: "creds",
},
},
},
want: want{
b: credentials,
},
},
"SecretFailureNotDefined": {
reason: "Failed extraction of credentials from Secret when key not defined",
args: args{},
want: want{
err: errors.New(errExtractSecretKey),
},
},
"SecretFailureGet": {
reason: "Failed extraction of credentials from Secret when client fails",
args: args{
client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(client.Object) error {
return errBoom
}),
},
creds: xpv1.CommonCredentialSelectors{
SecretRef: &xpv1.SecretKeySelector{
SecretReference: xpv1.SecretReference{
Name: "super",
Namespace: "secret",
},
Key: "creds",
},
},
},
want: want{
err: errors.Wrap(errBoom, errGetCredentialsSecret),
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
got, err := ExtractSecret(context.TODO(), tc.args.client, tc.args.creds)
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
t.Errorf("\n%s\npc.ExtractSecret(...): -want error, +got error:\n%s\n", tc.reason, diff)
}
if diff := cmp.Diff(tc.want.b, got); diff != "" {
t.Errorf("\n%s\npc.ExtractSecret(...): -want, +got:\n%s\n", tc.reason, diff)
}
})
}
}
func TestTrack(t *testing.T) {
errBoom := errors.New("boom")
name := "provisional"