Skip to content

Commit 4a21197

Browse files
committed
chore: azure depdnency outputs reading
1 parent ad1e1af commit 4a21197

7 files changed

Lines changed: 47 additions & 10 deletions

File tree

internal/remotestate/backend/azurerm/backend.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,11 @@ func NewStateBlobClient(
160160

161161
keyed, err := sharedKeyConfig(ctx, cfg)
162162
if err != nil {
163-
return nil, fmt.Errorf("resolving storage account key for azurerm state access: %w", err)
163+
// ErrStateClientSetup marks this as a setup failure: the ARM key lookup answers
164+
// 404 for a wrong resource group, account, or subscription, and a caller must not
165+
// read that as "the state blob does not exist yet".
166+
return nil, fmt.Errorf("%w: resolving storage account key for azurerm state access: %w",
167+
ErrStateClientSetup, err)
164168
}
165169

166170
l.Debugf("%s: using shared-key authorization for direct state access", BackendName)

internal/remotestate/backend/azurerm/errors.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ func (e *CrossCloudMigrationError) Error() string {
5959
// always supplies.
6060
var ErrBackendOptionsRequired = errors.New("backend options are required")
6161

62+
// ErrStateClientSetup marks a failure to build the state client, as opposed to the
63+
// state blob being absent. The ARM key lookup answers 404 for a wrong resource
64+
// group, account, or subscription, which callers must not read as "not applied yet".
65+
var ErrStateClientSetup = errors.New("building azurerm state client")
66+
6267
// ErrAzureBackendExperimentRequired is returned when an azurerm backend
6368
// lifecycle operation is attempted without the `azure-backend` experiment
6469
// enabled. Match with errors.Is.

pkg/config/dependency.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1115,6 +1115,14 @@ func isAwsS3StateMissing(err error) bool {
11151115
// isRemoteStateMissing reports whether err means a supported backend's state object or its
11161116
// containing bucket/container does not exist yet.
11171117
func isRemoteStateMissing(err error) bool {
1118+
// A failure to build the client is not an absent state. The azurerm key lookup
1119+
// calls ARM, which answers 404 for a wrong resource group, account, or
1120+
// subscription; treating that as "not applied yet" would swap mock outputs into
1121+
// a run whose state actually exists.
1122+
if errors.Is(err, azurermbackend.ErrStateClientSetup) {
1123+
return false
1124+
}
1125+
11181126
return isAwsS3StateMissing(err) ||
11191127
errors.Is(err, storage.ErrObjectNotExist) ||
11201128
errors.Is(err, storage.ErrBucketNotExist) ||

pkg/config/dependency_internal_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1166,6 +1166,21 @@ func TestIsRemoteStateMissing(t *testing.T) {
11661166
ErrorCode: "AuthorizationFailure",
11671167
}),
11681168
},
1169+
// The azurerm key lookup calls ARM, which answers 404 for a wrong resource
1170+
// group, account, or subscription. Reading that as an absent state would swap
1171+
// mock outputs into a run whose state actually exists.
1172+
{
1173+
name: "Azure ARM resource group not found is a setup failure",
1174+
err: fmt.Errorf("%w: resolving storage account key: %w",
1175+
azurermbackend.ErrStateClientSetup,
1176+
&azcore.ResponseError{StatusCode: http.StatusNotFound, ErrorCode: "ResourceGroupNotFound"}),
1177+
},
1178+
{
1179+
name: "Azure ARM subscription not found is a setup failure",
1180+
err: fmt.Errorf("%w: resolving storage account key: %w",
1181+
azurermbackend.ErrStateClientSetup,
1182+
&azcore.ResponseError{StatusCode: http.StatusNotFound, ErrorCode: "SubscriptionNotFound"}),
1183+
},
11691184
}
11701185

11711186
for _, testCase := range testCases {

test/fixtures/output-from-remote-state-gcs/root.hcl

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
remote_state {
22
backend = "gcs"
33

4-
generate = {
5-
path = "backend.tf"
6-
if_exists = "overwrite"
7-
}
8-
94
config = {
105
project = "__FILL_IN_PROJECT__"
116
location = "__FILL_IN_LOCATION__"

test/integration_azure_test.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ func TestAzureDependencyFetchOutputFromState(t *testing.T) {
5050
producerPath := filepath.Join(rootPath, "producer")
5151
consumerPath := filepath.Join(rootPath, "consumer")
5252
consumerPlan := "terragrunt run plan --backend-bootstrap --experiment azure-backend " +
53-
"--dependency-fetch-output-from-state --non-interactive --working-dir " + consumerPath
53+
"--dependency-fetch-output-from-state --non-interactive --log-level debug --working-dir " + consumerPath
5454

5555
stdout, _, err := helpers.RunTerragruntCommandWithOutput(t, consumerPlan)
5656
require.NoError(t, err)
@@ -64,10 +64,15 @@ func TestAzureDependencyFetchOutputFromState(t *testing.T) {
6464
)
6565
require.NoError(t, err)
6666

67-
stdout, _, err = helpers.RunTerragruntCommandWithOutput(t, consumerPlan)
67+
stdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, consumerPlan)
6868
require.NoError(t, err)
6969
assert.Contains(t, stdout, "from-azure-state")
7070
assert.NotContains(t, stdout, "mock-azure-value")
71+
72+
// The value alone does not prove the optimization ran: `tofu output` returns it
73+
// too. Only the direct reader logs this, naming the blob it read.
74+
assert.Contains(t, stderr+stdout, "Fetching outputs directly from azurerm://",
75+
"outputs must come from the state blob, not from running tofu output")
7176
}
7277

7378
// Environment variables the live tests read, most specific first. The ARM_* /

test/integration_gcp_test.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ func TestGcpDependencyFetchOutputFromState(t *testing.T) {
6565
producerPath := filepath.Join(rootPath, "producer")
6666
consumerPath := filepath.Join(rootPath, "consumer")
6767
consumerPlan := "terragrunt run plan --backend-bootstrap --dependency-fetch-output-from-state " +
68-
"--non-interactive --working-dir " + consumerPath
68+
"--non-interactive --log-level debug --working-dir " + consumerPath
6969

7070
stdout, _, err := helpers.RunTerragruntCommandWithOutput(t, consumerPlan)
7171
require.NoError(t, err)
@@ -79,10 +79,15 @@ func TestGcpDependencyFetchOutputFromState(t *testing.T) {
7979
)
8080
require.NoError(t, err)
8181

82-
stdout, _, err = helpers.RunTerragruntCommandWithOutput(t, consumerPlan)
82+
stdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, consumerPlan)
8383
require.NoError(t, err)
8484
assert.Contains(t, stdout, "from-gcs-state")
8585
assert.NotContains(t, stdout, "mock-gcs-value")
86+
87+
// The value alone does not prove the optimization ran: `tofu output` returns it
88+
// too. Only the direct reader logs this, naming the object it read.
89+
assert.Contains(t, stderr+stdout, "Fetching outputs directly from gs://",
90+
"outputs must come from the state object, not from running tofu output")
8691
}
8792

8893
func TestGcpBootstrapBackend(t *testing.T) {

0 commit comments

Comments
 (0)