@@ -28,13 +28,14 @@ import (
2828const (
2929 terraformRemoteStateGcpRegion = "eu"
3030
31- testFixtureGcsPath = "fixtures/gcs/"
32- testFixtureGcsByoBucketPath = "fixtures/gcs-byo-bucket/"
33- testFixtureGcsImpersonatePath = "fixtures/gcs-impersonate/"
34- testFixtureGcsNoBucket = "fixtures/gcs-no-bucket/"
35- testFixtureGcsNoPrefix = "fixtures/gcs-no-prefix/"
36- testFixtureGcsParallelStateInit = "fixtures/gcs-parallel-state-init"
37- testFixtureGCSBackend = "fixtures/gcs-backend"
31+ testFixtureGcsPath = "fixtures/gcs/"
32+ testFixtureGcsByoBucketPath = "fixtures/gcs-byo-bucket/"
33+ testFixtureGcsImpersonatePath = "fixtures/gcs-impersonate/"
34+ testFixtureGcsNoBucket = "fixtures/gcs-no-bucket/"
35+ testFixtureGcsNoPrefix = "fixtures/gcs-no-prefix/"
36+ testFixtureGcsParallelStateInit = "fixtures/gcs-parallel-state-init"
37+ testFixtureGCSBackend = "fixtures/gcs-backend"
38+ testFixtureOutputFromRemoteStateGCS = "fixtures/output-from-remote-state-gcs"
3839)
3940
4041func TestGcpBootstrapBackend (t * testing.T ) {
@@ -645,3 +646,196 @@ func deleteGCSBucket(t *testing.T, bucketName string) {
645646 t .Fatalf ("Failed to delete GCS bucket %s: %v" , bucketName , err )
646647 }
647648}
649+
650+ func TestGcpOutputFromRemoteState (t * testing.T ) { //nolint: paralleltest
651+ // 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:
652+ // "fixtures/output-from-remote-state-gcs/env1/app2/terragrunt.hcl:23,38-48: Unsupported attribute; This object does not have an attribute named "app3_text"."
653+ // t.Parallel()
654+ gcsBucketName := "terragrunt-test-bucket-" + strings .ToLower (helpers .UniqueID ())
655+ defer deleteGCSBucket (t , gcsBucketName )
656+
657+ tmpEnvPath := helpers .CopyEnvironment (t , testFixtureOutputFromRemoteStateGCS )
658+
659+ rootTerragruntConfigPath := filepath .Join (tmpEnvPath , testFixtureOutputFromRemoteStateGCS , "root.hcl" )
660+ project := os .Getenv ("GOOGLE_CLOUD_PROJECT" )
661+ copyTerragruntGCSConfigAndFillPlaceholders (
662+ t ,
663+ rootTerragruntConfigPath ,
664+ rootTerragruntConfigPath ,
665+ project ,
666+ terraformRemoteStateGcpRegion ,
667+ gcsBucketName ,
668+ )
669+
670+ environmentPath := fmt .Sprintf ("%s/%s/env1" , tmpEnvPath , testFixtureOutputFromRemoteStateGCS )
671+
672+ helpers .RunTerragrunt (
673+ t ,
674+ fmt .Sprintf (
675+ "terragrunt run --backend-bootstrap --dependency-fetch-output-from-state " +
676+ "--non-interactive --working-dir %s/app1 -- apply -auto-approve" ,
677+ environmentPath ,
678+ ),
679+ )
680+ helpers .RunTerragrunt (
681+ t ,
682+ fmt .Sprintf (
683+ "terragrunt run --backend-bootstrap --dependency-fetch-output-from-state " +
684+ "--non-interactive --working-dir %s/app3 -- apply -auto-approve" ,
685+ environmentPath ,
686+ ),
687+ )
688+ // Now delete dependencies cached state
689+ // Since terraform runs from cache, the state files are in the cache directories
690+ app1CacheDir := helpers .FindCacheWorkingDir (t , filepath .Join (environmentPath , "app1" ))
691+ require .NotEmpty (t , app1CacheDir , "Cache directory for app1 should exist" )
692+ require .NoError (t , os .Remove (filepath .Join (app1CacheDir , ".terraform/terraform.tfstate" )))
693+ require .NoError (t , os .RemoveAll (filepath .Join (app1CacheDir , ".terraform" )))
694+ app3CacheDir := helpers .FindCacheWorkingDir (t , filepath .Join (environmentPath , "app3" ))
695+ require .NotEmpty (t , app3CacheDir , "Cache directory for app3 should exist" )
696+ require .NoError (t , os .Remove (filepath .Join (app3CacheDir , ".terraform/terraform.tfstate" )))
697+ require .NoError (t , os .RemoveAll (filepath .Join (app3CacheDir , ".terraform" )))
698+
699+ helpers .RunTerragrunt (
700+ t ,
701+ fmt .Sprintf (
702+ "terragrunt run --backend-bootstrap --dependency-fetch-output-from-state " +
703+ "--non-interactive --working-dir %s/app2 -- apply -auto-approve" ,
704+ environmentPath ,
705+ ),
706+ )
707+
708+ stdout , stderr , err := helpers .RunTerragruntCommandWithOutput (t , "terragrunt run --all output --backend-bootstrap --dependency-fetch-output-from-state --non-interactive --working-dir " + environmentPath )
709+ require .NoError (t , err )
710+
711+ assert .Contains (t , stdout , "app1 output" )
712+ assert .Contains (t , stdout , "app2 output" )
713+ assert .Contains (t , stdout , "app3 output" )
714+ assert .NotContains (t , stderr , "terraform output -json" )
715+ assert .NotContains (t , stderr , "tofu output -json" )
716+
717+ assert .True (
718+ t , (strings .Index (stdout , "app3 output" ) < strings .Index (stdout , "app1 output" )) &&
719+ (strings .Index (stdout , "app1 output" ) < strings .Index (stdout , "app2 output" )),
720+ )
721+ }
722+
723+ func TestGcpNoDependencyFetchOutputFromState (t * testing.T ) { //nolint: paralleltest
724+ // 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:
725+ // "fixtures/output-from-remote-state-gcs/env1/app2/terragrunt.hcl:23,38-48: Unsupported attribute; This object does not have an attribute named "app3_text"."
726+ // t.Parallel()
727+ gcsBucketName := "terragrunt-test-bucket-" + strings .ToLower (helpers .UniqueID ())
728+ defer deleteGCSBucket (t , gcsBucketName )
729+
730+ tmpEnvPath := helpers .CopyEnvironment (t , testFixtureOutputFromRemoteStateGCS )
731+
732+ rootTerragruntConfigPath := filepath .Join (tmpEnvPath , testFixtureOutputFromRemoteStateGCS , "root.hcl" )
733+ project := os .Getenv ("GOOGLE_CLOUD_PROJECT" )
734+ copyTerragruntGCSConfigAndFillPlaceholders (t , rootTerragruntConfigPath , rootTerragruntConfigPath , project , terraformRemoteStateGcpRegion , gcsBucketName )
735+
736+ environmentPath := fmt .Sprintf ("%s/%s/env1" , tmpEnvPath , testFixtureOutputFromRemoteStateGCS )
737+
738+ // Apply dependencies first
739+ helpers .RunTerragrunt (
740+ t ,
741+ fmt .Sprintf (
742+ "terragrunt apply --backend-bootstrap --dependency-fetch-output-from-state " +
743+ "--auto-approve --non-interactive --working-dir %s/app1" ,
744+ environmentPath ,
745+ ),
746+ )
747+ helpers .RunTerragrunt (
748+ t ,
749+ fmt .Sprintf (
750+ "terragrunt apply --backend-bootstrap --dependency-fetch-output-from-state " +
751+ "--auto-approve --non-interactive --working-dir %s/app3" ,
752+ environmentPath ,
753+ ),
754+ )
755+ // Now delete dependencies cached state
756+ // Since terraform runs from cache, the state files are in the cache directories
757+ app1CacheDir := helpers .FindCacheWorkingDir (t , filepath .Join (environmentPath , "app1" ))
758+ require .NotEmpty (t , app1CacheDir , "Cache directory for app1 should exist" )
759+ require .NoError (t , os .Remove (filepath .Join (app1CacheDir , ".terraform/terraform.tfstate" )))
760+ require .NoError (t , os .RemoveAll (filepath .Join (app1CacheDir , ".terraform" )))
761+ app3CacheDir := helpers .FindCacheWorkingDir (t , filepath .Join (environmentPath , "app3" ))
762+ require .NotEmpty (t , app3CacheDir , "Cache directory for app3 should exist" )
763+ require .NoError (t , os .Remove (filepath .Join (app3CacheDir , ".terraform/terraform.tfstate" )))
764+ require .NoError (t , os .RemoveAll (filepath .Join (app3CacheDir , ".terraform" )))
765+
766+ // Apply app2 with experiment enabled but --no-dependency-fetch-output-from-state flag set
767+ // This should fall back to using terraform output instead of fetching from state
768+ helpers .RunTerragrunt (
769+ t ,
770+ fmt .Sprintf (
771+ "terragrunt apply --backend-bootstrap --experiment dependency-fetch-output-from-state " +
772+ "--no-dependency-fetch-output-from-state --auto-approve --non-interactive --working-dir %s/app2" ,
773+ environmentPath ,
774+ ),
775+ )
776+
777+ // Run output command with experiment enabled but flag set to disable
778+ // When the flag is set, it should use terraform output instead of fetching from GCS
779+ stdout , stderr , err := helpers .RunTerragruntCommandWithOutput (
780+ t ,
781+ "terragrunt run --log-level debug --all output --backend-bootstrap --experiment dependency-fetch-output-from-state " +
782+ "--no-dependency-fetch-output-from-state --non-interactive --working-dir " + environmentPath ,
783+ )
784+ require .NoError (t , err )
785+
786+ // Verify outputs are still correct
787+ assert .Contains (t , stdout , "app1 output" )
788+ assert .Contains (t , stdout , "app2 output" )
789+ assert .Contains (t , stdout , "app3 output" )
790+
791+ // When --no-dependency-fetch-output-from-state is set, it should use terraform output
792+ // This means we should see "terraform output -json" or "tofu output -json" in stderr
793+ // (The exact command depends on which terraform implementation is being used)
794+ // This is the opposite of TestGcpOutputFromRemoteState which asserts this is NOT present
795+ assert .True (
796+ t ,
797+ strings .Contains (
798+ stderr ,
799+ "terraform output" ,
800+ ) || strings .Contains (
801+ stderr ,
802+ "tofu output" ,
803+ ),
804+ "Expected to see terraform/tofu output command when --no-dependency-fetch-output-from-state flag is set, but stderr was: %s" ,
805+ stderr ,
806+ )
807+ }
808+
809+ func TestGcpMockOutputsFromRemoteState (t * testing.T ) { //nolint: paralleltest
810+ // 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:
811+ // "fixtures/output-from-remote-state-gcs/env1/app2/terragrunt.hcl:23,38-48: Unsupported attribute; This object does not have an attribute named "app3_text"."
812+ // t.Parallel()
813+ project := os .Getenv ("GOOGLE_CLOUD_PROJECT" )
814+ if project == "" {
815+ t .Skipf ("Skipping test because GOOGLE_CLOUD_PROJECT environment variable is not set" )
816+ }
817+
818+ gcsBucketName := "terragrunt-test-bucket-" + strings .ToLower (helpers .UniqueID ())
819+ defer deleteGCSBucket (t , gcsBucketName )
820+
821+ tmpEnvPath := helpers .CopyEnvironment (t , testFixtureOutputFromRemoteStateGCS )
822+
823+ rootTerragruntConfigPath := filepath .Join (tmpEnvPath , testFixtureOutputFromRemoteStateGCS , "root.hcl" )
824+ copyTerragruntGCSConfigAndFillPlaceholders (t , rootTerragruntConfigPath , rootTerragruntConfigPath , project , terraformRemoteStateGcpRegion , gcsBucketName )
825+
826+ environmentPath := filepath .Join (tmpEnvPath , testFixtureOutputFromRemoteStateGCS , "env1" )
827+
828+ // applying only the app1 dependency, the app3 dependency was purposely not applied and should be mocked when running the app2 module
829+ helpers .RunTerragrunt (t , fmt .Sprintf ("terragrunt apply --dependency-fetch-output-from-state --auto-approve --backend-bootstrap --non-interactive --working-dir %s/app1" , environmentPath ))
830+ // Now delete dependencies cached state
831+ // Since terraform runs from cache, the state files are in the cache directories
832+ app1CacheDir := helpers .FindCacheWorkingDir (t , filepath .Join (environmentPath , "app1" ))
833+ require .NotEmpty (t , app1CacheDir , "Cache directory for app1 should exist" )
834+ require .NoError (t , os .RemoveAll (filepath .Join (app1CacheDir , ".terraform" )))
835+
836+ _ , stderr , err := helpers .RunTerragruntCommandWithOutput (t , fmt .Sprintf ("terragrunt init --dependency-fetch-output-from-state --non-interactive --working-dir %s/app2" , environmentPath ))
837+ require .NoError (t , err )
838+
839+ assert .Contains (t , stderr , "Failed to read outputs" )
840+ assert .Contains (t , stderr , "fallback to mock outputs" )
841+ }
0 commit comments