From 227b88060ff11ac59582e4e3af1b0733eec2b39a Mon Sep 17 00:00:00 2001 From: "Barrera, Angel" Date: Thu, 5 Jan 2023 12:26:38 +0100 Subject: [PATCH] Add serviceAccount CredentialsSource Signed-off-by: Barrera, Angel --- apis/v1alpha1/types.go | 76 ++++++++++++++++++- apis/v1alpha1/zz_generated.deepcopy.go | 69 ++++++++++++++++- internal/controller/object/object.go | 36 ++++++++- internal/resource/providerconfig.go | 50 ++++++++++++ ...ernetes.crossplane.io_providerconfigs.yaml | 32 ++++++++ 5 files changed, 256 insertions(+), 7 deletions(-) create mode 100644 internal/resource/providerconfig.go diff --git a/apis/v1alpha1/types.go b/apis/v1alpha1/types.go index 0eae2050..c0ff7f9f 100644 --- a/apis/v1alpha1/types.go +++ b/apis/v1alpha1/types.go @@ -37,10 +37,80 @@ type ProviderConfigSpec struct { // ProviderCredentials required to authenticate. type ProviderCredentials struct { // Source of the provider credentials. - // +kubebuilder:validation:Enum=None;Secret;InjectedIdentity;Environment;Filesystem - Source xpv1.CredentialsSource `json:"source"` + // +kubebuilder:validation:Enum=None;Secret;ServiceAccount;InjectedIdentity;Environment;Filesystem + Source CredentialsSource `json:"source"` - xpv1.CommonCredentialSelectors `json:",inline"` + KubernetesCredentialSelectors `json:",inline"` +} + +// A CredentialsSource is a source from which provider credentials may be +// acquired. +type CredentialsSource string + +const ( + // CredentialsSourceNone indicates that a provider does not require + // credentials. + CredentialsSourceNone CredentialsSource = "None" + + // CredentialsSourceSecret indicates that a provider should acquire + // credentials from a secret. + CredentialsSourceSecret CredentialsSource = "Secret" + + // CredentialsSourceServiceAccount indicates that a provider should acquire + // credentials from a serviceaccount token. + CredentialsSourceServiceAccount CredentialsSource = "ServiceAccount" + + // CredentialsSourceInjectedIdentity indicates that a provider should use + // credentials via its (pod's) identity; i.e. via IRSA for AWS, + // 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" +) + +// KubernetesCredentialSelectors provides common selectors for extracting +// credentials. +type KubernetesCredentialSelectors struct { + // Fs is a reference to a filesystem location that contains credentials that + // must be used to connect to the provider. + // +optional + Fs *xpv1.FsSelector `json:"fs,omitempty"` + + // Env is a reference to an environment variable that contains credentials + // that must be used to connect to the provider. + // +optional + Env *xpv1.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 *xpv1.SecretKeySelector `json:"secretRef,omitempty"` + + // A ServiceAccountRef is a reference to a serviceaccount that contains the grants + // that must be used to connect to the provider. + // +optional + ServiceAccountRef *ServiceAccountSelector `json:"serviceAccountRef,omitempty"` +} + +// A ServiceAccountSelector is a reference to a serviceaccount in an arbitrary namespace. +type ServiceAccountSelector struct { + ServiceAccountReference `json:",inline"` +} + +// A ServiceAccountReference is a reference to a serviceaccount in an arbitrary namespace. +type ServiceAccountReference struct { + // Name of the serviceaccount. + Name string `json:"name"` + + // Namespace of the serviceaccount. + Namespace string `json:"namespace"` } // IdentityType used to authenticate to the Kubernetes API. diff --git a/apis/v1alpha1/zz_generated.deepcopy.go b/apis/v1alpha1/zz_generated.deepcopy.go index 3f9baeec..3aa33066 100644 --- a/apis/v1alpha1/zz_generated.deepcopy.go +++ b/apis/v1alpha1/zz_generated.deepcopy.go @@ -22,6 +22,7 @@ limitations under the License. package v1alpha1 import ( + "github.com/crossplane/crossplane-runtime/apis/common/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -41,6 +42,41 @@ func (in *Identity) DeepCopy() *Identity { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KubernetesCredentialSelectors) DeepCopyInto(out *KubernetesCredentialSelectors) { + *out = *in + if in.Fs != nil { + in, out := &in.Fs, &out.Fs + *out = new(v1.FsSelector) + **out = **in + } + if in.Env != nil { + in, out := &in.Env, &out.Env + *out = new(v1.EnvSelector) + **out = **in + } + if in.SecretRef != nil { + in, out := &in.SecretRef, &out.SecretRef + *out = new(v1.SecretKeySelector) + **out = **in + } + if in.ServiceAccountRef != nil { + in, out := &in.ServiceAccountRef, &out.ServiceAccountRef + *out = new(ServiceAccountSelector) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubernetesCredentialSelectors. +func (in *KubernetesCredentialSelectors) DeepCopy() *KubernetesCredentialSelectors { + if in == nil { + return nil + } + out := new(KubernetesCredentialSelectors) + 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 @@ -198,7 +234,7 @@ func (in *ProviderConfigUsageList) DeepCopyObject() runtime.Object { // 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 - in.CommonCredentialSelectors.DeepCopyInto(&out.CommonCredentialSelectors) + in.KubernetesCredentialSelectors.DeepCopyInto(&out.KubernetesCredentialSelectors) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderCredentials. @@ -210,3 +246,34 @@ func (in *ProviderCredentials) DeepCopy() *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 *ServiceAccountReference) DeepCopyInto(out *ServiceAccountReference) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceAccountReference. +func (in *ServiceAccountReference) DeepCopy() *ServiceAccountReference { + if in == nil { + return nil + } + out := new(ServiceAccountReference) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceAccountSelector) DeepCopyInto(out *ServiceAccountSelector) { + *out = *in + out.ServiceAccountReference = in.ServiceAccountReference +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceAccountSelector. +func (in *ServiceAccountSelector) DeepCopy() *ServiceAccountSelector { + if in == nil { + return nil + } + out := new(ServiceAccountSelector) + in.DeepCopyInto(out) + return out +} diff --git a/internal/controller/object/object.go b/internal/controller/object/object.go index 8f036d33..8ae6420d 100644 --- a/internal/controller/object/object.go +++ b/internal/controller/object/object.go @@ -21,6 +21,7 @@ import ( "github.com/pkg/errors" v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/equality" kerrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -39,6 +40,8 @@ import ( "github.com/crossplane/crossplane-runtime/pkg/reconciler/managed" "github.com/crossplane/crossplane-runtime/pkg/resource" + internalresource "github.com/crossplane-contrib/provider-kubernetes/internal/resource" + "github.com/crossplane-contrib/provider-kubernetes/apis/object/v1alpha1" apisv1alpha1 "github.com/crossplane-contrib/provider-kubernetes/apis/v1alpha1" "github.com/crossplane-contrib/provider-kubernetes/internal/clients" @@ -141,13 +144,32 @@ func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.E var err error switch cd := pc.Spec.Credentials; cd.Source { //nolint:exhaustive - case xpv1.CredentialsSourceInjectedIdentity: + case apisv1alpha1.CredentialsSourceInjectedIdentity: rc, err = rest.InClusterConfig() if err != nil { return nil, errors.Wrap(err, errFailedToCreateRestConfig) } + case apisv1alpha1.CredentialsSourceServiceAccount: + token, err := internalresource.ExtractServiceAccount(ctx, c.kube, cd.KubernetesCredentialSelectors) + if err != nil { + return nil, errors.Wrap(err, errGetCreds) + } + rc, err = rest.InClusterConfig() + if err != nil { + return nil, errors.Wrap(err, errFailedToCreateRestConfig) + } + rc.BearerToken = string(token) + rc.BearerTokenFile = "" default: - kc, err := c.kcfgExtractorFn(ctx, cd.Source, c.kube, cd.CommonCredentialSelectors) + // Needed to use upstream methods for extracting credentials. + ccs := xpv1.CommonCredentialSelectors{ + Fs: cd.KubernetesCredentialSelectors.Fs, + Env: cd.KubernetesCredentialSelectors.Env, + SecretRef: cd.KubernetesCredentialSelectors.SecretRef, + } + src := xpv1.CredentialsSource(cd.Source) + + kc, err := c.kcfgExtractorFn(ctx, src, c.kube, ccs) if err != nil { return nil, errors.Wrap(err, errGetCreds) } @@ -161,7 +183,15 @@ func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.E // time of writing there's only one valid value (Google App Creds), and // that value is required. if id := pc.Spec.Identity; id != nil { - creds, err := c.gcpExtractorFn(ctx, id.Source, c.kube, id.CommonCredentialSelectors) + // Needed to use upstream methods for extracting credentials. + ccs := xpv1.CommonCredentialSelectors{ + Fs: id.ProviderCredentials.KubernetesCredentialSelectors.Fs, + Env: id.ProviderCredentials.KubernetesCredentialSelectors.Env, + SecretRef: id.ProviderCredentials.KubernetesCredentialSelectors.SecretRef, + } + src := xpv1.CredentialsSource(id.ProviderCredentials.Source) + + creds, err := c.gcpExtractorFn(ctx, src, c.kube, ccs) if err != nil { return nil, errors.Wrap(err, errFailedToExtractGoogleCredentials) } diff --git a/internal/resource/providerconfig.go b/internal/resource/providerconfig.go new file mode 100644 index 00000000..93ac92d3 --- /dev/null +++ b/internal/resource/providerconfig.go @@ -0,0 +1,50 @@ +package resource + +import ( + "context" + + "github.com/google/uuid" + "github.com/pkg/errors" + authenticationv1 "k8s.io/api/authentication/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/pointer" + "sigs.k8s.io/controller-runtime/pkg/client" + + apisv1alpha1 "github.com/crossplane-contrib/provider-kubernetes/apis/v1alpha1" +) + +const ( + errExtractServiceAccount = "cannot extract from service account when none specified" + errGetServiceAccount = "cannot get service account" + errTokenRequest = "cannot create token request" +) + +// ExtractServiceAccount extracts credentials from a Kubernetes service account. +func ExtractServiceAccount(ctx context.Context, client client.Client, s apisv1alpha1.KubernetesCredentialSelectors) ([]byte, error) { + if s.ServiceAccountRef == nil { + return nil, errors.New(errExtractServiceAccount) + } + sa := &corev1.ServiceAccount{} + if err := client.Get(ctx, types.NamespacedName{Namespace: s.ServiceAccountRef.Namespace, Name: s.ServiceAccountRef.Name}, sa); err != nil { + return nil, errors.Wrap(err, errGetServiceAccount) + } + // Create a TokenRequest for the service account. + // The name must be the same as the service account name + "-token-". + tr := &authenticationv1.TokenRequest{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: sa.Namespace, + Name: sa.Name + "-token-" + uuid.New().String()[0:5], + }, + Spec: authenticationv1.TokenRequestSpec{ + Audiences: []string{"https://kubernetes.default.svc"}, + ExpirationSeconds: pointer.Int64Ptr(3600), // 1 hour. TODO: make this configurable. + }, + } + if err := client.Create(ctx, tr); err != nil { + return nil, errors.Wrap(err, errTokenRequest) + } + // Return the token. + return []byte(tr.Status.Token), nil +} diff --git a/package/crds/kubernetes.crossplane.io_providerconfigs.yaml b/package/crds/kubernetes.crossplane.io_providerconfigs.yaml index 8080006b..56c3ae87 100644 --- a/package/crds/kubernetes.crossplane.io_providerconfigs.yaml +++ b/package/crds/kubernetes.crossplane.io_providerconfigs.yaml @@ -89,11 +89,27 @@ spec: - name - namespace type: object + serviceAccountRef: + description: A ServiceAccountRef is a reference to a serviceaccount + that contains the grants that must be used to connect to the + provider. + properties: + name: + description: Name of the serviceaccount. + type: string + namespace: + description: Namespace of the serviceaccount. + type: string + required: + - name + - namespace + type: object source: description: Source of the provider credentials. enum: - None - Secret + - ServiceAccount - InjectedIdentity - Environment - Filesystem @@ -144,11 +160,27 @@ spec: - name - namespace type: object + serviceAccountRef: + description: A ServiceAccountRef is a reference to a serviceaccount + that contains the grants that must be used to connect to the + provider. + properties: + name: + description: Name of the serviceaccount. + type: string + namespace: + description: Namespace of the serviceaccount. + type: string + required: + - name + - namespace + type: object source: description: Source of the provider credentials. enum: - None - Secret + - ServiceAccount - InjectedIdentity - Environment - Filesystem