@@ -17,6 +17,7 @@ import (
17
17
"github.com/aws/copilot-cli/internal/pkg/aws/cloudformation"
18
18
"github.com/aws/copilot-cli/internal/pkg/aws/cloudformation/stackset"
19
19
"github.com/aws/copilot-cli/internal/pkg/aws/ecs"
20
+ "github.com/aws/copilot-cli/internal/pkg/deploy"
20
21
"github.com/aws/copilot-cli/internal/pkg/stream"
21
22
"github.com/aws/copilot-cli/internal/pkg/term/log"
22
23
"github.com/aws/copilot-cli/internal/pkg/term/progress"
@@ -30,7 +31,8 @@ const (
30
31
waitForStackTimeout = 1 * time .Hour + 30 * time .Minute
31
32
32
33
// CloudFormation resource types.
33
- ecsServiceResourceType = "AWS::ECS::Service"
34
+ ecsServiceResourceType = "AWS::ECS::Service"
35
+ envControllerResourceType = "Custom::EnvControllerFunction"
34
36
)
35
37
36
38
// StackConfiguration represents the set of methods needed to deploy a cloudformation stack.
@@ -174,7 +176,7 @@ func (cf CloudFormation) renderStackChanges(in *renderStackChangesInput) error {
174
176
defer cancelWait ()
175
177
g , ctx := errgroup .WithContext (waitCtx )
176
178
177
- renderer , err := cf .createStackRenderer (g , ctx , changeSetID , in .stackName , in .stackDescription , progress.RenderOptions {})
179
+ renderer , err := cf .createChangeSetRenderer (g , ctx , changeSetID , in .stackName , in .stackDescription , progress.RenderOptions {})
178
180
if err != nil {
179
181
return err
180
182
}
@@ -190,7 +192,7 @@ func (cf CloudFormation) renderStackChanges(in *renderStackChangesInput) error {
190
192
return nil
191
193
}
192
194
193
- func (cf CloudFormation ) createStackRenderer (group * errgroup.Group , ctx context.Context , changeSetID , stackName , description string , opts progress.RenderOptions ) (progress.DynamicRenderer , error ) {
195
+ func (cf CloudFormation ) createChangeSetRenderer (group * errgroup.Group , ctx context.Context , changeSetID , stackName , description string , opts progress.RenderOptions ) (progress.DynamicRenderer , error ) {
194
196
changeSet , err := cf .cfnClient .DescribeChangeSet (changeSetID , stackName )
195
197
if err != nil {
196
198
return nil , err
@@ -206,30 +208,34 @@ func (cf CloudFormation) createStackRenderer(group *errgroup.Group, ctx context.
206
208
207
209
streamer := stream .NewStackStreamer (cf .cfnClient , stackName , changeSet .CreationTime )
208
210
children , err := cf .changeRenderers (changeRenderersInput {
209
- g : group ,
210
- ctx : ctx ,
211
- stackStreamer : streamer ,
212
- changes : changeSet .Changes ,
213
- descriptions : descriptions ,
214
- opts : progress .NestedRenderOptions (opts ),
211
+ g : group ,
212
+ ctx : ctx ,
213
+ stackName : stackName ,
214
+ stackStreamer : streamer ,
215
+ changes : changeSet .Changes ,
216
+ changeSetTimestamp : changeSet .CreationTime ,
217
+ descriptions : descriptions ,
218
+ opts : progress .NestedRenderOptions (opts ),
215
219
})
216
220
if err != nil {
217
221
return nil , err
218
222
}
219
- renderer := progress .ListeningStackRenderer (streamer , stackName , description , children , opts )
223
+ renderer := progress .ListeningChangeSetRenderer (streamer , stackName , description , children , opts )
220
224
group .Go (func () error {
221
225
return stream .Stream (ctx , streamer )
222
226
})
223
227
return renderer , nil
224
228
}
225
229
226
230
type changeRenderersInput struct {
227
- g * errgroup.Group // Group that all goroutines belong.
228
- ctx context.Context // Context associated with the group.
229
- stackStreamer progress.StackSubscriber // Streamer for the stack where changes belong.
230
- changes []* sdkcloudformation.Change // List of changes that will be applied to the stack.
231
- descriptions map [string ]string // Descriptions for the logical IDs of the changes.
232
- opts progress.RenderOptions // Display options that should be applied to the changes.
231
+ g * errgroup.Group // Group that all goroutines belong.
232
+ ctx context.Context // Context associated with the group.
233
+ stackName string // Name of the stack.
234
+ stackStreamer progress.StackSubscriber // Streamer for the stack where changes belong.
235
+ changes []* sdkcloudformation.Change // List of changes that will be applied to the stack.
236
+ changeSetTimestamp time.Time // ChangeSet creation time.
237
+ descriptions map [string ]string // Descriptions for the logical IDs of the changes.
238
+ opts progress.RenderOptions // Display options that should be applied to the changes.
233
239
}
234
240
235
241
// changeRenderers filters changes by resources that have a description and returns the appropriate progress.Renderer for each resource type.
@@ -243,6 +249,21 @@ func (cf CloudFormation) changeRenderers(in changeRenderersInput) ([]progress.Re
243
249
}
244
250
var renderer progress.Renderer
245
251
switch {
252
+ case aws .StringValue (change .ResourceChange .ResourceType ) == envControllerResourceType :
253
+ r , err := cf .createEnvControllerRenderer (& envControllerRendererInput {
254
+ g : in .g ,
255
+ ctx : in .ctx ,
256
+ workloadStackName : in .stackName ,
257
+ workloadTimestamp : in .changeSetTimestamp ,
258
+ change : change ,
259
+ description : description ,
260
+ serviceStack : in .stackStreamer ,
261
+ renderOpts : in .opts ,
262
+ })
263
+ if err != nil {
264
+ return nil , err
265
+ }
266
+ renderer = r
246
267
case aws .StringValue (change .ResourceChange .ResourceType ) == ecsServiceResourceType :
247
268
renderer = progress .ListeningECSServiceResourceRenderer (in .stackStreamer , cf .ecsClient , logicalID , description , progress.ECSServiceRendererOpts {
248
269
Group : in .g ,
@@ -254,19 +275,63 @@ func (cf CloudFormation) changeRenderers(in changeRenderersInput) ([]progress.Re
254
275
changeSetID := aws .StringValue (change .ResourceChange .ChangeSetId )
255
276
stackName := parseStackNameFromARN (aws .StringValue (change .ResourceChange .PhysicalResourceId ))
256
277
257
- r , err := cf .createStackRenderer (in .g , in .ctx , changeSetID , stackName , description , in .opts )
278
+ r , err := cf .createChangeSetRenderer (in .g , in .ctx , changeSetID , stackName , description , in .opts )
258
279
if err != nil {
259
280
return nil , err
260
281
}
261
282
renderer = r
262
283
default :
263
- renderer = progress .ListeningResourceRenderer (in .stackStreamer , logicalID , description , in .opts )
284
+ renderer = progress .ListeningResourceRenderer (in .stackStreamer , logicalID , description , progress.ResourceRendererOpts {
285
+ RenderOpts : in .opts ,
286
+ })
264
287
}
265
288
resources = append (resources , renderer )
266
289
}
267
290
return resources , nil
268
291
}
269
292
293
+ type envControllerRendererInput struct {
294
+ g * errgroup.Group
295
+ ctx context.Context
296
+ workloadStackName string
297
+ workloadTimestamp time.Time
298
+ change * sdkcloudformation.Change
299
+ description string
300
+ serviceStack progress.StackSubscriber
301
+ renderOpts progress.RenderOptions
302
+ }
303
+
304
+ func (cf CloudFormation ) createEnvControllerRenderer (in * envControllerRendererInput ) (progress.DynamicRenderer , error ) {
305
+ switch {
306
+ case isEnvControllerInAction (in .change ):
307
+ // We can't use the workloadStackName to retrieve the envStackName because app names and env names can contain
308
+ // dashes. So we fetch the tags belonging to the workload stack and generate the stack name from there.
309
+ workload , err := cf .cfnClient .Describe (in .workloadStackName )
310
+ if err != nil {
311
+ return nil , err
312
+ }
313
+ envStackName := fmt .Sprintf ("%s-%s" , parseAppNameFromTags (workload .Tags ), parseEnvNameFromTags (workload .Tags ))
314
+ body , err := cf .cfnClient .TemplateBody (envStackName )
315
+ if err != nil {
316
+ return nil , err
317
+ }
318
+ descriptions , err := cloudformation .ParseTemplateDescriptions (body )
319
+ if err != nil {
320
+ return nil , fmt .Errorf ("parse cloudformation template for resource descriptions: %w" , err )
321
+ }
322
+ streamer := stream .NewStackStreamer (cf .cfnClient , envStackName , in .workloadTimestamp )
323
+ in .g .Go (func () error {
324
+ return stream .Stream (in .ctx , streamer )
325
+ })
326
+ return progress .ListeningStackRenderer (streamer , envStackName , in .description , descriptions , in .renderOpts ), nil
327
+ default :
328
+ logicalID := aws .StringValue (in .change .ResourceChange .LogicalResourceId )
329
+ return progress .ListeningResourceRenderer (in .serviceStack , logicalID , in .description , progress.ResourceRendererOpts {
330
+ RenderOpts : in .renderOpts ,
331
+ }), nil
332
+ }
333
+ }
334
+
270
335
func (cf CloudFormation ) errOnFailedStack (stackName string ) error {
271
336
stack , err := cf .cfnClient .Describe (stackName )
272
337
if err != nil {
@@ -307,6 +372,42 @@ func parseStackNameFromARN(stackARN string) string {
307
372
return strings .Split (stackARN , "/" )[1 ]
308
373
}
309
374
375
+ func isEnvControllerInAction (controller * sdkcloudformation.Change ) bool {
376
+ switch action := aws .StringValue (controller .ResourceChange .Action ); action {
377
+ case sdkcloudformation .ChangeActionAdd :
378
+ return true
379
+ case sdkcloudformation .ChangeActionModify :
380
+ for _ , detail := range controller .ResourceChange .Details {
381
+ attribute := aws .StringValue (detail .Target .Name )
382
+ if attribute == "Parameters" {
383
+ // The env controller modifies the env stack only if the "Parameters" field is modified.
384
+ return true
385
+ }
386
+ }
387
+ return false
388
+ default :
389
+ return false
390
+ }
391
+ }
392
+
393
+ func parseAppNameFromTags (tags []* sdkcloudformation.Tag ) string {
394
+ for _ , t := range tags {
395
+ if aws .StringValue (t .Key ) == deploy .AppTagKey {
396
+ return aws .StringValue (t .Value )
397
+ }
398
+ }
399
+ return ""
400
+ }
401
+
402
+ func parseEnvNameFromTags (tags []* sdkcloudformation.Tag ) string {
403
+ for _ , t := range tags {
404
+ if aws .StringValue (t .Key ) == deploy .EnvTagKey {
405
+ return aws .StringValue (t .Value )
406
+ }
407
+ }
408
+ return ""
409
+ }
410
+
310
411
func stopSpinner (spinner * progress.Spinner , err error , label string ) {
311
412
if err == nil {
312
413
spinner .Stop (log .Ssuccessf ("%s\n " , label ))
0 commit comments