Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Support for passing environment variables to dependency blocks #2759

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
9 changes: 9 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ const (
MetadataRetryMaxAttempts = "retry_max_attempts"
MetadataRetrySleepIntervalSec = "retry_sleep_interval_sec"
MetadataDependentModules = "dependent_modules"
MetadataDependencyEnvVars = "dependency_env_vars"
)

// Order matters, for example if none of the files are found `GetDefaultConfigPath` func returns the last element.
Expand Down Expand Up @@ -85,6 +86,7 @@ type TerragruntConfig struct {
RetryableErrors []string
RetryMaxAttempts *int
RetrySleepIntervalSec *int
DependencyEnvVars map[string]string

// Fields used for internal tracking
// Indicates whether or not this is the result of a partial evaluation
Expand Down Expand Up @@ -173,6 +175,8 @@ type terragruntConfigFile struct {
RetryMaxAttempts *int `hcl:"retry_max_attempts,optional"`
RetrySleepIntervalSec *int `hcl:"retry_sleep_interval_sec,optional"`

DependencyEnvVars map[string]string `hcl:"dependency_env_vars,optional"`

// This struct is used for validating and parsing the entire terragrunt config. Since locals and include are
// evaluated in a completely separate cycle, it should not be evaluated here. Otherwise, we can't support self
// referencing other elements in the same block.
Expand Down Expand Up @@ -1008,6 +1012,11 @@ func convertToTerragruntConfig(
terragruntConfig.SetFieldMetadata(MetadataIamAssumeRoleSessionName, defaultMetadata)
}

if terragruntConfigFromFile.DependencyEnvVars != nil {
terragruntConfig.DependencyEnvVars = terragruntConfigFromFile.DependencyEnvVars
terragruntConfig.SetFieldMetadata(MetadataDependencyEnvVars, defaultMetadata)
}

generateBlocks := []terragruntGenerateBlock{}
generateBlocks = append(generateBlocks, terragruntConfigFromFile.GenerateBlocks...)

Expand Down
12 changes: 12 additions & 0 deletions config/config_as_cty.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,14 @@ func TerragruntConfigAsCty(config *TerragruntConfig) (cty.Value, error) {
}
}

dependencyEnvVarsCty, err := goTypeToCty(config.DependencyEnvVars)
if err != nil {
return cty.NilVal, err
}
if dependencyEnvVarsCty != cty.NilVal {
output[MetadataDependencyEnvVars] = dependencyEnvVarsCty
}

return convertValuesMapToCtyVal(output)
}

Expand Down Expand Up @@ -305,6 +313,10 @@ func TerragruntConfigAsCtyWithMetadata(config *TerragruntConfig) (cty.Value, err
}
}

if err := wrapWithMetadata(config, config.DependencyEnvVars, MetadataDependencyEnvVars, &output); err != nil {
return cty.NilVal, err
}

return convertValuesMapToCtyVal(output)
}

