Skip to content

Commit a830133

Browse files
authored
chore: env manager patcher and service discovery namespace should work with bootstrap stack (#3966)
Previously, we have two fixes: 1. #3956 which patches the S3 permissions to the environment manager role if it's needed. 2. #3949 which preserves service discovery endpoint namespace from a previously deployed env stack. Fix 1 performs an environment template version check to infer whether the necessary permissions are present. This version check fails to take into account the `"bootstrap"` version, which is present when the environment is only bootstrapped (by running `env init` and not having run `env deploy`) Fix 2 grabs the old parameter from a deployed environment stack. It fails to take into consideration that, while an env stack that's on `"bootstrap"` version is not considered a "deployed environment" in Copilot, it is a deployed stack in CFN. By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the Apache 2.0 License.
1 parent 9cdf694 commit a830133

File tree

10 files changed

+91
-25
lines changed

10 files changed

+91
-25
lines changed

internal/pkg/cli/deploy/env.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ type appResourcesGetter interface {
3232

3333
type environmentDeployer interface {
3434
UpdateAndRenderEnvironment(conf deploycfn.StackConfiguration, bucketARN string, opts ...cloudformation.StackOption) error
35-
EnvironmentParameters(app, env string) ([]*awscfn.Parameter, error)
35+
DeployedEnvironmentParameters(app, env string) ([]*awscfn.Parameter, error)
3636
ForceUpdateOutputID(app, env string) (string, error)
3737
}
3838

@@ -171,7 +171,7 @@ func (d *envDeployer) GenerateCloudFormationTemplate(in *DeployEnvironmentInput)
171171
if err != nil {
172172
return nil, err
173173
}
174-
oldParams, err := d.envDeployer.EnvironmentParameters(d.app.Name, d.env.Name)
174+
oldParams, err := d.envDeployer.DeployedEnvironmentParameters(d.app.Name, d.env.Name)
175175
if err != nil {
176176
return nil, fmt.Errorf("describe environment stack parameters: %w", err)
177177
}
@@ -200,7 +200,7 @@ func (d *envDeployer) DeployEnvironment(in *DeployEnvironmentInput) error {
200200
if err != nil {
201201
return err
202202
}
203-
oldParams, err := d.envDeployer.EnvironmentParameters(d.app.Name, d.env.Name)
203+
oldParams, err := d.envDeployer.DeployedEnvironmentParameters(d.app.Name, d.env.Name)
204204
if err != nil {
205205
return fmt.Errorf("describe environment stack parameters: %w", err)
206206
}

internal/pkg/cli/deploy/env_test.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ func TestEnvDeployer_GenerateCloudFormationTemplate(t *testing.T) {
169169
m.appCFN.EXPECT().GetAppResourcesByRegion(gomock.Any(), gomock.Any()).Return(&stack.AppRegionalResources{
170170
S3Bucket: "mockS3Bucket",
171171
}, nil)
172-
m.envDeployer.EXPECT().EnvironmentParameters(gomock.Any(), gomock.Any()).Return(nil, errors.New("some error"))
172+
m.envDeployer.EXPECT().DeployedEnvironmentParameters(gomock.Any(), gomock.Any()).Return(nil, errors.New("some error"))
173173
},
174174
wantedError: errors.New("describe environment stack parameters: some error"),
175175
},
@@ -178,7 +178,7 @@ func TestEnvDeployer_GenerateCloudFormationTemplate(t *testing.T) {
178178
m.appCFN.EXPECT().GetAppResourcesByRegion(gomock.Any(), gomock.Any()).Return(&stack.AppRegionalResources{
179179
S3Bucket: "mockS3Bucket",
180180
}, nil)
181-
m.envDeployer.EXPECT().EnvironmentParameters(gomock.Any(), gomock.Any()).Return(nil, nil)
181+
m.envDeployer.EXPECT().DeployedEnvironmentParameters(gomock.Any(), gomock.Any()).Return(nil, nil)
182182
m.envDeployer.EXPECT().ForceUpdateOutputID(gomock.Any(), gomock.Any()).Return("", errors.New("some error"))
183183
},
184184
wantedError: errors.New("retrieve environment stack force update ID: some error"),
@@ -188,7 +188,7 @@ func TestEnvDeployer_GenerateCloudFormationTemplate(t *testing.T) {
188188
m.appCFN.EXPECT().GetAppResourcesByRegion(gomock.Any(), gomock.Any()).Return(&stack.AppRegionalResources{
189189
S3Bucket: "mockS3Bucket",
190190
}, nil)
191-
m.envDeployer.EXPECT().EnvironmentParameters(gomock.Any(), gomock.Any()).Return(nil, nil)
191+
m.envDeployer.EXPECT().DeployedEnvironmentParameters(gomock.Any(), gomock.Any()).Return(nil, nil)
192192
m.envDeployer.EXPECT().ForceUpdateOutputID(gomock.Any(), gomock.Any()).Return("", nil)
193193
m.stackSerializer.EXPECT().Template().Return("", errors.New("some error"))
194194
},
@@ -199,7 +199,7 @@ func TestEnvDeployer_GenerateCloudFormationTemplate(t *testing.T) {
199199
m.appCFN.EXPECT().GetAppResourcesByRegion(gomock.Any(), gomock.Any()).Return(&stack.AppRegionalResources{
200200
S3Bucket: "mockS3Bucket",
201201
}, nil)
202-
m.envDeployer.EXPECT().EnvironmentParameters(gomock.Any(), gomock.Any()).Return(nil, nil)
202+
m.envDeployer.EXPECT().DeployedEnvironmentParameters(gomock.Any(), gomock.Any()).Return(nil, nil)
203203
m.envDeployer.EXPECT().ForceUpdateOutputID(gomock.Any(), gomock.Any()).Return("", nil)
204204
m.stackSerializer.EXPECT().Template().Return("", nil)
205205
m.stackSerializer.EXPECT().SerializedParameters().Return("", errors.New("some error"))
@@ -211,7 +211,7 @@ func TestEnvDeployer_GenerateCloudFormationTemplate(t *testing.T) {
211211
m.appCFN.EXPECT().GetAppResourcesByRegion(mockApp, mockEnvRegion).Return(&stack.AppRegionalResources{
212212
S3Bucket: "mockS3Bucket",
213213
}, nil)
214-
m.envDeployer.EXPECT().EnvironmentParameters(mockAppName, mockEnvName).Return(nil, nil)
214+
m.envDeployer.EXPECT().DeployedEnvironmentParameters(mockAppName, mockEnvName).Return(nil, nil)
215215
m.envDeployer.EXPECT().ForceUpdateOutputID(gomock.Any(), gomock.Any()).Return("", nil)
216216
m.stackSerializer.EXPECT().Template().Return("aloo", nil)
217217
m.stackSerializer.EXPECT().SerializedParameters().Return("gobi", nil)
@@ -307,7 +307,7 @@ func TestEnvDeployer_DeployEnvironment(t *testing.T) {
307307
m.appCFN.EXPECT().GetAppResourcesByRegion(mockApp, mockEnvRegion).Return(&stack.AppRegionalResources{
308308
S3Bucket: "mockS3Bucket",
309309
}, nil)
310-
m.envDeployer.EXPECT().EnvironmentParameters(gomock.Any(), gomock.Any()).Return(nil, nil)
310+
m.envDeployer.EXPECT().DeployedEnvironmentParameters(gomock.Any(), gomock.Any()).Return(nil, nil)
311311
m.envDeployer.EXPECT().ForceUpdateOutputID(gomock.Any(), gomock.Any()).Return("", nil)
312312
m.prefixListGetter.EXPECT().CloudFrontManagedPrefixListID().Return("mockPrefixListID", nil).Times(0)
313313
m.envDeployer.EXPECT().UpdateAndRenderEnvironment(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil)
@@ -319,7 +319,7 @@ func TestEnvDeployer_DeployEnvironment(t *testing.T) {
319319
m.appCFN.EXPECT().GetAppResourcesByRegion(gomock.Any(), gomock.Any()).Return(&stack.AppRegionalResources{
320320
S3Bucket: "mockS3Bucket",
321321
}, nil)
322-
m.envDeployer.EXPECT().EnvironmentParameters(gomock.Any(), gomock.Any()).Return(nil, errors.New("some error"))
322+
m.envDeployer.EXPECT().DeployedEnvironmentParameters(gomock.Any(), gomock.Any()).Return(nil, errors.New("some error"))
323323
},
324324
wantedError: errors.New("describe environment stack parameters: some error"),
325325
},
@@ -328,7 +328,7 @@ func TestEnvDeployer_DeployEnvironment(t *testing.T) {
328328
m.appCFN.EXPECT().GetAppResourcesByRegion(gomock.Any(), gomock.Any()).Return(&stack.AppRegionalResources{
329329
S3Bucket: "mockS3Bucket",
330330
}, nil)
331-
m.envDeployer.EXPECT().EnvironmentParameters(gomock.Any(), gomock.Any()).Return(nil, nil)
331+
m.envDeployer.EXPECT().DeployedEnvironmentParameters(gomock.Any(), gomock.Any()).Return(nil, nil)
332332
m.envDeployer.EXPECT().ForceUpdateOutputID(gomock.Any(), gomock.Any()).Return("", errors.New("some error"))
333333
},
334334
wantedError: errors.New("retrieve environment stack force update ID: some error"),
@@ -339,7 +339,7 @@ func TestEnvDeployer_DeployEnvironment(t *testing.T) {
339339
S3Bucket: "mockS3Bucket",
340340
}, nil)
341341
m.prefixListGetter.EXPECT().CloudFrontManagedPrefixListID().Return("mockPrefixListID", nil).Times(0)
342-
m.envDeployer.EXPECT().EnvironmentParameters(gomock.Any(), gomock.Any()).Return(nil, nil)
342+
m.envDeployer.EXPECT().DeployedEnvironmentParameters(gomock.Any(), gomock.Any()).Return(nil, nil)
343343
m.envDeployer.EXPECT().ForceUpdateOutputID(gomock.Any(), gomock.Any()).Return("", nil)
344344
m.envDeployer.EXPECT().UpdateAndRenderEnvironment(gomock.Any(), gomock.Any(), gomock.Any()).Return(errors.New("some error"))
345345
},
@@ -351,7 +351,7 @@ func TestEnvDeployer_DeployEnvironment(t *testing.T) {
351351
S3Bucket: "mockS3Bucket",
352352
}, nil)
353353
m.prefixListGetter.EXPECT().CloudFrontManagedPrefixListID().Return("mockPrefixListID", nil).Times(0)
354-
m.envDeployer.EXPECT().EnvironmentParameters(gomock.Any(), gomock.Any()).Return(nil, nil)
354+
m.envDeployer.EXPECT().DeployedEnvironmentParameters(gomock.Any(), gomock.Any()).Return(nil, nil)
355355
m.envDeployer.EXPECT().ForceUpdateOutputID(gomock.Any(), gomock.Any()).Return("", nil)
356356
m.envDeployer.EXPECT().UpdateAndRenderEnvironment(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil)
357357
},

