diff --git a/config/dependency.go b/config/dependency.go index 0dee61f83..f253bbbb1 100644 --- a/config/dependency.go +++ b/config/dependency.go @@ -3,6 +3,7 @@ package config import ( "bufio" "bytes" + "context" "encoding/json" goErrors "errors" "fmt" @@ -16,6 +17,7 @@ import ( "github.com/gruntwork-io/terragrunt/internal/cache" + "cloud.google.com/go/storage" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/s3" "github.com/hashicorp/go-getter" @@ -911,6 +913,18 @@ func getTerragruntOutputJSONFromRemoteState( ctx.TerragruntOptions.Logger.Debugf("Retrieved output from %s as json: %s using s3 bucket", targetTGOptions.TerragruntConfigPath, jsonBytes) + return jsonBytes, nil + case "gcs": + jsonBytes, err := getTerragruntOutputJSONFromRemoteStateGCS( + targetTGOptions, + remoteState, + ) + if err != nil { + return nil, err + } + + ctx.TerragruntOptions.Logger.Debugf("Retrieved output from %s as json: %s using GCS bucket", targetTGOptions.TerragruntConfigPath, jsonBytes) + return jsonBytes, nil default: ctx.TerragruntOptions.Logger.Errorf("FetchDependencyOutputFromState is not supported for backend %s, falling back to normal method", backend) @@ -990,12 +1004,72 @@ func getTerragruntOutputJSONFromRemoteStateS3(terragruntOptions *options.Terragr } }(result.Body) - steateBody, err := io.ReadAll(result.Body) + stateBody, err := io.ReadAll(result.Body) + if err != nil { + return nil, err + } + + jsonState := string(stateBody) + jsonMap := make(map[string]interface{}) + + err = json.Unmarshal([]byte(jsonState), &jsonMap) + if err != nil { + return nil, err + } + + jsonOutputs, err := json.Marshal(jsonMap["outputs"]) + if err != nil { + return nil, err + } + + return jsonOutputs, nil +} + +// getTerragruntOutputJSONFromRemoteStateGCS pulls the output directly from a GCS bucket without calling Terraform +func getTerragruntOutputJSONFromRemoteStateGCS( + terragruntOptions *options.TerragruntOptions, + remoteState *remote.RemoteState, +) ([]byte, error) { + terragruntOptions.Logger.Debugf("Fetching outputs directly from gcs://%s/%s/default.tfstate", remoteState.Config["bucket"], remoteState.Config["prefix"]) + + gcsConfigExtended, err := remote.ParseExtendedGCSConfig(remoteState.Config) + if err != nil { + return nil, err + } + + if err := remote.ValidateGCSConfig(gcsConfigExtended); err != nil { + return nil, err + } + + var gcsConfig = gcsConfigExtended.RemoteStateConfigGCS + + gcsClient, err := remote.CreateGCSClient(gcsConfig) + if err != nil { + return nil, err + } + + bucket := gcsClient.Bucket(gcsConfig.Bucket) + object := bucket.Object(gcsConfig.Prefix + "/default.tfstate") + + reader, err := object.NewReader(context.Background()) + + if err != nil { + return nil, err + } + + defer func(reader *storage.Reader) { + err := reader.Close() + if err != nil { + terragruntOptions.Logger.Warnf("Failed to close remote state response %v", err) + } + }(reader) + + stateBody, err := io.ReadAll(reader) if err != nil { return nil, err } - jsonState := string(steateBody) + jsonState := string(stateBody) jsonMap := make(map[string]interface{}) err = json.Unmarshal([]byte(jsonState), &jsonMap) diff --git a/remote/remote_state_gcs.go b/remote/remote_state_gcs.go index 4deda7275..88403d9fd 100644 --- a/remote/remote_state_gcs.go +++ b/remote/remote_state_gcs.go @@ -31,7 +31,7 @@ import ( * has to create them. */ type ExtendedRemoteStateConfigGCS struct { - remoteStateConfigGCS RemoteStateConfigGCS + RemoteStateConfigGCS RemoteStateConfigGCS `mapstructure:"remote_state_config_gcs"` Project string `mapstructure:"project"` Location string `mapstructure:"location"` @@ -59,7 +59,6 @@ type RemoteStateConfigGCS struct { Credentials string `mapstructure:"credentials"` AccessToken string `mapstructure:"access_token"` Prefix string `mapstructure:"prefix"` - Path string `mapstructure:"path"` EncryptionKey string `mapstructure:"encryption_key"` ImpersonateServiceAccount string `mapstructure:"impersonate_service_account"` @@ -178,16 +177,16 @@ func (initializer GCSInitializer) buildInitializerCacheKey(gcsConfig *RemoteStat // Initialize the remote state GCS bucket specified in the given config. This function will validate the config // parameters, create the GCS bucket if it doesn't already exist, and check that versioning is enabled. func (initializer GCSInitializer) Initialize(ctx context.Context, remoteState *RemoteState, terragruntOptions *options.TerragruntOptions) error { - gcsConfigExtended, err := parseExtendedGCSConfig(remoteState.Config) + gcsConfigExtended, err := ParseExtendedGCSConfig(remoteState.Config) if err != nil { return err } - if err := validateGCSConfig(gcsConfigExtended); err != nil { + if err := ValidateGCSConfig(gcsConfigExtended); err != nil { return err } - var gcsConfig = gcsConfigExtended.remoteStateConfigGCS + var gcsConfig = gcsConfigExtended.RemoteStateConfigGCS cacheKey := initializer.buildInitializerCacheKey(&gcsConfig) if initialized, hit := initializedRemoteStateCache.Get(ctx, cacheKey); initialized && hit { @@ -245,7 +244,7 @@ func (initializer GCSInitializer) GetTerraformInitArgs(config map[string]interfa return filteredConfig } -// Parse the given map into a GCS config +// parseGCSConfig parses the given config map into a GCS config func parseGCSConfig(config map[string]interface{}) (*RemoteStateConfigGCS, error) { var gcsConfig RemoteStateConfigGCS if err := mapstructure.Decode(config, &gcsConfig); err != nil { @@ -255,8 +254,8 @@ func parseGCSConfig(config map[string]interface{}) (*RemoteStateConfigGCS, error return &gcsConfig, nil } -// Parse the given map into a GCS config -func parseExtendedGCSConfig(config map[string]interface{}) (*ExtendedRemoteStateConfigGCS, error) { +// ParseExtendedGCSConfig parses the given config map into a GCS config +func ParseExtendedGCSConfig(config map[string]interface{}) (*ExtendedRemoteStateConfigGCS, error) { var ( gcsConfig RemoteStateConfigGCS extendedConfig ExtendedRemoteStateConfigGCS @@ -270,14 +269,14 @@ func parseExtendedGCSConfig(config map[string]interface{}) (*ExtendedRemoteState return nil, errors.WithStackTrace(err) } - extendedConfig.remoteStateConfigGCS = gcsConfig + extendedConfig.RemoteStateConfigGCS = gcsConfig return &extendedConfig, nil } -// Validate all the parameters of the given GCS remote state configuration -func validateGCSConfig(extendedConfig *ExtendedRemoteStateConfigGCS) error { - var config = extendedConfig.remoteStateConfigGCS +// ValidateGCSConfig validates all the parameters of the given GCS remote state configuration +func ValidateGCSConfig(extendedConfig *ExtendedRemoteStateConfigGCS) error { + var config = extendedConfig.RemoteStateConfigGCS if config.Bucket == "" { return errors.WithStackTrace(MissingRequiredGCSRemoteStateConfig("bucket")) @@ -290,8 +289,8 @@ func validateGCSConfig(extendedConfig *ExtendedRemoteStateConfigGCS) error { // confirms, create the bucket and enable versioning for it. func createGCSBucketIfNecessary(ctx context.Context, gcsClient *storage.Client, config *ExtendedRemoteStateConfigGCS, terragruntOptions *options.TerragruntOptions) error { // TODO: Remove lint suppression - if !DoesGCSBucketExist(gcsClient, &config.remoteStateConfigGCS) { //nolint:contextcheck - terragruntOptions.Logger.Debugf("Remote state GCS bucket %s does not exist. Attempting to create it", config.remoteStateConfigGCS.Bucket) + if !DoesGCSBucketExist(gcsClient, &config.RemoteStateConfigGCS) { //nolint:contextcheck + terragruntOptions.Logger.Debugf("Remote state GCS bucket %s does not exist. Attempting to create it", config.RemoteStateConfigGCS.Bucket) // A project must be specified in order for terragrunt to automatically create a storage bucket. if config.Project == "" { @@ -304,10 +303,10 @@ func createGCSBucketIfNecessary(ctx context.Context, gcsClient *storage.Client, } if terragruntOptions.FailIfBucketCreationRequired { - return BucketCreationNotAllowed(config.remoteStateConfigGCS.Bucket) + return BucketCreationNotAllowed(config.RemoteStateConfigGCS.Bucket) } - prompt := fmt.Sprintf("Remote state GCS bucket %s does not exist or you don't have permissions to access it. Would you like Terragrunt to create it?", config.remoteStateConfigGCS.Bucket) + prompt := fmt.Sprintf("Remote state GCS bucket %s does not exist or you don't have permissions to access it. Would you like Terragrunt to create it?", config.RemoteStateConfigGCS.Bucket) shouldCreateBucket, err := shell.PromptUserForYesNo(ctx, prompt, terragruntOptions) if err != nil { @@ -316,7 +315,7 @@ func createGCSBucketIfNecessary(ctx context.Context, gcsClient *storage.Client, if shouldCreateBucket { // To avoid any eventual consistency issues with creating a GCS bucket we use a retry loop. - description := "Create GCS bucket " + config.remoteStateConfigGCS.Bucket + description := "Create GCS bucket " + config.RemoteStateConfigGCS.Bucket return util.DoWithRetry(ctx, description, gcpMaxRetries, gcpSleepBetweenRetries, terragruntOptions.Logger, log.DebugLevel, func(ctx context.Context) error { // TODO: Remove lint suppression @@ -354,7 +353,7 @@ func CreateGCSBucketWithVersioning(gcsClient *storage.Client, config *ExtendedRe return err } - if err := WaitUntilGCSBucketExists(gcsClient, &config.remoteStateConfigGCS, terragruntOptions); err != nil { + if err := WaitUntilGCSBucketExists(gcsClient, &config.RemoteStateConfigGCS, terragruntOptions); err != nil { return err } @@ -367,14 +366,14 @@ func CreateGCSBucketWithVersioning(gcsClient *storage.Client, config *ExtendedRe func AddLabelsToGCSBucket(gcsClient *storage.Client, config *ExtendedRemoteStateConfigGCS, terragruntOptions *options.TerragruntOptions) error { if len(config.GCSBucketLabels) == 0 { - terragruntOptions.Logger.Debugf("No labels specified for bucket %s.", config.remoteStateConfigGCS.Bucket) + terragruntOptions.Logger.Debugf("No labels specified for bucket %s.", config.RemoteStateConfigGCS.Bucket) return nil } terragruntOptions.Logger.Debugf("Adding labels to GCS bucket with %s", config.GCSBucketLabels) ctx := context.Background() - bucket := gcsClient.Bucket(config.remoteStateConfigGCS.Bucket) + bucket := gcsClient.Bucket(config.RemoteStateConfigGCS.Bucket) bucketAttrs := storage.BucketAttrsToUpdate{} @@ -393,7 +392,7 @@ func AddLabelsToGCSBucket(gcsClient *storage.Client, config *ExtendedRemoteState // CreateGCSBucket creates the GCS bucket specified in the given config. func CreateGCSBucket(gcsClient *storage.Client, config *ExtendedRemoteStateConfigGCS, terragruntOptions *options.TerragruntOptions) error { - terragruntOptions.Logger.Debugf("Creating GCS bucket %s in project %s", config.remoteStateConfigGCS.Bucket, config.Project) + terragruntOptions.Logger.Debugf("Creating GCS bucket %s in project %s", config.RemoteStateConfigGCS.Bucket, config.Project) // The project ID to which the bucket belongs. This is only used when creating a new bucket during initialization. // Since buckets have globally unique names, the project ID is not required to access the bucket during normal @@ -401,7 +400,7 @@ func CreateGCSBucket(gcsClient *storage.Client, config *ExtendedRemoteStateConfi projectID := config.Project ctx := context.Background() - bucket := gcsClient.Bucket(config.remoteStateConfigGCS.Bucket) + bucket := gcsClient.Bucket(config.RemoteStateConfigGCS.Bucket) bucketAttrs := &storage.BucketAttrs{} @@ -411,22 +410,22 @@ func CreateGCSBucket(gcsClient *storage.Client, config *ExtendedRemoteStateConfi } if config.SkipBucketVersioning { - terragruntOptions.Logger.Debugf("Versioning is disabled for the remote state GCS bucket %s using 'skip_bucket_versioning' config.", config.remoteStateConfigGCS.Bucket) + terragruntOptions.Logger.Debugf("Versioning is disabled for the remote state GCS bucket %s using 'skip_bucket_versioning' config.", config.RemoteStateConfigGCS.Bucket) } else { - terragruntOptions.Logger.Debugf("Enabling versioning on GCS bucket %s", config.remoteStateConfigGCS.Bucket) + terragruntOptions.Logger.Debugf("Enabling versioning on GCS bucket %s", config.RemoteStateConfigGCS.Bucket) bucketAttrs.VersioningEnabled = true } if config.EnableBucketPolicyOnly { - terragruntOptions.Logger.Debugf("Enabling uniform bucket-level access on GCS bucket %s", config.remoteStateConfigGCS.Bucket) + terragruntOptions.Logger.Debugf("Enabling uniform bucket-level access on GCS bucket %s", config.RemoteStateConfigGCS.Bucket) bucketAttrs.BucketPolicyOnly = storage.BucketPolicyOnly{Enabled: true} } err := bucket.Create(ctx, projectID, bucketAttrs) - return errors.WithStackTraceAndPrefix(err, "Error creating GCS bucket %s", config.remoteStateConfigGCS.Bucket) + return errors.WithStackTraceAndPrefix(err, "Error creating GCS bucket %s", config.RemoteStateConfigGCS.Bucket) } // WaitUntilGCSBucketExists waits for the GCS bucket specified in the given config to be created. diff --git a/test/fixtures/gcs-output-from-remote-state/env1/app1/main.tf b/test/fixtures/gcs-output-from-remote-state/env1/app1/main.tf new file mode 100644 index 000000000..eb82ada98 --- /dev/null +++ b/test/fixtures/gcs-output-from-remote-state/env1/app1/main.tf @@ -0,0 +1,7 @@ +terraform { + backend "gcs" {} +} + +output "app1_text" { + value = "app1 output" +} diff --git a/test/fixtures/gcs-output-from-remote-state/env1/app1/terragrunt.hcl b/test/fixtures/gcs-output-from-remote-state/env1/app1/terragrunt.hcl new file mode 100644 index 000000000..d7b167d0f --- /dev/null +++ b/test/fixtures/gcs-output-from-remote-state/env1/app1/terragrunt.hcl @@ -0,0 +1,7 @@ +include { + path = find_in_parent_folders() +} + +dependencies { + paths = ["../app3"] +} diff --git a/test/fixtures/gcs-output-from-remote-state/env1/app2/main.tf b/test/fixtures/gcs-output-from-remote-state/env1/app2/main.tf new file mode 100644 index 000000000..0732ce876 --- /dev/null +++ b/test/fixtures/gcs-output-from-remote-state/env1/app2/main.tf @@ -0,0 +1,15 @@ +terraform { + backend "gcs" {} +} + +output "app1_text" { + value = var.app1_text +} + +output "app2_text" { + value = "app2 output" +} + +output "app3_text" { + value = var.app3_text +} diff --git a/test/fixtures/gcs-output-from-remote-state/env1/app2/terragrunt.hcl b/test/fixtures/gcs-output-from-remote-state/env1/app2/terragrunt.hcl new file mode 100644 index 000000000..afa0aca9b --- /dev/null +++ b/test/fixtures/gcs-output-from-remote-state/env1/app2/terragrunt.hcl @@ -0,0 +1,24 @@ +include { + path = find_in_parent_folders() +} + +dependency "app1" { + config_path = "../app1" + + mock_outputs = { + app1_text = "(known after apply-all)" + } +} + +dependency "app3" { + config_path = "../app3" + + mock_outputs = { + app3_text = "(known after apply-all)" + } +} + +inputs = { + app1_text = dependency.app1.outputs.app1_text + app3_text = dependency.app3.outputs.app3_text +} diff --git a/test/fixtures/gcs-output-from-remote-state/env1/app2/variables.tf b/test/fixtures/gcs-output-from-remote-state/env1/app2/variables.tf new file mode 100644 index 000000000..bd6688177 --- /dev/null +++ b/test/fixtures/gcs-output-from-remote-state/env1/app2/variables.tf @@ -0,0 +1,7 @@ +variable "app1_text" { + type = string +} + +variable "app3_text" { + type = string +} diff --git a/test/fixtures/gcs-output-from-remote-state/env1/app3/main.tf b/test/fixtures/gcs-output-from-remote-state/env1/app3/main.tf new file mode 100644 index 000000000..bbef8e818 --- /dev/null +++ b/test/fixtures/gcs-output-from-remote-state/env1/app3/main.tf @@ -0,0 +1,7 @@ +terraform { + backend "gcs" {} +} + +output "app3_text" { + value = "app3 output" +} diff --git a/test/fixtures/gcs-output-from-remote-state/env1/app3/terragrunt.hcl b/test/fixtures/gcs-output-from-remote-state/env1/app3/terragrunt.hcl new file mode 100644 index 000000000..3e1f65a23 --- /dev/null +++ b/test/fixtures/gcs-output-from-remote-state/env1/app3/terragrunt.hcl @@ -0,0 +1,3 @@ +include { + path = find_in_parent_folders() +} diff --git a/test/fixtures/gcs-output-from-remote-state/terragrunt.hcl b/test/fixtures/gcs-output-from-remote-state/terragrunt.hcl new file mode 100644 index 000000000..cbb6f909b --- /dev/null +++ b/test/fixtures/gcs-output-from-remote-state/terragrunt.hcl @@ -0,0 +1,11 @@ +# Configure Terragrunt to automatically store tfstate files in a GCS bucket +remote_state { + backend = "gcs" + + config = { + project = "__FILL_IN_PROJECT__" + location = "__FILL_IN_LOCATION__" + bucket = "__FILL_IN_BUCKET_NAME__" + prefix = "${path_relative_to_include()}/terraform.tfstate" + } +} diff --git a/test/integration_gcp_test.go b/test/integration_gcp_test.go index 7b2bbf121..c61a94055 100644 --- a/test/integration_gcp_test.go +++ b/test/integration_gcp_test.go @@ -3,11 +3,13 @@ package test_test import ( + "bytes" "context" goErrors "errors" "fmt" "os" "path" + "path/filepath" "strconv" "strings" "testing" @@ -24,12 +26,13 @@ import ( const ( terraformRemoteStateGcpRegion = "eu" - testFixtureGcsPath = "fixtures/gcs/" - testFixtureGcsByoBucketPath = "fixtures/gcs-byo-bucket/" - testFixtureGcsImpersonatePath = "fixtures/gcs-impersonate/" - testFixtureGcsNoBucket = "fixtures/gcs-no-bucket/" - testFixtureGcsNoPrefix = "fixtures/gcs-no-prefix/" - testFixtureGcsParallelStateInit = "fixtures/gcs-parallel-state-init" + testFixtureGcsPath = "fixtures/gcs/" + testFixtureGcsByoBucketPath = "fixtures/gcs-byo-bucket/" + testFixtureGcsImpersonatePath = "fixtures/gcs-impersonate/" + testFixtureGcsNoBucket = "fixtures/gcs-no-bucket/" + testFixtureGcsNoPrefix = "fixtures/gcs-no-prefix/" + testFixtureGcsOutputFromRemoteState = "fixtures/gcs-output-from-remote-state" + testFixtureGcsParallelStateInit = "fixtures/gcs-parallel-state-init" ) func TestGcpWorksWithBackend(t *testing.T) { @@ -130,6 +133,80 @@ func TestGcpParallelStateInit(t *testing.T) { runTerragrunt(t, "terragrunt run-all apply -auto-approve --terragrunt-non-interactive --terragrunt-working-dir "+tmpEnvPath) } +func TestGcpOutputFromRemoteState(t *testing.T) { //nolint: paralleltest + // NOTE: We can't run this test in parallel because there are other tests that also call `config.ClearOutputCache()`, but this function uses a global variable and sometimes it throws an unexpected error: + // "fixture-gcs-output-from-remote-state/env1/app2/terragrunt.hcl:23,38-48: Unsupported attribute; This object does not have an attribute named "app3_text"." + // t.Parallel() + + project := os.Getenv("GOOGLE_CLOUD_PROJECT") + location := terraformRemoteStateGcpRegion + gcsBucketName := "terragrunt-test-bucket-" + strings.ToLower(uniqueID()) + defer deleteGCSBucket(t, gcsBucketName) + + tmpEnvPath := copyEnvironment(t, testFixtureGcsOutputFromRemoteState) + + rootTerragruntConfigPath := util.JoinPath(tmpEnvPath, testFixtureGcsOutputFromRemoteState, config.DefaultTerragruntConfigPath) + copyTerragruntGCSConfigAndFillPlaceholders(t, rootTerragruntConfigPath, rootTerragruntConfigPath, project, location, gcsBucketName) + + environmentPath := fmt.Sprintf("%s/%s/env1", tmpEnvPath, testFixtureGcsOutputFromRemoteState) + + runTerragrunt(t, fmt.Sprintf("terragrunt apply --terragrunt-fetch-dependency-output-from-state --auto-approve --terragrunt-non-interactive --terragrunt-working-dir %s/app1", environmentPath)) + runTerragrunt(t, fmt.Sprintf("terragrunt apply --terragrunt-fetch-dependency-output-from-state --auto-approve --terragrunt-non-interactive --terragrunt-working-dir %s/app3", environmentPath)) + // Now delete dependencies cached state + config.ClearOutputCache() + require.NoError(t, os.Remove(filepath.Join(environmentPath, "/app1/.terraform/terraform.tfstate"))) + require.NoError(t, os.RemoveAll(filepath.Join(environmentPath, "/app1/.terraform"))) + require.NoError(t, os.Remove(filepath.Join(environmentPath, "/app3/.terraform/terraform.tfstate"))) + require.NoError(t, os.RemoveAll(filepath.Join(environmentPath, "/app3/.terraform"))) + + runTerragrunt(t, fmt.Sprintf("terragrunt apply --terragrunt-fetch-dependency-output-from-state --auto-approve --terragrunt-non-interactive --terragrunt-working-dir %s/app2", environmentPath)) + var ( + stdout bytes.Buffer + stderr bytes.Buffer + ) + + runTerragruntRedirectOutput(t, "terragrunt run-all output --terragrunt-fetch-dependency-output-from-state --terragrunt-non-interactive --terragrunt-log-level debug --terragrunt-working-dir "+environmentPath, &stdout, &stderr) + output := stdout.String() + + assert.True(t, strings.Contains(output, "app1 output")) + assert.True(t, strings.Contains(output, "app2 output")) + assert.True(t, strings.Contains(output, "app3 output")) + assert.False(t, strings.Contains(stderr.String(), "terraform output -json")) + + assert.True(t, (strings.Index(output, "app3 output") < strings.Index(output, "app1 output")) && (strings.Index(output, "app1 output") < strings.Index(output, "app2 output"))) +} + +func TestGcpMockOutputsFromRemoteState(t *testing.T) { //nolint: paralleltest + // NOTE: We can't run this test in parallel because there are other tests that also call `config.ClearOutputCache()`, but this function uses a global variable and sometimes it throws an unexpected error: + // "fixture-gcs-output-from-remote-state/env1/app2/terragrunt.hcl:23,38-48: Unsupported attribute; This object does not have an attribute named "app3_text"." + // t.Parallel() + + project := os.Getenv("GOOGLE_CLOUD_PROJECT") + location := terraformRemoteStateGcpRegion + gcsBucketName := "terragrunt-test-bucket-" + strings.ToLower(uniqueID()) + defer deleteGCSBucket(t, gcsBucketName) + + tmpEnvPath := copyEnvironment(t, testFixtureGcsOutputFromRemoteState) + + rootTerragruntConfigPath := util.JoinPath(tmpEnvPath, testFixtureGcsOutputFromRemoteState, config.DefaultTerragruntConfigPath) + copyTerragruntGCSConfigAndFillPlaceholders(t, rootTerragruntConfigPath, rootTerragruntConfigPath, project, location, gcsBucketName) + + environmentPath := fmt.Sprintf("%s/%s/env1", tmpEnvPath, testFixtureGcsOutputFromRemoteState) + + // applying only the app1 dependency, the app3 dependency was purposely not applied and should be mocked when running the app2 module + runTerragrunt(t, fmt.Sprintf("terragrunt apply --terragrunt-fetch-dependency-output-from-state --auto-approve --terragrunt-non-interactive --terragrunt-working-dir %s/app1", environmentPath)) + // Now delete dependencies cached state + config.ClearOutputCache() + require.NoError(t, os.Remove(filepath.Join(environmentPath, "/app1/.terraform/terraform.tfstate"))) + require.NoError(t, os.RemoveAll(filepath.Join(environmentPath, "/app1/.terraform"))) + + _, stderr, err := runTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt init --terragrunt-fetch-dependency-output-from-state --terragrunt-non-interactive --terragrunt-working-dir %s/app2", environmentPath)) + require.NoError(t, err) + + assert.True(t, strings.Contains(stderr, "Failed to read outputs")) + assert.True(t, strings.Contains(stderr, "fallback to mock outputs")) +} + func createTmpTerragruntGCSConfig(t *testing.T, templatesPath string, project string, location string, gcsBucketName string, configFileName string) string { t.Helper()