Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 57 additions & 5 deletions pkg/recipes/terraform/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,10 @@ func (cfg *TerraformConfig) AddProviders(ctx context.Context, requiredProviders
return err
}

// Add generated provider configs for required providers to the existing terraform json config file
// Add generated provider configs for required providers to the existing terraform json config file.
// Always replace the existing value so that the placeholders from AddProviderAliasPlaceholders
// do not stay in the configuration that Terraform applies.
cfg.Provider = nil
if len(providerConfigs) > 0 {
cfg.Provider = providerConfigs
}
Expand Down Expand Up @@ -173,16 +176,65 @@ func (cfg *TerraformConfig) updateModuleWithProviderAliases(requiredProviders ma
}

// Update the module provider configuration in the Terraform config.
if len(moduleAliasConfig) > 0 {
moduleConfig := cfg.Module
for _, module := range moduleConfig {
module["providers"] = moduleAliasConfig
// Remove a providers map from an earlier step (for example, a placeholder) when no alias matches.
for _, module := range cfg.Module {
if len(moduleAliasConfig) > 0 {
module[moduleProvidersKey] = moduleAliasConfig
} else {
delete(module, moduleProvidersKey)
}
}

return nil
}

// AddProviderAliasPlaceholders adds a placeholder provider block and a module providers map entry for each
// aliased provider in the environment recipe configuration. A placeholder contains only the alias.
//
// Terraform validates the configuration_aliases of a module when it downloads the module (terraform get).
// Radius downloads the module before it can inspect the module and generate the full provider configuration
// in AddProviders. Without these placeholders, the download fails for modules that declare configuration_aliases.
// If the module does not declare an alias, Terraform only gives a warning.
// AddProviders replaces the placeholders after the module is inspected. Save() must be called to persist the config.
func (cfg *TerraformConfig) AddProviderAliasPlaceholders(envConfig *recipes.Configuration) {
if envConfig == nil {
return
}

placeholders := map[string][]map[string]any{}
moduleAliasConfig := map[string]string{}
for providerName, providerConfigs := range envConfig.RecipeConfig.Terraform.Providers {
for _, providerConfig := range providerConfigs {
alias, ok := providerConfig.AdditionalProperties["alias"]
if !ok {
continue
}

aliasName := fmt.Sprintf("%v", alias)
if aliasName == "" {
continue
}

aliasProviderConfig := providerName + "." + aliasName
if _, exists := moduleAliasConfig[aliasProviderConfig]; exists {
continue
}

moduleAliasConfig[aliasProviderConfig] = aliasProviderConfig
placeholders[providerName] = append(placeholders[providerName], map[string]any{"alias": aliasName})
}
}

if len(moduleAliasConfig) == 0 {
return
}

cfg.Provider = placeholders
Comment thread
nicolejms marked this conversation as resolved.
for _, module := range cfg.Module {
module[moduleProvidersKey] = moduleAliasConfig
Comment thread
nicolejms marked this conversation as resolved.
Comment thread
nicolejms marked this conversation as resolved.
}
}

// AddRecipeContext adds RecipeContext to TerraformConfig module parameters if recipeCtx is not nil.
// Save() must be called after adding recipe context to the module config.
func (cfg *TerraformConfig) AddRecipeContext(ctx context.Context, moduleName string, recipeCtx *recipecontext.Context) error {
Expand Down
157 changes: 157 additions & 0 deletions pkg/recipes/terraform/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -638,6 +638,163 @@ func Test_AddProviders(t *testing.T) {
}
}

func Test_AddProviderAliasPlaceholders(t *testing.T) {
tests := []struct {
desc string
envConfig *recipes.Configuration
expectedProvider map[string][]map[string]any
expectedProviders map[string]string
}{
{
desc: "nil env config",
},
{
desc: "no recipe providers",
envConfig: &recipes.Configuration{},
},
{
desc: "recipe providers without aliases",
envConfig: &recipes.Configuration{
RecipeConfig: datamodel.RecipeConfigProperties{
Terraform: datamodel.TerraformConfigProperties{
Providers: map[string][]datamodel.ProviderConfigProperties{
"azurerm": {
{AdditionalProperties: map[string]any{"subscription_id": "sub"}},
{AdditionalProperties: map[string]any{"alias": ""}},
},
},
},
},
},
},
{
desc: "aliased recipe providers",
envConfig: &recipes.Configuration{
RecipeConfig: datamodel.RecipeConfigProperties{
Terraform: datamodel.TerraformConfigProperties{
Providers: map[string][]datamodel.ProviderConfigProperties{
"azurerm": {
{AdditionalProperties: map[string]any{"subscription_id": "sub"}},
{AdditionalProperties: map[string]any{"alias": "custom", "subscription_id": "sub"}},
{AdditionalProperties: map[string]any{"alias": "custom", "subscription_id": "other"}},
},
"kubernetes": {
{AdditionalProperties: map[string]any{"alias": "k8s_first", "config_path": "/path1"}},
{AdditionalProperties: map[string]any{"alias": "k8s_second", "config_path": "/path2"}},
},
},
},
},
},
expectedProvider: map[string][]map[string]any{
"azurerm": {{"alias": "custom"}},
"kubernetes": {{"alias": "k8s_first"}, {"alias": "k8s_second"}},
},
expectedProviders: map[string]string{
"azurerm.custom": "azurerm.custom",
"kubernetes.k8s_first": "kubernetes.k8s_first",
"kubernetes.k8s_second": "kubernetes.k8s_second",
},
},
}

for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
envRecipe, resourceRecipe := getTestInputs()
tfconfig, err := New(t.Context(), testRecipeName, &envRecipe, &resourceRecipe)
require.NoError(t, err)

tfconfig.AddProviderAliasPlaceholders(tc.envConfig)

require.Equal(t, tc.expectedProvider, tfconfig.Provider)
moduleProviders, ok := tfconfig.Module[testRecipeName][moduleProvidersKey]
if tc.expectedProviders == nil {
require.False(t, ok)
return
}
require.Equal(t, tc.expectedProviders, moduleProviders)
})
}
}

