Skip to content

Commit a40e930

Browse files
committed
chore: Add tests for GCS output from remote state
1 parent 48c9b13 commit a40e930

9 files changed

Lines changed: 281 additions & 7 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
terraform {
2+
backend "gcs" {}
3+
}
4+
5+
output "app1_text" {
6+
value = "app1 output"
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
include {
2+
path = find_in_parent_folders("root.hcl")
3+
}
4+
5+
dependencies {
6+
paths = ["../app3"]
7+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
terraform {
2+
backend "gcs" {}
3+
}
4+
5+
output "app1_text" {
6+
value = var.app1_text
7+
}
8+
9+
output "app2_text" {
10+
value = "app2 output"
11+
}
12+
13+
output "app3_text" {
14+
value = var.app3_text
15+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
include {
2+
path = find_in_parent_folders("root.hcl")
3+
}
4+
5+
dependency "app1" {
6+
config_path = "../app1"
7+
8+
mock_outputs = {
9+
app1_text = "(known after run --all apply)"
10+
}
11+
}
12+
13+
dependency "app3" {
14+
config_path = "../app3"
15+
16+
mock_outputs = {
17+
app3_text = "(known after run --all apply)"
18+
}
19+
}
20+
21+
inputs = {
22+
app1_text = dependency.app1.outputs.app1_text
23+
app3_text = dependency.app3.outputs.app3_text
24+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
variable "app1_text" {
2+
type = string
3+
}
4+
5+
variable "app3_text" {
6+
type = string
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
terraform {
2+
backend "gcs" {}
3+
}
4+
5+
output "app3_text" {
6+
value = "app3 output"
7+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
include {
2+
path = find_in_parent_folders("root.hcl")
3+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Configure Terragrunt to automatically store tfstate files in a GCS bucket
2+
remote_state {
3+
backend = "gcs"
4+
config = {
5+
project = "__FILL_IN_PROJECT__"
6+
location = "__FILL_IN_LOCATION__"
7+
bucket = "__FILL_IN_BUCKET_NAME__"
8+
prefix = "${path_relative_to_include()}"
9+
}
10+
}

test/integration_gcp_test.go

Lines changed: 201 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,14 @@ import (
2828
const (
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

4041
func 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

Comments
 (0)