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
704718func 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.
12151314func setupTerragruntOptionsForBareTerraform (
0 commit comments