diff --git a/CHANGELOG.md b/CHANGELOG.md index 20e654028b6..4b601610651 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,7 +35,7 @@ - **General:** Introduce new GCP Stackdriver Scaler ([#2661](https://github.com/kedacore/keda/issues/2661)) - **General:** Introduce new GCP Storage Scaler ([#2628](https://github.com/kedacore/keda/issues/2628)) - **General:** Introduce ARM-based container image for KEDA ([#2263](https://github.com/kedacore/keda/issues/2263)|[#2262](https://github.com/kedacore/keda/issues/2262)) -- **General:** Provide support for authentication via Azure Key Vault ([#900](https://github.com/kedacore/keda/issues/900)) +- **General:** Provide support for authentication via Azure Key Vault ([#900](https://github.com/kedacore/keda/issues/900)|[#2733](https://github.com/kedacore/keda/issues/2733)) ### Improvements diff --git a/apis/keda/v1alpha1/triggerauthentication_types.go b/apis/keda/v1alpha1/triggerauthentication_types.go index 888dc36b989..a04466f669d 100644 --- a/apis/keda/v1alpha1/triggerauthentication_types.go +++ b/apis/keda/v1alpha1/triggerauthentication_types.go @@ -183,6 +183,8 @@ type AzureKeyVault struct { VaultURI string `json:"vaultUri"` Credentials *AzureKeyVaultCredentials `json:"credentials"` Secrets []AzureKeyVaultSecret `json:"secrets"` + // +optional + Cloud *AzureKeyVaultCloudInfo `json:"cloud"` } type AzureKeyVaultCredentials struct { @@ -211,6 +213,14 @@ type AzureKeyVaultSecret struct { Version string `json:"version,omitempty"` } +type AzureKeyVaultCloudInfo struct { + Type string `json:"type"` + // +optional + KeyVaultResourceURL string `json:"keyVaultResourceURL"` + // +optional + ActiveDirectoryEndpoint string `json:"activeDirectoryEndpoint"` +} + func init() { SchemeBuilder.Register(&ClusterTriggerAuthentication{}, &ClusterTriggerAuthenticationList{}) SchemeBuilder.Register(&TriggerAuthentication{}, &TriggerAuthenticationList{}) diff --git a/apis/keda/v1alpha1/zz_generated.deepcopy.go b/apis/keda/v1alpha1/zz_generated.deepcopy.go index 65cc27d2276..7ecccfa4044 100644 --- a/apis/keda/v1alpha1/zz_generated.deepcopy.go +++ b/apis/keda/v1alpha1/zz_generated.deepcopy.go @@ -105,6 +105,11 @@ func (in *AzureKeyVault) DeepCopyInto(out *AzureKeyVault) { *out = make([]AzureKeyVaultSecret, len(*in)) copy(*out, *in) } + if in.Cloud != nil { + in, out := &in.Cloud, &out.Cloud + *out = new(AzureKeyVaultCloudInfo) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AzureKeyVault. @@ -133,6 +138,21 @@ func (in *AzureKeyVaultClientSecret) DeepCopy() *AzureKeyVaultClientSecret { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AzureKeyVaultCloudInfo) DeepCopyInto(out *AzureKeyVaultCloudInfo) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AzureKeyVaultCloudInfo. +func (in *AzureKeyVaultCloudInfo) DeepCopy() *AzureKeyVaultCloudInfo { + if in == nil { + return nil + } + out := new(AzureKeyVaultCloudInfo) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AzureKeyVaultCredentials) DeepCopyInto(out *AzureKeyVaultCredentials) { *out = *in diff --git a/config/crd/bases/keda.sh_clustertriggerauthentications.yaml b/config/crd/bases/keda.sh_clustertriggerauthentications.yaml index f6e472206c6..a07a808c67b 100644 --- a/config/crd/bases/keda.sh_clustertriggerauthentications.yaml +++ b/config/crd/bases/keda.sh_clustertriggerauthentications.yaml @@ -57,6 +57,17 @@ spec: description: AzureKeyVault is used to authenticate using Azure Key Vault properties: + cloud: + properties: + activeDirectoryEndpoint: + type: string + keyVaultResourceURL: + type: string + type: + type: string + required: + - type + type: object credentials: properties: clientId: diff --git a/config/crd/bases/keda.sh_triggerauthentications.yaml b/config/crd/bases/keda.sh_triggerauthentications.yaml index 8729e9c2d5a..a8ad47f2279 100644 --- a/config/crd/bases/keda.sh_triggerauthentications.yaml +++ b/config/crd/bases/keda.sh_triggerauthentications.yaml @@ -56,6 +56,17 @@ spec: description: AzureKeyVault is used to authenticate using Azure Key Vault properties: + cloud: + properties: + activeDirectoryEndpoint: + type: string + keyVaultResourceURL: + type: string + type: + type: string + required: + - type + type: object credentials: properties: clientId: diff --git a/pkg/scaling/resolver/azure_keyvault_handler.go b/pkg/scaling/resolver/azure_keyvault_handler.go index cd3b2e9ac33..68784728091 100644 --- a/pkg/scaling/resolver/azure_keyvault_handler.go +++ b/pkg/scaling/resolver/azure_keyvault_handler.go @@ -18,17 +18,17 @@ package resolver import ( "context" + "fmt" + "strings" "github.com/Azure/azure-sdk-for-go/services/keyvault/v7.0/keyvault" + az "github.com/Azure/go-autorest/autorest/azure" "github.com/Azure/go-autorest/autorest/azure/auth" "github.com/go-logr/logr" "sigs.k8s.io/controller-runtime/pkg/client" kedav1alpha1 "github.com/kedacore/keda/v2/apis/keda/v1alpha1" -) - -const ( - azureKeyVaultResource = "https://vault.azure.net" + "github.com/kedacore/keda/v2/pkg/scalers/azure" ) type AzureKeyVaultHandler struct { @@ -51,7 +51,13 @@ func (vh *AzureKeyVaultHandler) Initialize(ctx context.Context, client client.Cl clientSecret := resolveAuthSecret(ctx, client, logger, clientSecretName, triggerNamespace, clientSecretKey) clientCredentialsConfig := auth.NewClientCredentialsConfig(clientID, clientSecret, tenantID) - clientCredentialsConfig.Resource = azureKeyVaultResource + + keyvaultResourceURL, activeDirectoryEndpoint, err := vh.getPropertiesForCloud() + if err != nil { + return err + } + clientCredentialsConfig.Resource = keyvaultResourceURL + clientCredentialsConfig.AADEndpoint = activeDirectoryEndpoint authorizer, err := clientCredentialsConfig.Authorizer() if err != nil { @@ -74,3 +80,28 @@ func (vh *AzureKeyVaultHandler) Read(ctx context.Context, secretName string, ver return *result.Value, nil } + +func (vh *AzureKeyVaultHandler) getPropertiesForCloud() (string, string, error) { + cloud := vh.vault.Cloud + + if cloud == nil { + return az.PublicCloud.ResourceIdentifiers.KeyVault, az.PublicCloud.ActiveDirectoryEndpoint, nil + } + + if strings.EqualFold(cloud.Type, azure.PrivateCloud) { + if cloud.KeyVaultResourceURL == "" || cloud.ActiveDirectoryEndpoint == "" { + err := fmt.Errorf("properties keyVaultResourceURL and activeDirectoryEndpoint must be provided for cloud %s", + azure.PrivateCloud) + return "", "", err + } + + return cloud.KeyVaultResourceURL, cloud.ActiveDirectoryEndpoint, nil + } + + env, err := az.EnvironmentFromName(cloud.Type) + if err != nil { + return "", "", err + } + + return env.ResourceIdentifiers.KeyVault, env.ActiveDirectoryEndpoint, nil +} diff --git a/pkg/scaling/resolver/azure_keyvault_handler_test.go b/pkg/scaling/resolver/azure_keyvault_handler_test.go new file mode 100644 index 00000000000..4a6740f567d --- /dev/null +++ b/pkg/scaling/resolver/azure_keyvault_handler_test.go @@ -0,0 +1,135 @@ +/* +Copyright 2022 The KEDA 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 resolver + +import ( + "testing" + + az "github.com/Azure/go-autorest/autorest/azure" + + kedav1alpha1 "github.com/kedacore/keda/v2/apis/keda/v1alpha1" +) + +const ( + testResourceURL = "testResourceURL" + testActiveDirectoryEndpoint = "testActiveDirectoryEndpoint" +) + +type testData struct { + name string + isError bool + vault kedav1alpha1.AzureKeyVault + expectedKVResourceURL string + expectedADEndpoint string +} + +var testDataset = []testData{ + { + name: "known Azure cloud", + isError: false, + vault: kedav1alpha1.AzureKeyVault{ + Cloud: &kedav1alpha1.AzureKeyVaultCloudInfo{ + Type: "azurePublicCloud", + }, + }, + expectedKVResourceURL: az.PublicCloud.ResourceIdentifiers.KeyVault, + expectedADEndpoint: az.PublicCloud.ActiveDirectoryEndpoint, + }, + { + name: "private cloud", + isError: false, + vault: kedav1alpha1.AzureKeyVault{ + Cloud: &kedav1alpha1.AzureKeyVaultCloudInfo{ + Type: "private", + KeyVaultResourceURL: testResourceURL, + ActiveDirectoryEndpoint: testActiveDirectoryEndpoint, + }, + }, + expectedKVResourceURL: testResourceURL, + expectedADEndpoint: testActiveDirectoryEndpoint, + }, + { + name: "nil cloud info", + isError: false, + vault: kedav1alpha1.AzureKeyVault{ + Cloud: nil, + }, + expectedKVResourceURL: az.PublicCloud.ResourceIdentifiers.KeyVault, + expectedADEndpoint: az.PublicCloud.ActiveDirectoryEndpoint, + }, + { + name: "invalid cloud", + isError: true, + vault: kedav1alpha1.AzureKeyVault{ + Cloud: &kedav1alpha1.AzureKeyVaultCloudInfo{ + Type: "invalid cloud", + }, + }, + expectedKVResourceURL: "", + expectedADEndpoint: "", + }, + { + name: "private cloud missing keyvault resource URL", + isError: true, + vault: kedav1alpha1.AzureKeyVault{ + Cloud: &kedav1alpha1.AzureKeyVaultCloudInfo{ + Type: "private", + ActiveDirectoryEndpoint: testActiveDirectoryEndpoint, + }, + }, + expectedKVResourceURL: "", + expectedADEndpoint: "", + }, + { + name: "private cloud missing active directory endpoint", + isError: true, + vault: kedav1alpha1.AzureKeyVault{ + Cloud: &kedav1alpha1.AzureKeyVaultCloudInfo{ + Type: "private", + KeyVaultResourceURL: testResourceURL, + }, + }, + expectedKVResourceURL: "", + expectedADEndpoint: "", + }, +} + +func TestGetPropertiesForCloud(t *testing.T) { + for _, testData := range testDataset { + vh := NewAzureKeyVaultHandler(&testData.vault) + + kvResourceURL, adEndpoint, err := vh.getPropertiesForCloud() + + if err != nil && !testData.isError { + t.Fatalf("test %s: expected success but got error - %s", testData.name, err) + } + + if err == nil && testData.isError { + t.Fatalf("test %s: expected error but got success, testData - %+v", testData.name, testData) + } + + if kvResourceURL != testData.expectedKVResourceURL { + t.Errorf("test %s: keyvault resource URl does not match. expected - %s, got - %s", + testData.name, testData.expectedKVResourceURL, kvResourceURL) + } + + if adEndpoint != testData.expectedADEndpoint { + t.Errorf("test %s: active directory endpoint does not match. expected - %s, got - %s", + testData.name, testData.expectedADEndpoint, adEndpoint) + } + } +}