func Test_AddProviders_ReplacesAliasPlaceholders(t *testing.T) {
aliasEnvConfig := &recipes.Configuration{
RecipeConfig: datamodel.RecipeConfigProperties{
Terraform: datamodel.TerraformConfigProperties{
Providers: map[string][]datamodel.ProviderConfigProperties{
"azurerm": {
{AdditionalProperties: map[string]any{"alias": "custom", "subscription_id": "sub"}},
},
},
},
},
}

tests := []struct {
desc string
envConfig *recipes.Configuration
requiredProviders map[string]*RequiredProviderInfo
expectedProvider map[string][]map[string]any
expectedProviders map[string]string
}{
{
desc: "module declares the alias",
envConfig: aliasEnvConfig,
requiredProviders: map[string]*RequiredProviderInfo{
providers.AzureProviderName: {
Source: "hashicorp/azurerm",
ConfigurationAliases: []string{"azurerm.custom"},
},
},
expectedProvider: map[string][]map[string]any{
"azurerm": {{"alias": "custom", "subscription_id": "sub"}},
},
expectedProviders: map[string]string{"azurerm.custom": "azurerm.custom"},
},
{
desc: "module does not declare the alias",
envConfig: aliasEnvConfig,
requiredProviders: map[string]*RequiredProviderInfo{
"random": {Source: "hashicorp/random"},
},
expectedProvider: map[string][]map[string]any{
"azurerm": {{"alias": "custom", "subscription_id": "sub"}},
},
},
{
desc: "no provider configuration is generated",
envConfig: &recipes.Configuration{},
requiredProviders: map[string]*RequiredProviderInfo{
"random": {Source: "hashicorp/random"},
},
},
}

for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
envRecipe, resourceRecipe := getTestInputs()
tfconfig, err := New(t.Context(), testRecipeName, &envRecipe, &resourceRecipe)
require.NoError(t, err)

// Simulate the placeholders that are added before the module download.
tfconfig.AddProviderAliasPlaceholders(aliasEnvConfig)
require.NotNil(t, tfconfig.Provider)
require.Contains(t, tfconfig.Module[testRecipeName], moduleProvidersKey)

err = tfconfig.AddProviders(t.Context(), tc.requiredProviders, map[string]providers.Provider{}, tc.envConfig, nil)
require.NoError(t, err)

require.Equal(t, tc.expectedProvider, tfconfig.Provider)
moduleProviders, ok := tfconfig.Module[testRecipeName][moduleProvidersKey]
if tc.expectedProviders == nil {
require.False(t, ok)
return
}
require.Equal(t, tc.expectedProviders, moduleProviders)
})
}
}

