Skip to content

Commit

Permalink
Add logic to build configuration for multiple Terraform providers sup…
Browse files Browse the repository at this point in the history
…port (#7189)

# Description

Adding logic to build configuration for multiple Terraform provider
support. This is constructed from a combination of environment level
recipe configuration located under RecipeConfig/Terraform/Providers
section and the provider configurations registered with UCP. The
environment level recipe configuration for providers takes precedence
over UCP provider configurations.
[link to PR for design
doc](radius-project/design-notes#39)

The design document describes a SecretReference type and ability to
fetch data from secrets and populate Provider Configuration. This will
be implemented in subsequent PRs.

## Type of change
- This pull request adds or changes features of Radius and has an
approved issue #6539

Fixes: Part of #6539
  • Loading branch information
lakshmimsft authored Feb 28, 2024
1 parent b15c0fe commit 8a99607
Show file tree
Hide file tree
Showing 9 changed files with 395 additions and 30 deletions.
24 changes: 17 additions & 7 deletions pkg/recipes/terraform/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}

Expand Down
164 changes: 146 additions & 18 deletions pkg/recipes/terraform/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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",
},
Expand Down Expand Up @@ -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{
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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{},
},
Expand All @@ -444,22 +444,150 @@ 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 {
t.Run(tc.desc, func(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
Expand Down
34 changes: 30 additions & 4 deletions pkg/recipes/terraform/config/providers/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Loading

0 comments on commit 8a99607

Please sign in to comment.