internal/pkg/cli/deploy/mocks/mock_env.go

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/pkg/cli/deploy/patch/env.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/aws/copilot-cli/internal/pkg/aws/cloudformation"
1313
"github.com/aws/copilot-cli/internal/pkg/aws/s3"
1414
"github.com/aws/copilot-cli/internal/pkg/config"
15+
"github.com/aws/copilot-cli/internal/pkg/deploy"
1516
"github.com/aws/copilot-cli/internal/pkg/term/log"
1617
"golang.org/x/mod/semver"
1718
"gopkg.in/yaml.v3"
@@ -122,6 +123,10 @@ func isManagerRoleAllowedToUpload(body string) (bool, error) {
122123
if err := yaml.Unmarshal([]byte(body), &tpl); err != nil {
123124
return false, fmt.Errorf("unmarshal environment template to detect Metadata.Version: %v", err)
124125
}
126+
if tpl.Metadata.Version == deploy.EnvTemplateVersionBootstrap {
127+
// "bootstrap" version is introduced after v1.9.0. The environment manager roles must have had the permissions.
128+
return true, nil
129+
}
125130
if !semver.IsValid(tpl.Metadata.Version) { // The template doesn't contain a version.
126131
return false, nil
127132
}

internal/pkg/deploy/cloudformation/cloudformation.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ type cfnClient interface {
7979
ErrorEvents(stackName string) ([]cloudformation.StackEvent, error)
8080
Outputs(stack *cloudformation.Stack) (map[string]string, error)
8181
StackResources(name string) ([]*cloudformation.StackResource, error)
82+
Metadata(opts cloudformation.MetadataOpts) (string, error)
8283

8384
// Methods vended by the aws sdk struct.
8485
DescribeStackEvents(*sdkcloudformation.DescribeStackEventsInput) (*sdkcloudformation.DescribeStackEventsOutput, error)

internal/pkg/deploy/cloudformation/cloudformation_integration_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -501,7 +501,7 @@ func Test_Environment_Deployment_Integration(t *testing.T) {
501501
}
502502

503503
// Deploy the environment and wait for it to be complete.
504-
oldParams, err := deployer.EnvironmentParameters(environmentToDeploy.App.Name, environmentToDeploy.Name)
504+
oldParams, err := deployer.DeployedEnvironmentParameters(environmentToDeploy.App.Name, environmentToDeploy.Name)
505505
require.NoError(t, err)
506506
lastForceUpdateID, err := deployer.ForceUpdateOutputID(environmentToDeploy.App.Name, environmentToDeploy.Name)
507507
require.NoError(t, err)

internal/pkg/deploy/cloudformation/env.go

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
"github.com/aws/aws-sdk-go/aws/arn"
1212
"github.com/aws/copilot-cli/internal/pkg/template"
13+
"gopkg.in/yaml.v3"
1314

1415
"github.com/aws/aws-sdk-go/aws"
1516
awscfn "github.com/aws/aws-sdk-go/service/cloudformation"
@@ -116,8 +117,15 @@ func (cf CloudFormation) ForceUpdateOutputID(app, env string) (string, error) {
116117
return "", nil
117118
}
118119

119-
// EnvironmentParameters returns the environment stack's parameters.
120-
func (cf CloudFormation) EnvironmentParameters(appName, envName string) ([]*awscfn.Parameter, error) {
120+
// DeployedEnvironmentParameters returns the environment stack's parameters.
121+
func (cf CloudFormation) DeployedEnvironmentParameters(appName, envName string) ([]*awscfn.Parameter, error) {
122+
isInitial, err := cf.isInitialDeployment(appName, envName)
123+
if err != nil {
124+
return nil, err
125+
}
126+
if isInitial {
127+
return nil, nil
128+
}
121129
out, err := cf.cachedStack(stack.NameForEnv(appName, envName))
122130
if err != nil {
123131
return nil, err
@@ -188,3 +196,18 @@ func (cf CloudFormation) cachedStack(stackName string) (*cloudformation.StackDes
188196
cf.cachedDeployedStack = stackDescr
189197
return cf.cachedDeployedStack, nil
190198
}
199+
200+
// isInitialDeployment returns whether this is the first deployment of the environment stack.
201+
func (cf CloudFormation) isInitialDeployment(appName, envName string) (bool, error) {
202+
raw, err := cf.cfnClient.Metadata(cloudformation.MetadataWithStackName(stack.NameForEnv(appName, envName)))
203+
if err != nil {
204+
return false, fmt.Errorf("get metadata of stack %q: %w", stack.NameForEnv(appName, envName), err)
205+
}
206+
metadata := struct {
207+
Version string `yaml:"Version"`
208+
}{}
209+
if err := yaml.Unmarshal([]byte(raw), &metadata); err != nil {
210+
return false, fmt.Errorf("unmarshal Metadata property to read Version: %w", err)
211+
}
212+
return metadata.Version == deploy.EnvTemplateVersionBootstrap, nil
213+
}

internal/pkg/deploy/cloudformation/env_test.go

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ func TestCloudFormation_EnvironmentTemplate(t *testing.T) {
4848
}
4949
}
5050

51-
func TestCloudFormation_EnvironmentParameters(t *testing.T) {
51+
func TestCloudFormation_DeployedEnvironmentParameters(t *testing.T) {
5252
testCases := map[string]struct {
5353
inAppName string
5454
inEnvName string
@@ -57,11 +57,31 @@ func TestCloudFormation_EnvironmentParameters(t *testing.T) {
5757
wantedParams []*awscfn.Parameter
5858
wantedErr error
5959
}{
60+
"error retrieving metadata": {
61+
inAppName: "phonetool",
62+
inEnvName: "test",
63+
inClient: func(ctrl *gomock.Controller) *mocks.MockcfnClient {
64+
m := mocks.NewMockcfnClient(ctrl)
65+
m.EXPECT().Metadata(gomock.Any()).Return("", errors.New("some error"))
66+
return m
67+
},
68+
wantedErr: errors.New("get metadata of stack \"phonetool-test\": some error"),
69+
},
70+
"returns nil if the version is bootstrap": {
71+
inAppName: "phonetool",
72+
inEnvName: "test",
73+
inClient: func(ctrl *gomock.Controller) *mocks.MockcfnClient {
74+
m := mocks.NewMockcfnClient(ctrl)
75+
m.EXPECT().Metadata(gomock.Any()).Return(`Version: bootstrap`, nil)
76+
return m
77+
},
78+
},
6079
"should return stack parameters from a stack description": {
6180
inAppName: "phonetool",
6281
inEnvName: "test",
6382
inClient: func(ctrl *gomock.Controller) *mocks.MockcfnClient {
6483
m := mocks.NewMockcfnClient(ctrl)
84+
m.EXPECT().Metadata(gomock.Any()).Return(`Version: `, nil)
6585
m.EXPECT().Describe("phonetool-test").Return(&cloudformation.StackDescription{
6686
Parameters: []*awscfn.Parameter{
6787
{
@@ -85,6 +105,7 @@ func TestCloudFormation_EnvironmentParameters(t *testing.T) {
85105
inEnvName: "test",
86106
inClient: func(ctrl *gomock.Controller) *mocks.MockcfnClient {
87107
m := mocks.NewMockcfnClient(ctrl)
108+
m.EXPECT().Metadata(gomock.Any()).Return(`Version: v1.21.0`, nil)
88109
m.EXPECT().Describe(gomock.Any()).Return(nil, errors.New("some error"))
89110
return m
90111
},
@@ -102,7 +123,7 @@ func TestCloudFormation_EnvironmentParameters(t *testing.T) {
102123
}
103124

104125
// WHEN
105-
actual, err := cf.EnvironmentParameters(tc.inAppName, tc.inEnvName)
126+
actual, err := cf.DeployedEnvironmentParameters(tc.inAppName, tc.inEnvName)
106127
if tc.wantedErr != nil {
107128
require.EqualError(t, err, tc.wantedErr.Error())
108129
} else {

internal/pkg/deploy/cloudformation/mocks/mock_cloudformation.go

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/pkg/deploy/env.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ const (
1414
// LegacyEnvTemplateVersion is the version associated with the environment template before we started versioning.
1515
LegacyEnvTemplateVersion = "v0.0.0"
1616
// LatestEnvTemplateVersion is the latest version number available for environment templates.
17-
LatestEnvTemplateVersion = "v1.12.2"
17+
LatestEnvTemplateVersion = "v1.12.2"
18+
EnvTemplateVersionBootstrap = "bootstrap"
1819
)
1920

2021
// CreateEnvironmentInput holds the fields required to deploy an environment.

0 commit comments

Comments
 (0)