func Test_AddOutputs(t *testing.T) {
envRecipe, resourceRecipe := getTestInputs()
tests := []struct {
Expand Down
2 changes: 2 additions & 0 deletions pkg/recipes/terraform/config/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ const (
moduleSourceKey = "source"
// moduleVersionKey represents the key for the module version parameter.
moduleVersionKey = "version"
// moduleProvidersKey represents the key for the module providers map.
moduleProvidersKey = "providers"

mainConfigFileName = "main.tf.json"
)
Expand Down
5 changes: 5 additions & 0 deletions pkg/recipes/terraform/execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,11 @@ func getTerraformConfig(ctx context.Context, workingDir string, options Options)
return nil, err
}

// Terraform validates the provider configuration_aliases of the module when it downloads the module.
// Add placeholders for the aliased providers in the environment configuration so that the download succeeds.
// AddProviders replaces the placeholders after the module is inspected.
tfConfig.AddProviderAliasPlaceholders(options.EnvConfig)
Comment thread
nicolejms marked this conversation as resolved.
Outdated

// Before downloading the module, Teraform configuration needs to be persisted in the working directory.
// Terraform Get command uses this config file to download module from the source specified in the config.
if err := tfConfig.Save(ctx, workingDir); err != nil {
Expand Down
48 changes: 48 additions & 0 deletions pkg/recipes/terraform/execute_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,54 @@ func Test_GetTerraformConfig(t *testing.T) {
require.Equal(t, &expectedConfig, tfConfig)
}

func Test_GetTerraformConfig_ProviderAliasPlaceholders(t *testing.T) {
testDir := t.TempDir()

options := Options{
EnvConfig: &recipes.Configuration{
RecipeConfig: dm.RecipeConfigProperties{
Terraform: dm.TerraformConfigProperties{
Providers: map[string][]dm.ProviderConfigProperties{
"azurerm": {
{AdditionalProperties: map[string]any{"subscription_id": "sub"}},
{AdditionalProperties: map[string]any{"alias": "custom", "subscription_id": "sub"}},
},
},
},
},
},
EnvRecipe: &recipes.EnvironmentDefinition{
Name: "test-recipe",
TemplatePath: "test/module/source",
},
ResourceRecipe: &recipes.ResourceMetadata{},
}

expectedConfig := config.TerraformConfig{
Provider: map[string][]map[string]any{
"azurerm": {{"alias": "custom"}},
},
Module: map[string]config.TFModuleConfig{
"test-recipe": {
"source": "test/module/source",
"providers": map[string]string{"azurerm.custom": "azurerm.custom"},
},
},
}
tfConfig, err := getTerraformConfig(t.Context(), testDir, options)
require.NoError(t, err)
require.Equal(t, &expectedConfig, tfConfig)

// The placeholders must be saved before "terraform get" runs.
content, err := os.ReadFile(filepath.Join(testDir, "main.tf.json"))
require.NoError(t, err)
require.JSONEq(t, `{
"terraform": null,
"provider": {"azurerm": [{"alias": "custom"}]},
"module": {"test-recipe": {"source": "test/module/source", "providers": {"azurerm.custom": "azurerm.custom"}}}
}`, string(content))
}

func Test_GetTerraformConfig_EmptyRecipeName(t *testing.T) {
// Create a temporary directory for testing.
testDir := t.TempDir()
Expand Down
Loading
Loading