Expand Down
12 changes: 8 additions & 4 deletions config/config_partial.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,11 @@ type terraformConfigSourceOnly struct {

// terragruntFlags is a struct that can be used to only decode the flag attributes (skip and prevent_destroy)
type terragruntFlags struct {
IamRole *string `hcl:"iam_role,attr"`
PreventDestroy *bool `hcl:"prevent_destroy,attr"`
Skip *bool `hcl:"skip,attr"`
Remain hcl.Body `hcl:",remain"`
IamRole *string `hcl:"iam_role,attr"`
PreventDestroy *bool `hcl:"prevent_destroy,attr"`
Skip *bool `hcl:"skip,attr"`
DependenciesEnvVars *map[string]string `hcl:"dependency_env_vars,attr"`
Remain hcl.Body `hcl:",remain"`
}

// terragruntVersionConstraints is a struct that can be used to only decode the attributes related to constraining the
Expand Down Expand Up @@ -312,6 +313,9 @@ func PartialParseConfigString(
if decoded.IamRole != nil {
output.IamRole = *decoded.IamRole
}
if decoded.DependenciesEnvVars != nil {
output.DependencyEnvVars = *decoded.DependenciesEnvVars
}

case TerragruntVersionConstraints:
decoded := terragruntVersionConstraints{}
Expand Down
44 changes: 35 additions & 9 deletions config/dependency.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ type Dependency struct {

// Used to store the rendered outputs for use when the config is imported or read with `read_terragrunt_config`
RenderedOutputs *cty.Value `cty:"outputs"`

// Used to supply additional info to subsequent terragrunt runs
EnvVars map[string]string `hcl:"env_vars,attr" cty:"env_vars"`
}

// DeepMerge will deep merge two Dependency configs, updating the target. Deep merge for Dependency configs is defined
Expand Down Expand Up @@ -427,7 +430,16 @@ func getTerragruntOutput(dependencyConfig Dependency, terragruntOptions *options
return nil, true, errors.WithStackTrace(DependencyConfigNotFound{Path: targetConfig})
}

jsonBytes, err := getOutputJsonWithCaching(targetConfig, terragruntOptions)
envVars := dependencyConfig.EnvVars
for k, v := range terragruntOptions.DependencyEnvVars {
if envVars[k] != "" {
envVars[k] = v
}
}

dependencyConfig.EnvVars = envVars

jsonBytes, err := getOutputJsonWithCaching(targetConfig, &dependencyConfig, terragruntOptions)
if err != nil {
if !isRenderJsonCommand(terragruntOptions) {
return nil, true, err
Expand Down Expand Up @@ -459,7 +471,7 @@ func isRenderJsonCommand(terragruntOptions *options.TerragruntOptions) bool {
}

// getOutputJsonWithCaching will run terragrunt output on the target config if it is not already cached.
func getOutputJsonWithCaching(targetConfig string, terragruntOptions *options.TerragruntOptions) ([]byte, error) {
func getOutputJsonWithCaching(targetConfig string, dependencyConfig *Dependency, terragruntOptions *options.TerragruntOptions) ([]byte, error) {
// Acquire synchronization lock to ensure only one instance of output is called per config.
rawActualLock, _ := outputLocks.LoadOrStore(targetConfig, &sync.Mutex{})
actualLock := rawActualLock.(*sync.Mutex)
Expand All @@ -471,16 +483,18 @@ func getOutputJsonWithCaching(targetConfig string, terragruntOptions *options.Te
// output" log for the dependency.
terragruntOptions.Logger.Debugf("Getting output of dependency %s for config %s", targetConfig, terragruntOptions.TerragruntConfigPath)

cacheKey := util.EncodeStringMap(&dependencyConfig.EnvVars, targetConfig)

// Look up if we have already run terragrunt output for this target config
rawJsonBytes, hasRun := jsonOutputCache.Load(targetConfig)
rawJsonBytes, hasRun := jsonOutputCache.Load(cacheKey)
if hasRun {
// Cache hit, so return cached output
terragruntOptions.Logger.Debugf("%s was run before. Using cached output.", targetConfig)
terragruntOptions.Logger.Debugf("%s was run before with same configuration (hash: %s). Using cached output.", targetConfig, cacheKey)
return rawJsonBytes.([]byte), nil
}

// Cache miss, so look up the output and store in cache
newJsonBytes, err := getTerragruntOutputJson(terragruntOptions, targetConfig)
newJsonBytes, err := getTerragruntOutputJson(terragruntOptions, dependencyConfig, targetConfig)
if err != nil {
return nil, err
}
Expand All @@ -492,7 +506,7 @@ func getOutputJsonWithCaching(targetConfig string, terragruntOptions *options.Te
newJsonBytes = newJsonBytes[index:]
}

jsonOutputCache.Store(targetConfig, newJsonBytes)
jsonOutputCache.Store(cacheKey, newJsonBytes)
return newJsonBytes, nil
}

Expand All @@ -514,7 +528,7 @@ func cloneTerragruntOptionsForDependency(terragruntOptions *options.TerragruntOp
}

// Clone terragrunt options and update context for dependency block so that the outputs can be read correctly
func cloneTerragruntOptionsForDependencyOutput(terragruntOptions *options.TerragruntOptions, targetConfig string) (*options.TerragruntOptions, error) {
func cloneTerragruntOptionsForDependencyOutput(terragruntOptions *options.TerragruntOptions, dependencyConfig *Dependency, targetConfig string) (*options.TerragruntOptions, error) {
targetOptions := cloneTerragruntOptionsForDependency(terragruntOptions, targetConfig)
targetOptions.IncludeModulePrefix = false
// just read outputs, so no need to check for dependent modules
Expand Down Expand Up @@ -575,6 +589,18 @@ func cloneTerragruntOptionsForDependencyOutput(terragruntOptions *options.Terrag
targetOptions.Source = targetSource
}

if terragruntOptions.DependencyEnvVars != nil {
for key, val := range terragruntOptions.DependencyEnvVars {
targetOptions.Env[key] = val
}
}

if dependencyConfig.EnvVars != nil {
for key, val := range dependencyConfig.EnvVars {
targetOptions.Env[key] = val
}
}

return targetOptions, nil
}

Expand All @@ -585,10 +611,10 @@ func cloneTerragruntOptionsForDependencyOutput(terragruntOptions *options.Terrag
// If these conditions are met, terragrunt can optimize the retrieval to avoid recursively retrieving dependency outputs
// by directly pulling down the state file. Otherwise, terragrunt will fallback to running `terragrunt output` on the
// target module.
func getTerragruntOutputJson(terragruntOptions *options.TerragruntOptions, targetConfig string) ([]byte, error) {
func getTerragruntOutputJson(terragruntOptions *options.TerragruntOptions, dependencyConfig *Dependency, targetConfig string) ([]byte, error) {
// Make a copy of the terragruntOptions so that we can reuse the same execution environment, but in the context of
// the target config.
targetTGOptions, err := cloneTerragruntOptionsForDependencyOutput(terragruntOptions, targetConfig)
targetTGOptions, err := cloneTerragruntOptionsForDependencyOutput(terragruntOptions, dependencyConfig, targetConfig)
if err != nil {
return nil, err
}
Expand Down
8 changes: 8 additions & 0 deletions config/include.go
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,10 @@ func (targetConfig *TerragruntConfig) Merge(sourceConfig *TerragruntConfig, terr
targetConfig.Inputs = mergeInputs(sourceConfig.Inputs, targetConfig.Inputs)
}

for key, val := range sourceConfig.DependencyEnvVars {
targetConfig.DependencyEnvVars[key] = val
}

copyFieldsMetadata(sourceConfig, targetConfig)

return nil
Expand Down Expand Up @@ -496,6 +500,10 @@ func (targetConfig *TerragruntConfig) DeepMerge(sourceConfig *TerragruntConfig,
targetConfig.GenerateConfigs[key] = val
}

for key, val := range sourceConfig.DependencyEnvVars {
targetConfig.DependencyEnvVars[key] = val
}

copyFieldsMetadata(sourceConfig, targetConfig)
return nil
}
Expand Down
5 changes: 5 additions & 0 deletions options/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,9 @@ type TerragruntOptions struct {

// Disalbes validation terraform command
DisableCommandValidation bool

// Supplies the environment variables when calling output from dependencies
DependencyEnvVars map[string]string
}

// IAMOptions represents options that are used by Terragrunt to assume an IAM role.
Expand Down Expand Up @@ -316,6 +319,7 @@ func NewTerragruntOptions() *TerragruntOptions {
UsePartialParseConfigCache: false,
OutputPrefix: "",
IncludeModulePrefix: false,
DependencyEnvVars: map[string]string{},
JSONOut: DefaultJSONOutName,
TerraformImplementation: UnknownImpl,
RunTerragrunt: func(opts *TerragruntOptions) error {
Expand Down Expand Up @@ -443,6 +447,7 @@ func (opts *TerragruntOptions) Clone(terragruntConfigPath string) *TerragruntOpt
FailIfBucketCreationRequired: opts.FailIfBucketCreationRequired,
DisableBucketUpdate: opts.DisableBucketUpdate,
TerraformImplementation: opts.TerraformImplementation,
DependencyEnvVars: util.CloneStringMap(opts.DependencyEnvVars),
}
}

Expand Down
11 changes: 11 additions & 0 deletions test/fixture-dependency-variables/module-a/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
terraform {
backend "local" {}
}

variable "content" {
type = string
}

output "result" {
value = "Hello World, from A: ${var.content}"
}
23 changes: 23 additions & 0 deletions test/fixture-dependency-variables/module-a/terragrunt.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
locals {
env_value = get_env("VARIANT", "")
variant = local.env_value == "" ? "default" : local.env_value
}

terraform {
extra_arguments "data_directory" {
commands = [get_terraform_command()]
arguments = []
env_vars = {
"TF_DATA_DIR" = ".terraform/${local.variant}"
}
}

extra_arguments "state_backend" {
commands = ["init"]
arguments = ["-backend-config=path=terraform-${local.variant}.tfstate"]
}
}

inputs = {
content = local.variant
}
11 changes: 11 additions & 0 deletions test/fixture-dependency-variables/module-b/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
terraform {
backend "local" {}
}

variable "content" {
type = string
}

output "result" {
value = "Hello World, from B: ${var.content}"
}
13 changes: 13 additions & 0 deletions test/fixture-dependency-variables/module-b/terragrunt.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
dependency "a" {
config_path = "../module-a"

env_vars = {}
}

dependency_env_vars = {
"VARIANT" = "a"
}

inputs = {
content = dependency.a.outputs.result
}
10 changes: 10 additions & 0 deletions util/hash.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,13 @@ func GenerateRandomSha256() (string, error) {

return fmt.Sprintf("%x", sha256.Sum256(randomBytes)), nil
}

func EncodeStringMap(stringMap *map[string]string, seed string) string {
rollingHash := sha1.Sum([]byte(seed))

for k, v := range *stringMap {
rollingHash = sha1.Sum([]byte(fmt.Sprintf("%s:%s:%s", rollingHash, k, v)))
}

return string(rollingHash[:])
}