Merge pull request #240 from hasheddan/creds-flex
Support additional ProviderConfig cred sources and provide tooling for extraction
This commit is contained in:
commit
45bc6d5035
|
|
@ -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"`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue