From a7239b993cd3550676642a99dec2ec0b1b708a33 Mon Sep 17 00:00:00 2001 From: Noah Kantrowitz Date: Thu, 24 Dec 2020 23:38:58 -0800 Subject: [PATCH] Add support for cluster-scoped trigger authentication, ClusterTriggerAuthentication. This allows creating a single authentication for Keda, for scalers where that makes sense. Signed-off-by: Noah Kantrowitz --- CHANGELOG.md | 1 + api/v1alpha1/scaledobject_types.go | 5 +- api/v1alpha1/triggerauthentication_types.go | 26 +++ api/v1alpha1/zz_generated.deepcopy.go | 58 +++++++ ...keda.sh_clustertriggerauthentications.yaml | 153 ++++++++++++++++++ config/crd/bases/keda.sh_scaledjobs.yaml | 7 +- config/crd/bases/keda.sh_scaledobjects.yaml | 7 +- pkg/scaling/resolver/scale_resolvers.go | 69 ++++++-- pkg/scaling/resolver/scale_resolvers_test.go | 85 ++++++++++ 9 files changed, 396 insertions(+), 15 deletions(-) create mode 100644 config/crd/bases/keda.sh_clustertriggerauthentications.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index c8a3d1d1b8a..4ad42af1dab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ ### New - Can use Pod Identity with Azure Event Hub scaler ([#994](https://github.com/kedacore/keda/issues/994)) - Introducing InfluxDB scaler ([#1239](https://github.com/kedacore/keda/issues/1239)) +- Global authentication credentials can be managed using ClusterTriggerAuthentication objects ([#1452](https://github.com/kedacore/keda/pull/1452)) ### Improvements - Support add ScaledJob's label to its job ([#1311](https://github.com/kedacore/keda/issues/1311)) diff --git a/api/v1alpha1/scaledobject_types.go b/api/v1alpha1/scaledobject_types.go index 2a0cc35b184..5cbf6465546 100644 --- a/api/v1alpha1/scaledobject_types.go +++ b/api/v1alpha1/scaledobject_types.go @@ -107,10 +107,13 @@ type ScaledObjectList struct { Items []ScaledObject `json:"items"` } -// ScaledObjectAuthRef points to the TriggerAuthentication object that +// ScaledObjectAuthRef points to the TriggerAuthentication or ClusterTriggerAuthentication object that // is used to authenticate the scaler with the environment type ScaledObjectAuthRef struct { Name string `json:"name"` + // Kind of the resource being referred to. Defaults to TriggerAuthentication. + // +optional + Kind string `json:"kind,omitempty"` } func init() { diff --git a/api/v1alpha1/triggerauthentication_types.go b/api/v1alpha1/triggerauthentication_types.go index 656c50cf65a..964f69bce3e 100644 --- a/api/v1alpha1/triggerauthentication_types.go +++ b/api/v1alpha1/triggerauthentication_types.go @@ -6,6 +6,31 @@ import ( // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// ClusterTriggerAuthentication defines how a trigger can authenticate globally +// +genclient +// +genclient:nonNamespaced +// +kubebuilder:resource:path=clustertriggerauthentications,scope=Cluster,shortName=cta;clustertriggerauth +// +kubebuilder:printcolumn:name="PodIdentity",type="string",JSONPath=".spec.podIdentity.provider" +// +kubebuilder:printcolumn:name="Secret",type="string",JSONPath=".spec.secretTargetRef[*].name" +// +kubebuilder:printcolumn:name="Env",type="string",JSONPath=".spec.env[*].name" +type ClusterTriggerAuthentication struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec TriggerAuthenticationSpec `json:"spec"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ClusterTriggerAuthenticationList contains a list of ClusterTriggerAuthentication +type ClusterTriggerAuthenticationList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata"` + Items []ClusterTriggerAuthentication `json:"items"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + // TriggerAuthentication defines how a trigger can authenticate // +genclient // +kubebuilder:resource:path=triggerauthentications,scope=Namespaced,shortName=ta;triggerauth @@ -130,5 +155,6 @@ type VaultSecret struct { } func init() { + SchemeBuilder.Register(&ClusterTriggerAuthentication{}, &ClusterTriggerAuthenticationList{}) SchemeBuilder.Register(&TriggerAuthentication{}, &TriggerAuthenticationList{}) } diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 3ca8841e234..b5afe3ee424 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -91,6 +91,64 @@ func (in *AuthSecretTargetRef) DeepCopy() *AuthSecretTargetRef { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterTriggerAuthentication) DeepCopyInto(out *ClusterTriggerAuthentication) { + *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 ClusterTriggerAuthentication. +func (in *ClusterTriggerAuthentication) DeepCopy() *ClusterTriggerAuthentication { + if in == nil { + return nil + } + out := new(ClusterTriggerAuthentication) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterTriggerAuthentication) 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 *ClusterTriggerAuthenticationList) DeepCopyInto(out *ClusterTriggerAuthenticationList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ClusterTriggerAuthentication, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterTriggerAuthenticationList. +func (in *ClusterTriggerAuthenticationList) DeepCopy() *ClusterTriggerAuthenticationList { + if in == nil { + return nil + } + out := new(ClusterTriggerAuthenticationList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterTriggerAuthenticationList) 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 *Condition) DeepCopyInto(out *Condition) { *out = *in diff --git a/config/crd/bases/keda.sh_clustertriggerauthentications.yaml b/config/crd/bases/keda.sh_clustertriggerauthentications.yaml new file mode 100644 index 00000000000..4e95110c14d --- /dev/null +++ b/config/crd/bases/keda.sh_clustertriggerauthentications.yaml @@ -0,0 +1,153 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.3.0 + creationTimestamp: null + name: clustertriggerauthentications.keda.sh +spec: + group: keda.sh + names: + kind: ClusterTriggerAuthentication + listKind: ClusterTriggerAuthenticationList + plural: clustertriggerauthentications + shortNames: + - cta + - clustertriggerauth + singular: clustertriggerauthentication + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .spec.podIdentity.provider + name: PodIdentity + type: string + - jsonPath: .spec.secretTargetRef[*].name + name: Secret + type: string + - jsonPath: .spec.env[*].name + name: Env + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: ClusterTriggerAuthentication defines how a trigger can authenticate + globally + 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: + description: TriggerAuthenticationSpec defines the various ways to authenticate + properties: + env: + items: + description: AuthEnvironment is used to authenticate using environment + variables in the destination ScaleTarget spec + properties: + containerName: + type: string + name: + type: string + parameter: + type: string + required: + - name + - parameter + type: object + type: array + hashiCorpVault: + description: HashiCorpVault is used to authenticate using Hashicorp + Vault + properties: + address: + type: string + authentication: + description: VaultAuthentication contains the list of Hashicorp + Vault authentication methods + type: string + credential: + description: Credential defines the Hashicorp Vault credentials + depending on the authentication method + properties: + serviceAccount: + type: string + token: + type: string + type: object + mount: + type: string + role: + type: string + secrets: + items: + description: VaultSecret defines the mapping between the path + of the secret in Vault to the parameter + properties: + key: + type: string + parameter: + type: string + path: + type: string + required: + - key + - parameter + - path + type: object + type: array + required: + - address + - authentication + - secrets + type: object + podIdentity: + description: AuthPodIdentity allows users to select the platform native + identity mechanism + properties: + provider: + description: PodIdentityProvider contains the list of providers + type: string + required: + - provider + type: object + secretTargetRef: + items: + description: AuthSecretTargetRef is used to authenticate using a + reference to a secret + properties: + key: + type: string + name: + type: string + parameter: + type: string + required: + - key + - name + - parameter + type: object + type: array + type: object + required: + - spec + type: object + served: true + storage: true + subresources: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/config/crd/bases/keda.sh_scaledjobs.yaml b/config/crd/bases/keda.sh_scaledjobs.yaml index 4db5592cdc4..ccd848dc0b8 100644 --- a/config/crd/bases/keda.sh_scaledjobs.yaml +++ b/config/crd/bases/keda.sh_scaledjobs.yaml @@ -6363,8 +6363,13 @@ spec: properties: authenticationRef: description: ScaledObjectAuthRef points to the TriggerAuthentication - object that is used to authenticate the scaler with the environment + or ClusterTriggerAuthentication object that is used to authenticate + the scaler with the environment properties: + kind: + description: Kind of the resource being referred to. Defaults + to TriggerAuthentication. + type: string name: type: string required: diff --git a/config/crd/bases/keda.sh_scaledobjects.yaml b/config/crd/bases/keda.sh_scaledobjects.yaml index ab6fe3dc45e..40fb95b88e1 100644 --- a/config/crd/bases/keda.sh_scaledobjects.yaml +++ b/config/crd/bases/keda.sh_scaledobjects.yaml @@ -224,8 +224,13 @@ spec: properties: authenticationRef: description: ScaledObjectAuthRef points to the TriggerAuthentication - object that is used to authenticate the scaler with the environment + or ClusterTriggerAuthentication object that is used to authenticate + the scaler with the environment properties: + kind: + description: Kind of the resource being referred to. Defaults + to TriggerAuthentication. + type: string name: type: string required: diff --git a/pkg/scaling/resolver/scale_resolvers.go b/pkg/scaling/resolver/scale_resolvers.go index b46e63948c1..4141e29d10e 100644 --- a/pkg/scaling/resolver/scale_resolvers.go +++ b/pkg/scaling/resolver/scale_resolvers.go @@ -4,6 +4,8 @@ import ( "bytes" "context" "fmt" + "io/ioutil" + "os" "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" @@ -54,16 +56,15 @@ func ResolveAuthRef(client client.Client, logger logr.Logger, triggerAuthRef *ke var podIdentity kedav1alpha1.PodIdentityProvider if namespace != "" && triggerAuthRef != nil && triggerAuthRef.Name != "" { - triggerAuth := &kedav1alpha1.TriggerAuthentication{} - err := client.Get(context.TODO(), types.NamespacedName{Name: triggerAuthRef.Name, Namespace: namespace}, triggerAuth) + triggerAuthSpec, triggerNamespace, err := getTriggerAuthSpec(client, triggerAuthRef, namespace) if err != nil { logger.Error(err, "Error getting triggerAuth", "triggerAuthRef.Name", triggerAuthRef.Name) } else { - if triggerAuth.Spec.PodIdentity != nil { - podIdentity = triggerAuth.Spec.PodIdentity.Provider + if triggerAuthSpec.PodIdentity != nil { + podIdentity = triggerAuthSpec.PodIdentity.Provider } - if triggerAuth.Spec.Env != nil { - for _, e := range triggerAuth.Spec.Env { + if triggerAuthSpec.Env != nil { + for _, e := range triggerAuthSpec.Env { if podSpec == nil { result[e.Parameter] = "" continue @@ -76,18 +77,18 @@ func ResolveAuthRef(client client.Client, logger logr.Logger, triggerAuthRef *ke } } } - if triggerAuth.Spec.SecretTargetRef != nil { - for _, e := range triggerAuth.Spec.SecretTargetRef { - result[e.Parameter] = resolveAuthSecret(client, logger, e.Name, namespace, e.Key) + if triggerAuthSpec.SecretTargetRef != nil { + for _, e := range triggerAuthSpec.SecretTargetRef { + result[e.Parameter] = resolveAuthSecret(client, logger, e.Name, triggerNamespace, e.Key) } } - if triggerAuth.Spec.HashiCorpVault != nil && len(triggerAuth.Spec.HashiCorpVault.Secrets) > 0 { - vault := NewHashicorpVaultHandler(triggerAuth.Spec.HashiCorpVault) + if triggerAuthSpec.HashiCorpVault != nil && len(triggerAuthSpec.HashiCorpVault.Secrets) > 0 { + vault := NewHashicorpVaultHandler(triggerAuthSpec.HashiCorpVault) err := vault.Initialize(logger) if err != nil { logger.Error(err, "Error authenticate to Vault", "triggerAuthRef.Name", triggerAuthRef.Name) } else { - for _, e := range triggerAuth.Spec.HashiCorpVault.Secrets { + for _, e := range triggerAuthSpec.HashiCorpVault.Secrets { secret, err := vault.Read(e.Path) if err != nil { logger.Error(err, "Error trying to read secret from Vault", "triggerAuthRef.Name", triggerAuthRef.Name, @@ -107,6 +108,50 @@ func ResolveAuthRef(client client.Client, logger logr.Logger, triggerAuthRef *ke return result, podIdentity } +var clusterObjectNamespaceCache *string + +func getClusterObjectNamespace() (string, error) { + // Check if a cached value is available. + if clusterObjectNamespaceCache != nil { + return *clusterObjectNamespaceCache, nil + } + env := os.Getenv("KEDA_CLUSTER_OBJECT_NAMESPACE") + if env != "" { + clusterObjectNamespaceCache = &env + return env, nil + } + data, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace") + if err != nil { + return "", err + } + strData := string(data) + clusterObjectNamespaceCache = &strData + return strData, nil +} + +func getTriggerAuthSpec(client client.Client, triggerAuthRef *kedav1alpha1.ScaledObjectAuthRef, namespace string) (*kedav1alpha1.TriggerAuthenticationSpec, string, error) { + if triggerAuthRef.Kind == "" || triggerAuthRef.Kind == "TriggerAuthentication" { + triggerAuth := &kedav1alpha1.TriggerAuthentication{} + err := client.Get(context.TODO(), types.NamespacedName{Name: triggerAuthRef.Name, Namespace: namespace}, triggerAuth) + if err != nil { + return nil, "", err + } + return &triggerAuth.Spec, namespace, nil + } else if triggerAuthRef.Kind == "ClusterTriggerAuthentication" { + clusterNamespace, err := getClusterObjectNamespace() + if err != nil { + return nil, "", err + } + triggerAuth := &kedav1alpha1.ClusterTriggerAuthentication{} + err = client.Get(context.TODO(), types.NamespacedName{Name: triggerAuthRef.Name}, triggerAuth) + if err != nil { + return nil, "", err + } + return &triggerAuth.Spec, clusterNamespace, nil + } + return nil, "", fmt.Errorf("unknown trigger auth kind %s", triggerAuthRef.Kind) +} + func resolveEnv(client client.Client, logger logr.Logger, container *corev1.Container, namespace string) (map[string]string, error) { resolved := make(map[string]string) diff --git a/pkg/scaling/resolver/scale_resolvers_test.go b/pkg/scaling/resolver/scale_resolvers_test.go index 6bb976a6a84..e77da21e79a 100644 --- a/pkg/scaling/resolver/scale_resolvers_test.go +++ b/pkg/scaling/resolver/scale_resolvers_test.go @@ -15,6 +15,7 @@ import ( var ( namespace = "test-namespace" + clusterNamespace = "keda" triggerAuthenticationName = "triggerauth" secretName = "supersecret" secretKey = "mysecretkey" @@ -223,10 +224,94 @@ func TestResolveAuthRef(t *testing.T) { expected: map[string]string{"host": secretData}, expectedPodIdentity: kedav1alpha1.PodIdentityProviderNone, }, + { + name: "clustertriggerauth exists, podidentity nil", + existing: []runtime.Object{ + &kedav1alpha1.ClusterTriggerAuthentication{ + ObjectMeta: metav1.ObjectMeta{ + Name: triggerAuthenticationName, + }, + Spec: kedav1alpha1.TriggerAuthenticationSpec{ + SecretTargetRef: []kedav1alpha1.AuthSecretTargetRef{ + { + Parameter: "host", + Name: secretName, + Key: secretKey, + }, + }, + }, + }, + }, + soar: &kedav1alpha1.ScaledObjectAuthRef{Name: triggerAuthenticationName, Kind: "ClusterTriggerAuthentication"}, + expected: map[string]string{"host": ""}, + }, + { + name: "clustertriggerauth exists and secret", + existing: []runtime.Object{ + &kedav1alpha1.ClusterTriggerAuthentication{ + ObjectMeta: metav1.ObjectMeta{ + Name: triggerAuthenticationName, + }, + Spec: kedav1alpha1.TriggerAuthenticationSpec{ + PodIdentity: &kedav1alpha1.AuthPodIdentity{ + Provider: kedav1alpha1.PodIdentityProviderNone, + }, + SecretTargetRef: []kedav1alpha1.AuthSecretTargetRef{ + { + Parameter: "host", + Name: secretName, + Key: secretKey, + }, + }, + }, + }, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: clusterNamespace, + Name: secretName, + }, + Data: map[string][]byte{secretKey: []byte(secretData)}}, + }, + soar: &kedav1alpha1.ScaledObjectAuthRef{Name: triggerAuthenticationName, Kind: "ClusterTriggerAuthentication"}, + expected: map[string]string{"host": secretData}, + expectedPodIdentity: kedav1alpha1.PodIdentityProviderNone, + }, + { + name: "clustertriggerauth exists and secret in the wrong namespace", + existing: []runtime.Object{ + &kedav1alpha1.ClusterTriggerAuthentication{ + ObjectMeta: metav1.ObjectMeta{ + Name: triggerAuthenticationName, + }, + Spec: kedav1alpha1.TriggerAuthenticationSpec{ + PodIdentity: &kedav1alpha1.AuthPodIdentity{ + Provider: kedav1alpha1.PodIdentityProviderNone, + }, + SecretTargetRef: []kedav1alpha1.AuthSecretTargetRef{ + { + Parameter: "host", + Name: secretName, + Key: secretKey, + }, + }, + }, + }, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: secretName, + }, + Data: map[string][]byte{secretKey: []byte(secretData)}}, + }, + soar: &kedav1alpha1.ScaledObjectAuthRef{Name: triggerAuthenticationName, Kind: "ClusterTriggerAuthentication"}, + expected: map[string]string{"host": ""}, + expectedPodIdentity: kedav1alpha1.PodIdentityProviderNone, + }, } for _, test := range tests { test := test t.Run(test.name, func(t *testing.T) { + clusterObjectNamespaceCache = &clusterNamespace // Inject test cluster namespace. gotMap, gotPodIdentity := ResolveAuthRef(fake.NewFakeClientWithScheme(scheme.Scheme, test.existing...), logf.Log.WithName("test"), test.soar, test.podSpec, namespace) if diff := cmp.Diff(gotMap, test.expected); diff != "" { t.Errorf("Returned authParams are different: %s", diff)