diff --git a/pkg/recipes/terraform/config/config.go b/pkg/recipes/terraform/config/config.go index 30ff5691a4..13fa0d0615 100644 --- a/pkg/recipes/terraform/config/config.go +++ b/pkg/recipes/terraform/config/config.go @@ -113,8 +113,8 @@ func (cfg *TerraformConfig) Save(ctx context.Context, workingDir string) error { // by Radius to generate custom provider configurations. Save() must be called to save // the generated providers config. requiredProviders contains a list of provider names // that are required for the module. -func (cfg *TerraformConfig) AddProviders(ctx context.Context, requiredProviders []string, supportedProviders map[string]providers.Provider, envConfig *recipes.Configuration) error { - providerConfigs, err := getProviderConfigs(ctx, requiredProviders, supportedProviders, envConfig) +func (cfg *TerraformConfig) AddProviders(ctx context.Context, requiredProviders []string, ucpConfiguredProviders map[string]providers.Provider, envConfig *recipes.Configuration) error { + providerConfigs, err := getProviderConfigs(ctx, requiredProviders, ucpConfiguredProviders, envConfig) if err != nil { return err } @@ -165,13 +165,23 @@ func newModuleConfig(moduleSource string, moduleVersion string, params ...Recipe return moduleConfig } -// getProviderConfigs generates the Terraform provider configurations for the required providers. -func getProviderConfigs(ctx context.Context, requiredProviders []string, supportedProviders map[string]providers.Provider, envConfig *recipes.Configuration) (map[string]any, error) { - providerConfigs := make(map[string]any) +// getProviderConfigs generates the Terraform provider configurations. This is built from a combination of environment level recipe configuration for +// providers and the provider configurations registered with UCP. The environment level recipe configuration for providers takes precedence over UCP provider configurations. +func getProviderConfigs(ctx context.Context, requiredProviders []string, ucpConfiguredProviders map[string]providers.Provider, envConfig *recipes.Configuration) (map[string]any, error) { + // Get recipe provider configurations from the environment configuration + providerConfigs := providers.GetRecipeProviderConfigs(ctx, envConfig) + + // Build provider configurations for required providers excluding the ones already present in providerConfigs for _, provider := range requiredProviders { - builder, ok := supportedProviders[provider] + if _, ok := providerConfigs[provider]; ok { + // Environment level recipe configuration for providers will take precedence over + // UCP provider configuration (currently these include azurerm, aws, kubernetes providers) + continue + } + + builder, ok := ucpConfiguredProviders[provider] if !ok { - // No-op: For any other provider, Radius doesn't generate any custom configuration. + // No-op: For any other provider under required_providers, Radius doesn't generate any custom configuration. continue } diff --git a/pkg/recipes/terraform/config/config_test.go b/pkg/recipes/terraform/config/config_test.go index a9f1f90174..c12d3d1527 100644 --- a/pkg/recipes/terraform/config/config_test.go +++ b/pkg/recipes/terraform/config/config_test.go @@ -331,7 +331,7 @@ func Test_AddRecipeContext(t *testing.T) { } func Test_AddProviders(t *testing.T) { - mProvider, supportedProviders, mBackend := setup(t) + mProvider, ucpConfiguredProviders, mBackend := setup(t) envRecipe, resourceRecipe := getTestInputs() expectedBackend := map[string]any{ "kubernetes": map[string]any{ @@ -340,17 +340,18 @@ func Test_AddProviders(t *testing.T) { "namespace": "radius-system", }, } + configTests := []struct { - desc string - envConfig recipes.Configuration - requiredProviders []string - expectedProviders []map[string]any - expectedConfigFile string - Err error + desc string + envConfig recipes.Configuration + requiredProviders []string + expectedUCPConfiguredProviders []map[string]any + expectedConfigFile string + Err error }{ { desc: "valid all supported providers", - expectedProviders: []map[string]any{ + expectedUCPConfiguredProviders: []map[string]any{ { "region": "test-region", }, @@ -379,13 +380,12 @@ func Test_AddProviders(t *testing.T) { providers.KubernetesProviderName, "sql", }, - expectedConfigFile: "testdata/providers-valid.tf.json", }, { - desc: "invalid aws scope", - expectedProviders: nil, - Err: errors.New("Invalid AWS provider scope"), + desc: "invalid aws scope", + expectedUCPConfiguredProviders: nil, + Err: errors.New("Invalid AWS provider scope"), envConfig: recipes.Configuration{ Providers: datamodel.Providers{ AWS: datamodel.ProvidersAWS{ @@ -399,7 +399,7 @@ func Test_AddProviders(t *testing.T) { }, { desc: "empty aws provider config", - expectedProviders: []map[string]any{ + expectedUCPConfiguredProviders: []map[string]any{ {}, }, Err: nil, @@ -411,7 +411,7 @@ func Test_AddProviders(t *testing.T) { }, { desc: "empty aws scope", - expectedProviders: []map[string]any{ + expectedUCPConfiguredProviders: []map[string]any{ nil, }, Err: nil, @@ -432,7 +432,7 @@ func Test_AddProviders(t *testing.T) { }, { desc: "empty azure provider config", - expectedProviders: []map[string]any{ + expectedUCPConfiguredProviders: []map[string]any{ { "features": map[string]any{}, }, @@ -444,6 +444,134 @@ func Test_AddProviders(t *testing.T) { }, expectedConfigFile: "testdata/providers-emptyazureconfig.tf.json", }, + { + desc: "valid recipe providers in env config", + expectedUCPConfiguredProviders: nil, + Err: nil, + envConfig: recipes.Configuration{ + RecipeConfig: datamodel.RecipeConfigProperties{ + Terraform: datamodel.TerraformConfigProperties{ + Providers: map[string][]datamodel.ProviderConfigProperties{ + "azurerm": { + { + AdditionalProperties: map[string]any{ + "subscriptionid": 1234, + "tenant_id": "745fg88bf-86f1-41af-43ut", + }, + }, + { + AdditionalProperties: map[string]any{ + "alias": "az-paymentservice", + "subscriptionid": 45678, + "tenant_id": "gfhf45345-5d73-gh34-wh84", + }, + }, + }, + }, + }, + }, + }, + requiredProviders: nil, + expectedConfigFile: "testdata/providers-envrecipeproviders.tf.json", + }, + { + desc: "recipe provider config overridding required provider configs", + expectedUCPConfiguredProviders: []map[string]any{ + { + "region": "test-region", + }, + }, + Err: nil, + envConfig: recipes.Configuration{ + RecipeConfig: datamodel.RecipeConfigProperties{ + Terraform: datamodel.TerraformConfigProperties{ + Providers: map[string][]datamodel.ProviderConfigProperties{ + "kubernetes": { + { + AdditionalProperties: map[string]any{ + "ConfigPath": "/home/radius/.kube/configPath1", + }, + }, + { + AdditionalProperties: map[string]any{ + "ConfigPath": "/home/radius/.kube/configPath2", + }, + }, + }, + }, + }, + }, + }, + requiredProviders: []string{ + providers.AWSProviderName, + providers.KubernetesProviderName, + }, + expectedConfigFile: "testdata/providers-overridereqproviders.tf.json", + }, + { + desc: "recipe providers in env config setup but nil", + expectedUCPConfiguredProviders: nil, + Err: nil, + envConfig: recipes.Configuration{ + RecipeConfig: datamodel.RecipeConfigProperties{ + Terraform: datamodel.TerraformConfigProperties{ + Providers: map[string][]datamodel.ProviderConfigProperties{ + "azurerm": { + { + AdditionalProperties: nil, + }, + { + AdditionalProperties: map[string]any{ + "alias": "az-paymentservice", + "subscriptionid": 45678, + "tenant_id": "gfhf45345-5d73-gh34-wh84", + }, + }, + }, + }, + }, + }, + }, + requiredProviders: nil, + expectedConfigFile: "testdata/providers-envrecipedefaultconfig.tf.json", + }, + { + desc: "recipe providers not populated", + expectedUCPConfiguredProviders: nil, + Err: nil, + envConfig: recipes.Configuration{ + RecipeConfig: datamodel.RecipeConfigProperties{ + Terraform: datamodel.TerraformConfigProperties{}, + }, + }, + requiredProviders: nil, + expectedConfigFile: "testdata/providers-empty.tf.json", + }, + { + desc: "recipe providers and tfconfigproperties not populated", + expectedUCPConfiguredProviders: nil, + Err: nil, + envConfig: recipes.Configuration{ + RecipeConfig: datamodel.RecipeConfigProperties{}, + }, + requiredProviders: nil, + expectedConfigFile: "testdata/providers-empty.tf.json", + }, + { + desc: "envConfig set to empty recipe config", + expectedUCPConfiguredProviders: nil, + Err: nil, + envConfig: recipes.Configuration{}, + requiredProviders: nil, + expectedConfigFile: "testdata/providers-empty.tf.json", + }, + { + desc: "envConfig not populated", + expectedUCPConfiguredProviders: nil, + Err: nil, + requiredProviders: nil, + expectedConfigFile: "testdata/providers-empty.tf.json", + }, } for _, tc := range configTests { @@ -451,15 +579,15 @@ func Test_AddProviders(t *testing.T) { ctx := testcontext.New(t) workingDir := t.TempDir() - tfconfig, err := New(context.Background(), testRecipeName, &envRecipe, &resourceRecipe, nil) + tfconfig, err := New(ctx, testRecipeName, &envRecipe, &resourceRecipe, &tc.envConfig) require.NoError(t, err) - for _, p := range tc.expectedProviders { + for _, p := range tc.expectedUCPConfiguredProviders { mProvider.EXPECT().BuildConfig(ctx, &tc.envConfig).Times(1).Return(p, nil) } if tc.Err != nil { mProvider.EXPECT().BuildConfig(ctx, &tc.envConfig).Times(1).Return(nil, tc.Err) } - err = tfconfig.AddProviders(ctx, tc.requiredProviders, supportedProviders, &tc.envConfig) + err = tfconfig.AddProviders(ctx, tc.requiredProviders, ucpConfiguredProviders, &tc.envConfig) if tc.Err != nil { require.ErrorContains(t, err, tc.Err.Error()) return diff --git a/pkg/recipes/terraform/config/providers/types.go b/pkg/recipes/terraform/config/providers/types.go index 7a12fec82c..66fbf29090 100644 --- a/pkg/recipes/terraform/config/providers/types.go +++ b/pkg/recipes/terraform/config/providers/types.go @@ -34,13 +34,39 @@ type Provider interface { BuildConfig(ctx context.Context, envConfig *recipes.Configuration) (map[string]any, error) } -// GetSupportedTerraformProviders returns a map of Terraform provider names to provider config builder. -// Providers represent Terraform providers for which Radius generates custom provider configurations. -// For example, the Azure subscription id is added to Azure provider config using Radius Environment's Azure provider scope. -func GetSupportedTerraformProviders(ucpConn sdk.Connection, secretProvider *ucp_provider.SecretProvider) map[string]Provider { +// GetUCPConfiguredTerraformProviders returns a map of Terraform provider names to provider config builder. +// These providers represent Terraform providers for which Radius generates custom provider configurations based on credentials stored with UCP +// and providers configured on the Radius environment. For example, the Azure subscription id is added to Azure provider config using Radius Environment's Azure provider scope. +func GetUCPConfiguredTerraformProviders(ucpConn sdk.Connection, secretProvider *ucp_provider.SecretProvider) map[string]Provider { return map[string]Provider{ AWSProviderName: NewAWSProvider(ucpConn, secretProvider), AzureProviderName: NewAzureProvider(ucpConn, secretProvider), KubernetesProviderName: &kubernetesProvider{}, } } + +// GetRecipeProviderConfigs returns the Terraform provider configurations for Terraform providers +// specified under the RecipeConfig/Terraform/Providers section under environment configuration. +func GetRecipeProviderConfigs(ctx context.Context, envConfig *recipes.Configuration) map[string]any { + providerConfigs := make(map[string]any) + + // If the provider is not configured, or has empty configuration, skip this iteration + if envConfig != nil && envConfig.RecipeConfig.Terraform.Providers != nil { + for provider, config := range envConfig.RecipeConfig.Terraform.Providers { + if len(config) > 0 { + configList := make([]map[string]any, 0) + + // Retrieve configuration details from 'AdditionalProperties' property and add to the list. + for _, configDetails := range config { + if configDetails.AdditionalProperties != nil && len(configDetails.AdditionalProperties) > 0 { + configList = append(configList, configDetails.AdditionalProperties) + } + } + + providerConfigs[provider] = configList + } + } + } + + return providerConfigs +} diff --git a/pkg/recipes/terraform/config/providers/types_test.go b/pkg/recipes/terraform/config/providers/types_test.go new file mode 100644 index 0000000000..2df4a9a4f6 --- /dev/null +++ b/pkg/recipes/terraform/config/providers/types_test.go @@ -0,0 +1,105 @@ +package providers + +import ( + "context" + "testing" + + "github.com/radius-project/radius/pkg/corerp/datamodel" + "github.com/radius-project/radius/pkg/recipes" + "github.com/stretchr/testify/require" +) + +func TestGetRecipeProviderConfigs(t *testing.T) { + testCases := []struct { + desc string + envConfig *recipes.Configuration + expected map[string]any + }{ + { + desc: "envConfig not set", + envConfig: nil, + expected: map[string]any{}, + }, + { + desc: "no providers configured", + envConfig: &recipes.Configuration{}, + expected: map[string]any{}, + }, + { + desc: "empty provider config", + envConfig: &recipes.Configuration{ + RecipeConfig: datamodel.RecipeConfigProperties{ + Terraform: datamodel.TerraformConfigProperties{ + Providers: map[string][]datamodel.ProviderConfigProperties{ + "aws": {}, + }, + }, + }, + }, + expected: map[string]any{}, + }, + { + desc: "Additional Properties set to nil in provider config", + envConfig: &recipes.Configuration{ + RecipeConfig: datamodel.RecipeConfigProperties{ + Terraform: datamodel.TerraformConfigProperties{ + Providers: map[string][]datamodel.ProviderConfigProperties{ + "aws": { + { + AdditionalProperties: nil, + }, + }, + }, + }, + }, + }, + expected: map[string]any{"aws": []map[string]any{}}, + }, + { + desc: "provider with config", + envConfig: &recipes.Configuration{ + RecipeConfig: datamodel.RecipeConfigProperties{ + Terraform: datamodel.TerraformConfigProperties{ + Providers: map[string][]datamodel.ProviderConfigProperties{ + "azurerm": { + { + AdditionalProperties: map[string]any{ + "subscriptionid": 1234, + "tenant_id": "745fg88bf-86f1-41af-43ut", + }, + }, + { + AdditionalProperties: map[string]any{ + "alias": "az-paymentservice", + "subscriptionid": 45678, + "tenant_id": "gfhf45345-5d73-gh34-wh84", + }, + }, + }, + }, + }, + }, + }, + expected: map[string]any{ + "azurerm": []map[string]any{ + { + "subscriptionid": 1234, + "tenant_id": "745fg88bf-86f1-41af-43ut", + }, + { + "alias": "az-paymentservice", + "subscriptionid": 45678, + "tenant_id": "gfhf45345-5d73-gh34-wh84", + }, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + result := GetRecipeProviderConfigs(context.Background(), tc.envConfig) + require.Equal(t, tc.expected, result) + }) + } +} diff --git a/pkg/recipes/terraform/config/testdata/providers-envrecipedefaultconfig.tf.json b/pkg/recipes/terraform/config/testdata/providers-envrecipedefaultconfig.tf.json new file mode 100644 index 0000000000..88893b5541 --- /dev/null +++ b/pkg/recipes/terraform/config/testdata/providers-envrecipedefaultconfig.tf.json @@ -0,0 +1,29 @@ +{ + "terraform": { + "backend": { + "kubernetes": { + "config_path": "/home/radius/.kube/config", + "namespace": "radius-system", + "secret_suffix": "test-secret-suffix" + } + } + }, + "provider": { + "azurerm": [ + { + "alias": "az-paymentservice", + "subscriptionid": 45678, + "tenant_id": "gfhf45345-5d73-gh34-wh84" + } + ] + }, + "module": { + "redis-azure": { + "redis_cache_name": "redis-test", + "resource_group_name": "test-rg", + "sku": "P", + "source": "Azure/redis/azurerm", + "version": "1.1.0" + } + } +} \ No newline at end of file diff --git a/pkg/recipes/terraform/config/testdata/providers-envrecipeproviders.tf.json b/pkg/recipes/terraform/config/testdata/providers-envrecipeproviders.tf.json new file mode 100644 index 0000000000..796b3bd5f9 --- /dev/null +++ b/pkg/recipes/terraform/config/testdata/providers-envrecipeproviders.tf.json @@ -0,0 +1,33 @@ +{ + "terraform": { + "backend": { + "kubernetes": { + "config_path": "/home/radius/.kube/config", + "namespace": "radius-system", + "secret_suffix": "test-secret-suffix" + } + } + }, + "provider": { + "azurerm": [ + { + "subscriptionid": 1234, + "tenant_id": "745fg88bf-86f1-41af-43ut" + }, + { + "alias": "az-paymentservice", + "subscriptionid": 45678, + "tenant_id": "gfhf45345-5d73-gh34-wh84" + } + ] + }, + "module": { + "redis-azure": { + "redis_cache_name": "redis-test", + "resource_group_name": "test-rg", + "sku": "P", + "source": "Azure/redis/azurerm", + "version": "1.1.0" + } + } +} \ No newline at end of file diff --git a/pkg/recipes/terraform/config/testdata/providers-overridereqproviders.tf.json b/pkg/recipes/terraform/config/testdata/providers-overridereqproviders.tf.json new file mode 100644 index 0000000000..51ef6252ca --- /dev/null +++ b/pkg/recipes/terraform/config/testdata/providers-overridereqproviders.tf.json @@ -0,0 +1,33 @@ +{ + "terraform": { + "backend": { + "kubernetes": { + "config_path": "/home/radius/.kube/config", + "namespace": "radius-system", + "secret_suffix": "test-secret-suffix" + } + } + }, + "provider": { + "aws": { + "region": "test-region" + }, + "kubernetes": [ + { + "ConfigPath": "/home/radius/.kube/configPath1" + }, + { + "ConfigPath": "/home/radius/.kube/configPath2" + } + ] + }, + "module": { + "redis-azure": { + "redis_cache_name": "redis-test", + "resource_group_name": "test-rg", + "sku": "P", + "source": "Azure/redis/azurerm", + "version": "1.1.0" + } + } +} \ No newline at end of file diff --git a/pkg/recipes/terraform/config/types.go b/pkg/recipes/terraform/config/types.go index fca829429b..17a9aa54f8 100644 --- a/pkg/recipes/terraform/config/types.go +++ b/pkg/recipes/terraform/config/types.go @@ -45,6 +45,7 @@ type TerraformConfig struct { // Provider is the Terraform provider configuration. // https://developer.hashicorp.com/terraform/language/providers/configuration + // https://developer.hashicorp.com/terraform/language/syntax/json#provider-blocks Provider map[string]any `json:"provider,omitempty"` // Module is the Terraform module configuration. diff --git a/pkg/recipes/terraform/execute.go b/pkg/recipes/terraform/execute.go index 270429b694..18cf5c1f90 100644 --- a/pkg/recipes/terraform/execute.go +++ b/pkg/recipes/terraform/execute.go @@ -235,7 +235,7 @@ func (e *executor) generateConfig(ctx context.Context, tf *tfexec.Terraform, opt // Generate Terraform providers configuration for required providers and add it to the Terraform configuration. logger.Info(fmt.Sprintf("Adding provider config for required providers %+v", loadedModule.RequiredProviders)) - if err := tfConfig.AddProviders(ctx, loadedModule.RequiredProviders, providers.GetSupportedTerraformProviders(e.ucpConn, e.secretProvider), + if err := tfConfig.AddProviders(ctx, loadedModule.RequiredProviders, providers.GetUCPConfiguredTerraformProviders(e.ucpConn, e.secretProvider), options.EnvConfig); err != nil { return "", err }