@@ -11,6 +11,7 @@ import (
1111 "path/filepath"
1212 "strconv"
1313 "strings"
14+ "time"
1415
1516 "github.com/aws/aws-sdk-go-v2/aws"
1617 awsconfig "github.com/aws/aws-sdk-go-v2/config"
@@ -56,7 +57,10 @@ func NewS3PlanStore(cfg S3PlanStoreConfig, logger logging.SimpleLogging) (*S3Pla
5657 opts = append (opts , awsconfig .WithSharedConfigProfile (cfg .Profile ))
5758 }
5859
59- awsCfg , err := awsconfig .LoadDefaultConfig (context .Background (), opts ... )
60+ ctx , cancel := context .WithTimeout (context .Background (), 10 * time .Second )
61+ defer cancel ()
62+
63+ awsCfg , err := awsconfig .LoadDefaultConfig (ctx , opts ... )
6064 if err != nil {
6165 return nil , fmt .Errorf ("loading AWS config: %w" , err )
6266 }
@@ -77,7 +81,7 @@ func NewS3PlanStore(cfg S3PlanStoreConfig, logger logging.SimpleLogging) (*S3Pla
7781
7882 client := s3 .NewFromConfig (awsCfg , s3Opts ... )
7983
80- if _ , err := client .HeadBucket (context . Background () , & s3.HeadBucketInput {
84+ if _ , err := client .HeadBucket (ctx , & s3.HeadBucketInput {
8185 Bucket : aws .String (cfg .Bucket ),
8286 }); err != nil {
8387 return nil , fmt .Errorf ("validating S3 plan store bucket %q: %w" , cfg .Bucket , err )
@@ -144,9 +148,16 @@ func (s *S3PlanStore) Load(ctx command.ProjectContext, planPath string) error {
144148 // Reject stale plans: the plan must have been created at the same commit
145149 // the PR currently points to. This prevents applying outdated plans after
146150 // new commits are pushed (e.g. across pod restarts).
147- // Note: S3 normalizes user-defined metadata keys to title case in responses,
148- // so "head-commit" (as written in Save) becomes "Head-Commit" here.
149- planCommit := resp .Metadata ["Head-Commit" ]
151+ // Note: different S3/S3-compatible implementations may return user-defined
152+ // metadata keys with different casing, so we look up "head-commit"
153+ // case-insensitively.
154+ var planCommit string
155+ for k , v := range resp .Metadata {
156+ if strings .EqualFold (k , "head-commit" ) {
157+ planCommit = v
158+ break
159+ }
160+ }
150161 if planCommit == "" {
151162 return fmt .Errorf ("plan in S3 has no head-commit metadata (key=%s) — run plan again" , key )
152163 }
@@ -176,15 +187,15 @@ func (s *S3PlanStore) Load(ctx command.ProjectContext, planPath string) error {
176187func (s * S3PlanStore ) Remove (ctx command.ProjectContext , planPath string ) error {
177188 key := s .s3Key (ctx , planPath )
178189
179- _ , err := s .client .DeleteObject (context .Background (), & s3.DeleteObjectInput {
190+ if _ , err := s .client .DeleteObject (context .Background (), & s3.DeleteObjectInput {
180191 Bucket : aws .String (s .bucket ),
181192 Key : aws .String (key ),
182- })
183- if err != nil {
184- return fmt .Errorf ("deleting plan from S3 (key=%s): %w" , key , err )
193+ }); err != nil {
194+ s .logger .Warn ("failed to delete plan from S3 (key=%s): %v" , key , err )
195+ } else {
196+ s .logger .Debug ("deleted plan from s3://%s/%s" , s .bucket , key )
185197 }
186198
187- s .logger .Debug ("deleted plan from s3://%s/%s" , s .bucket , key )
188199 return utils .RemoveIgnoreNonExistent (planPath )
189200}
190201
@@ -227,6 +238,14 @@ func (s *S3PlanStore) RestorePlans(pullDir, owner, repo string, pullNum int) err
227238
228239 // Strip the prefix up to and including <pullNum>/ to get the relative path.
229240 relPath := strings .TrimPrefix (key , listPrefix )
241+ relPath = filepath .Clean (relPath )
242+ if relPath == "." || relPath == string (os .PathSeparator ) {
243+ s .logger .Info ("skipping S3 object with empty relative path (key=%s, prefix=%s)" , key , listPrefix )
244+ continue
245+ }
246+ if filepath .IsAbs (relPath ) || relPath == ".." || strings .HasPrefix (relPath , ".." + string (os .PathSeparator )) {
247+ return fmt .Errorf ("refusing to restore plan outside pull dir (key=%s, relPath=%s)" , key , relPath )
248+ }
230249 localPath := filepath .Join (pullDir , relPath )
231250
232251 if err := os .MkdirAll (filepath .Dir (localPath ), 0o700 ); err != nil {
@@ -295,7 +314,8 @@ func (s *S3PlanStore) DeleteForPull(owner, repo string, pullNum int) error {
295314 Bucket : aws .String (s .bucket ),
296315 Key : aws .String (key ),
297316 }); err != nil {
298- return fmt .Errorf ("deleting plan from S3 (key=%s): %w" , key , err )
317+ s .logger .Warn ("failed to delete plan from S3 (key=%s): %v" , key , err )
318+ continue
299319 }
300320 deleted ++
301321 }
0 commit comments