Skip to content

Commit 48c9b13

Browse files
committed
feat: Support fetching dependency output direct from GCS remote state
1 parent 4f7facf commit 48c9b13

1 file changed

Lines changed: 101 additions & 2 deletions

File tree

pkg/config/dependency.go

Lines changed: 101 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"fmt"
99
"io"
1010
"os"
11+
"path"
1112
"path/filepath"
1213
"slices"
1314
"strings"
@@ -18,12 +19,14 @@ import (
1819
"github.com/gruntwork-io/terragrunt/internal/awshelper"
1920
"github.com/gruntwork-io/terragrunt/internal/cache"
2021
"github.com/gruntwork-io/terragrunt/internal/experiment"
22+
"github.com/gruntwork-io/terragrunt/internal/gcphelper"
2123
"github.com/gruntwork-io/terragrunt/internal/iacargs"
2224
"github.com/gruntwork-io/terragrunt/internal/remotestate"
2325
"github.com/gruntwork-io/terragrunt/internal/report"
2426
"github.com/gruntwork-io/terragrunt/internal/runner/run"
2527
"github.com/gruntwork-io/terragrunt/pkg/log"
2628

29+
gcsbackend "github.com/gruntwork-io/terragrunt/internal/remotestate/backend/gcs"
2730
s3backend "github.com/gruntwork-io/terragrunt/internal/remotestate/backend/s3"
2831

2932
"github.com/hashicorp/go-getter"
@@ -657,7 +660,7 @@ func getTerragruntOutput(
657660

658661
jsonBytes, err := getOutputJSONWithCaching(ctx, pctx, l, targetConfigPath)
659662
if err != nil {
660-
if !isRenderJSONCommand(pctx) && !isRenderCommand(pctx) && !isAwsS3NoSuchKey(err) {
663+
if !isRenderJSONCommand(pctx) && !isRenderCommand(pctx) && !isAwsS3NoSuchKey(err) && !isGcsObjectNotFound(err) {
661664
return nil, true, err
662665
}
663666

@@ -700,6 +703,17 @@ func isAwsS3NoSuchKey(err error) bool {
700703
return false
701704
}
702705

706+
func isGcsObjectNotFound(err error) bool {
707+
if err != nil {
708+
errStr := err.Error()
709+
710+
return strings.Contains(errStr, "storage: object doesn't exist") ||
711+
strings.Contains(errStr, "storage.ObjectNotExist")
712+
}
713+
714+
return false
715+
}
716+
703717
// isRenderJSONCommand This function will true if terragrunt was invoked with render-json
704718
func isRenderJSONCommand(pctx *ParsingContext) bool {
705719
return pctx.TerragruntOptions.TerraformCliArgs.Contains(renderJSONCommand)
@@ -906,7 +920,8 @@ func getTerragruntOutputJSON(ctx context.Context, pctx *ParsingContext, l log.Lo
906920

907921
shouldFetchFromState := pctx.TerragruntOptions.Experiments.Evaluate(experiment.DependencyFetchOutputFromState) &&
908922
!pctx.TerragruntOptions.NoDependencyFetchOutputFromState &&
909-
remoteStateTGConfig.RemoteState.BackendName == s3backend.BackendName
923+
(remoteStateTGConfig.RemoteState.BackendName == s3backend.BackendName ||
924+
remoteStateTGConfig.RemoteState.BackendName == gcsbackend.BackendName)
910925

911926
if shouldFetchFromState {
912927
return getTerragruntOutputJSONFromRemoteState(
@@ -1111,6 +1126,20 @@ func getTerragruntOutputJSONFromRemoteState(
11111126

11121127
l.Debugf("Retrieved output from %s as json: %s using s3 bucket", targetTGOptions.TerragruntConfigPath, jsonBytes)
11131128

1129+
return jsonBytes, nil
1130+
case gcsbackend.BackendName:
1131+
jsonBytes, gcsGetErr := getTerragruntOutputJSONFromRemoteStateGCS(
1132+
ctx,
1133+
l,
1134+
targetTGOptions,
1135+
remoteState,
1136+
)
1137+
if gcsGetErr != nil {
1138+
return nil, gcsGetErr
1139+
}
1140+
1141+
l.Debugf("Retrieved output from %s as json: %s using gcs bucket", targetTGOptions.TerragruntConfigPath, jsonBytes)
1142+
11141143
return jsonBytes, nil
11151144
default:
11161145
l.Debugf("dependency-fetch-output-from-state experiment is not supported for backend %s, falling back to default output retrieval", backend)
@@ -1210,6 +1239,76 @@ func getTerragruntOutputJSONFromRemoteStateS3(ctx context.Context, l log.Logger,
12101239
return jsonOutputs, nil
12111240
}
12121241

1242+
// getTerragruntOutputJSONFromRemoteStateGCS pulls the output directly from a GCS bucket without calling Terraform
1243+
func getTerragruntOutputJSONFromRemoteStateGCS(ctx context.Context, l log.Logger, opts *options.TerragruntOptions, remoteState *remotestate.RemoteState) ([]byte, error) {
1244+
gcsConfigExtended, err := gcsbackend.Config(remoteState.BackendConfig).ExtendedGCSConfig()
1245+
if err != nil {
1246+
return nil, err
1247+
}
1248+
1249+
bucketName := gcsConfigExtended.RemoteStateConfigGCS.Bucket
1250+
1251+
var keyPath string
1252+
1253+
// Use path if set, otherwise join prefix with default.tfstate
1254+
if gcsConfigExtended.RemoteStateConfigGCS.Path != "" {
1255+
keyPath = gcsConfigExtended.RemoteStateConfigGCS.Path
1256+
} else {
1257+
keyPath = path.Join(gcsConfigExtended.RemoteStateConfigGCS.Prefix, "default.tfstate")
1258+
}
1259+
1260+
l.Debugf("Fetching outputs directly from gs://%s/%s", bucketName, keyPath)
1261+
1262+
gcpConfig := gcsConfigExtended.GetGCPSessionConfig()
1263+
1264+
client, err := gcphelper.CreateGCSClient(ctx, l, gcpConfig, opts)
1265+
if err != nil {
1266+
return nil, errors.New(err)
1267+
}
1268+
1269+
defer func() {
1270+
err := client.Close()
1271+
if err != nil {
1272+
l.Warnf("Failed to close GCS client: %v", err)
1273+
}
1274+
}()
1275+
1276+
bucket := client.Bucket(bucketName)
1277+
obj := bucket.Object(keyPath)
1278+
1279+
reader, err := obj.NewReader(ctx)
1280+
if err != nil {
1281+
return nil, err
1282+
}
1283+
1284+
defer func() {
1285+
err := reader.Close()
1286+
if err != nil {
1287+
l.Warnf("Failed to close GCS reader: %v", err)
1288+
}
1289+
}()
1290+
1291+
stateBody, err := io.ReadAll(reader)
1292+
if err != nil {
1293+
return nil, err
1294+
}
1295+
1296+
jsonState := string(stateBody)
1297+
jsonMap := make(map[string]any)
1298+
1299+
err = json.Unmarshal([]byte(jsonState), &jsonMap)
1300+
if err != nil {
1301+
return nil, err
1302+
}
1303+
1304+
jsonOutputs, err := json.Marshal(jsonMap["outputs"])
1305+
if err != nil {
1306+
return nil, err
1307+
}
1308+
1309+
return jsonOutputs, nil
1310+
}
1311+
12131312
// setupTerragruntOptionsForBareTerraform sets up a new TerragruntOptions struct that can be used to run terraform
12141313
// without going through the full RunTerragrunt operation.
12151314
func setupTerragruntOptionsForBareTerraform(

0 commit comments

Comments
 (0)