Skip to content

Commit

Permalink
Add required_providers block to main.tf.config, functional test for T…
Browse files Browse the repository at this point in the history
…erraform Provider (#7364)

# Description

PR includes updates to Terraform Providers functionality including:
1. Added required_providers block to main.tf.config file, (reading data
from the loaded recipe module).
2. Updated Save to include a json encoder to not escape HTML characters
so data can be saved into config file as authored.
3. Added a 'providers' block to module section if providers include
'alias', in main.tf.config in UpdateModuleWithProviderAliases()
4.  Updated unit tests and added functional test.


## Type of change

- This pull request fixes a bug in Radius and has an approved issue
(#7311 ).


Fixes: #7311
  • Loading branch information
lakshmimsft committed May 21, 2024
1 parent ea316f3 commit 66aad2c
Show file tree
Hide file tree
Showing 22 changed files with 996 additions and 91 deletions.
124 changes: 110 additions & 14 deletions pkg/recipes/terraform/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package config

import (
"bytes"
"context"
"encoding/json"
"errors"
Expand Down Expand Up @@ -96,24 +97,51 @@ func (cfg *TerraformConfig) Save(ctx context.Context, workingDir string) error {
// JSON configuration syntax for Terraform requires the file to be named with .tf.json suffix.
// https://developer.hashicorp.com/terraform/language/syntax/json

// Convert the Terraform config to JSON
jsonData, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
// Create a buffer to write the JSON to
buf := &bytes.Buffer{}
enc := json.NewEncoder(buf)
enc.SetEscapeHTML(false)
enc.SetIndent("", " ") // Indent with 2 spaces to make the JSON file human-readable and consistent with codebase.

// Encode the Terraform config to JSON. JSON encoding is being used to ensure that special characters
// in the original text are preserved when writing to the file.
// For example, when writing this text to file with JSON encoding (using enc.Encode(cfg)),
// the special characters in the following text will be preserved:
// "required_providers": {
// "aws": {
// "source": "hashicorp/aws",
// "version": ">= 3.0"
// },
// }
// However, if we were to write the text directly to the file without JSON encoding, t
// the special characters would be escaped and be written as follows:
// "required_providers": {
// "aws": {
// "source": "hashicorp/aws",
// "version": "\u003e= 2.0"
// },
// }
if err := enc.Encode(cfg); err != nil {
return fmt.Errorf("error marshalling JSON: %w", err)
}

// Remove trailing newline character from the JSON data so that file is written without any extra newline characters.
// This is to maintain consistency with the codebase.
jsonData := strings.TrimSuffix(buf.String(), "\n")
logger.Info(fmt.Sprintf("Writing Terraform JSON config to file: %s", getMainConfigFilePath(workingDir)))
if err = os.WriteFile(getMainConfigFilePath(workingDir), jsonData, modeConfigFile); err != nil {
if err := os.WriteFile(getMainConfigFilePath(workingDir), []byte(jsonData), modeConfigFile); err != nil {
return fmt.Errorf("error creating file: %w", err)
}

return nil
}

// AddProviders adds provider configurations for requiredProviders that are supported
// by Radius to generate custom provider configurations. Save() must be called to save
// the generated providers config. requiredProviders contains a list of provider names
// AddProviders adds provider configurations to Terraform configuration file based on input of environment recipe configuration, requiredProviders and ucpConfiguredProviders.
// It also updates module provider block if aliases exist and required_provider configuration to the file.
// 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, ucpConfiguredProviders map[string]providers.Provider, envConfig *recipes.Configuration) error {
func (cfg *TerraformConfig) AddProviders(ctx context.Context, requiredProviders map[string]*RequiredProviderInfo, ucpConfiguredProviders map[string]providers.Provider, envConfig *recipes.Configuration) error {
logger := ucplog.FromContextOrDiscard(ctx)
providerConfigs, err := getProviderConfigs(ctx, requiredProviders, ucpConfiguredProviders, envConfig)
if err != nil {
return err
Expand All @@ -124,6 +152,67 @@ func (cfg *TerraformConfig) AddProviders(ctx context.Context, requiredProviders
cfg.Provider = providerConfigs
}

// Update module configuration with aliased provider names, if they exist.
logger.Info("Updating module config with providers aliases")
if err := cfg.updateModuleWithProviderAliases(requiredProviders); err != nil {
return err
}

// Set the required providers for the Terraform configuration.
logger.Info("Update Terraform configuration with required providers")
if cfg.Terraform == nil {
cfg.Terraform = &TerraformDefinition{}
}
cfg.Terraform.RequiredProviders = requiredProviders

return nil
}

// updateModuleWithProviderAliases updates the module provider configuration in the Terraform config
// by adding aliases to the provider configurations.
// https://developer.hashicorp.com/terraform/language/syntax/json#module-blocks
func (cfg *TerraformConfig) updateModuleWithProviderAliases(requiredProviders map[string]*RequiredProviderInfo) error {
if cfg == nil {
return fmt.Errorf("terraform configuration is not initialized")
}
moduleAliasConfig := map[string]string{}

for providerName, providerConfigList := range cfg.Provider {
// For each provider in the providerConfigs, if provider has a property "alias",
// add entry to the module provider configuration.
// Provider configurations (those with the alias argument set) are never inherited automatically by modules,
// and so must always be passed explicitly using the providers map.
// https://developer.hashicorp.com/terraform/language/modules/develop/providers#legacy-shared-modules-with-provider-configurations

// Note: We're building configuration from user input, we're mapping the provider.alias names in
// the required provider configuration (ConfigurationAliases) to the environment recipe provider configuration data.
// This is being done to ensure that the provider configuration is passed to the module correctly.

for _, providerConfig := range providerConfigList {
if alias, ok := providerConfig["alias"]; ok {
aliasProviderConfig := providerName + "." + fmt.Sprintf("%v", alias)

// Check if the alias is in the required providers' configuration aliases. If there is a match, add the alias to the module provider configuration.
if requiredProviders[providerName] != nil && len(requiredProviders[providerName].ConfigurationAliases) > 0 {
for _, alias := range requiredProviders[providerName].ConfigurationAliases {
if alias == aliasProviderConfig {
moduleAliasConfig[alias] = alias
break
}
}
}
}
}
}

// Update the module provider configuration in the Terraform config.
if len(moduleAliasConfig) > 0 {
moduleConfig := cfg.Module
for _, module := range moduleConfig {
module["providers"] = moduleAliasConfig
}
}

return nil
}

Expand Down Expand Up @@ -167,12 +256,16 @@ func newModuleConfig(moduleSource string, moduleVersion string, params ...Recipe

// 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) {
// The function returns a map where the keys are provider names and the values are slices of maps.
// Each map in the slice represents a specific configuration for the corresponding provider.
// This structure allows for multiple configurations per provider.
func getProviderConfigs(ctx context.Context, requiredProviders map[string]*RequiredProviderInfo, ucpConfiguredProviders map[string]providers.Provider, envConfig *recipes.Configuration) (map[string][]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 {
// Build provider configurations for required providers excluding the ones already present in providerConfigs (environment level configuration).
// Required providers that are not configured with UCP will be skipped.
for provider := range requiredProviders {
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)
Expand All @@ -189,8 +282,9 @@ func getProviderConfigs(ctx context.Context, requiredProviders []string, ucpConf
if err != nil {
return nil, err
}

if len(config) > 0 {
providerConfigs[provider] = config
providerConfigs[provider] = []map[string]any{config}
}
}

Expand All @@ -205,9 +299,11 @@ func (cfg *TerraformConfig) AddTerraformBackend(resourceRecipe *recipes.Resource
if err != nil {
return nil, err
}
cfg.Terraform = &TerraformDefinition{
Backend: backendConfig,

if cfg.Terraform == nil {
cfg.Terraform = &TerraformDefinition{}
}
cfg.Terraform.Backend = backendConfig

return backendConfig, nil
}
Expand Down
Loading

0 comments on commit 66aad2c

Please sign in to comment.