-
Notifications
You must be signed in to change notification settings - Fork 394
/
project.go
243 lines (215 loc) · 8.42 KB
/
project.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package stack
import (
"fmt"
"sort"
"strings"
"github.com/aws/amazon-ecs-cli-v2/internal/pkg/archer"
"github.com/aws/amazon-ecs-cli-v2/internal/pkg/aws/ecr"
"github.com/aws/amazon-ecs-cli-v2/internal/pkg/deploy"
"github.com/aws/amazon-ecs-cli-v2/internal/pkg/template"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/cloudformation"
"gopkg.in/yaml.v3"
)
// DeployedProjectMetadata wraps the Metadata field of a deployed
// project StackSet.
type DeployedProjectMetadata struct {
Metadata ProjectResourcesConfig `yaml:"Metadata"`
}
// ProjectResourcesConfig is a configuration for a deployed Project
// StackSet.
type ProjectResourcesConfig struct {
Accounts []string `yaml:"Accounts,flow"`
Apps []string `yaml:"Apps,flow"`
Project string `yaml:"Project"`
Version int `yaml:"Version"`
}
// ProjectStackConfig is for providing all the values to set up an
// environment stack and to interpret the outputs from it.
type ProjectStackConfig struct {
*deploy.CreateProjectInput
parser template.ReadParser
}
const (
projectTemplatePath = "project/project.yml"
projectResourcesTemplatePath = "project/cf.yml"
projectAdminRoleParamName = "AdminRoleName"
projectExecutionRoleParamName = "ExecutionRoleName"
projectDNSDelegationRoleParamName = "DNSDelegationRoleName"
projectOutputKMSKey = "KMSKeyARN"
projectOutputS3Bucket = "PipelineBucket"
projectOutputECRRepoPrefix = "ECRRepo"
projectDNSDelegatedAccountsKey = "ProjectDNSDelegatedAccounts"
projectDomainNameKey = "ProjectDomainName"
projectNameKey = "ProjectName"
projectDNSDelegationRoleName = "DNSDelegationRole"
)
// ProjectConfigFrom takes a template file and extracts the metadata block,
// and parses it into a projectStackConfig
func ProjectConfigFrom(template *string) (*ProjectResourcesConfig, error) {
resourceConfig := DeployedProjectMetadata{}
err := yaml.Unmarshal([]byte(*template), &resourceConfig)
return &resourceConfig.Metadata, err
}
// NewProjectStackConfig sets up a struct which can provide values to CloudFormation for
// spinning up an environment.
func NewProjectStackConfig(in *deploy.CreateProjectInput) *ProjectStackConfig {
return &ProjectStackConfig{
CreateProjectInput: in,
parser: template.New(),
}
}
// Template returns the environment CloudFormation template.
func (c *ProjectStackConfig) Template() (string, error) {
content, err := c.parser.Read(projectTemplatePath)
if err != nil {
return "", err
}
return content.String(), nil
}
// ResourceTemplate generates a StackSet template with all the Project-wide resources (ECR Repos, KMS keys, S3 buckets)
func (c *ProjectStackConfig) ResourceTemplate(config *ProjectResourcesConfig) (string, error) {
// Sort the account IDs and Apps so that the template we generate is deterministic
sort.Strings(config.Accounts)
sort.Strings(config.Apps)
content, err := c.parser.Parse(projectResourcesTemplatePath, struct {
*ProjectResourcesConfig
AppTagKey string
}{
config,
AppTagKey,
}, template.WithFuncs(templateFunctions))
if err != nil {
return "", err
}
return content.String(), err
}
// Parameters returns the parameters to be passed into a environment CloudFormation template.
func (c *ProjectStackConfig) Parameters() []*cloudformation.Parameter {
return []*cloudformation.Parameter{
{
ParameterKey: aws.String(projectAdminRoleParamName),
ParameterValue: aws.String(c.stackSetAdminRoleName()),
},
{
ParameterKey: aws.String(projectExecutionRoleParamName),
ParameterValue: aws.String(c.StackSetExecutionRoleName()),
},
{
ParameterKey: aws.String(projectDNSDelegatedAccountsKey),
ParameterValue: aws.String(strings.Join(c.dnsDelegationAccounts(), ",")),
},
{
ParameterKey: aws.String(projectDomainNameKey),
ParameterValue: aws.String(c.DomainName),
},
{
ParameterKey: aws.String(projectNameKey),
ParameterValue: aws.String(c.Project),
},
{
ParameterKey: aws.String(projectDNSDelegationRoleParamName),
ParameterValue: aws.String(dnsDelegationRoleName(c.Project)),
},
}
}
// Tags returns the tags that should be applied to the project CloudFormation stack.
func (c *ProjectStackConfig) Tags() []*cloudformation.Tag {
return mergeAndFlattenTags(c.AdditionalTags, map[string]string{
ProjectTagKey: c.Project,
})
}
// StackName returns the name of the CloudFormation stack (based on the project name).
func (c *ProjectStackConfig) StackName() string {
return fmt.Sprintf("%s-infrastructure-roles", c.Project)
}
// StackSetName returns the name of the CloudFormation StackSet (based on the project name).
func (c *ProjectStackConfig) StackSetName() string {
return fmt.Sprintf("%s-infrastructure", c.Project)
}
// StackSetDescription returns the description of the StackSet for project resources.
func (c *ProjectStackConfig) StackSetDescription() string {
return "ECS CLI Project Resources (ECR repos, KMS keys, S3 buckets)"
}
func (c *ProjectStackConfig) stackSetAdminRoleName() string {
return fmt.Sprintf("%s-adminrole", c.Project)
}
// StackSetAdminRoleARN returns the role ARN of the role used to administer the Project
// StackSet.
func (c *ProjectStackConfig) StackSetAdminRoleARN() string {
//TODO find a partition-neutral way to construct this ARN
return fmt.Sprintf("arn:aws:iam::%s:role/%s", c.AccountID, c.stackSetAdminRoleName())
}
// StackSetExecutionRoleName returns the role name of the role used to actually create
// Project resources.
func (c *ProjectStackConfig) StackSetExecutionRoleName() string {
return fmt.Sprintf("%s-executionrole", c.Project)
}
func (c *ProjectStackConfig) dnsDelegationAccounts() []string {
accounts := append(c.CreateProjectInput.DNSDelegationAccounts, c.CreateProjectInput.AccountID)
accountIDs := make(map[string]bool)
var uniqueAccountIDs []string
for _, entry := range accounts {
if _, value := accountIDs[entry]; !value {
accountIDs[entry] = true
uniqueAccountIDs = append(uniqueAccountIDs, entry)
}
}
return uniqueAccountIDs
}
// ToProjectRegionalResources takes a Project Resource Stack Instance stack, reads the output resources
// and returns a modeled ProjectRegionalResources.
func ToProjectRegionalResources(stack *cloudformation.Stack) (*archer.ProjectRegionalResources, error) {
regionalResources := archer.ProjectRegionalResources{
RepositoryURLs: map[string]string{},
}
for _, output := range stack.Outputs {
key := *output.OutputKey
value := *output.OutputValue
switch {
case key == projectOutputKMSKey:
regionalResources.KMSKeyARN = value
case key == projectOutputS3Bucket:
regionalResources.S3Bucket = value
case strings.HasPrefix(key, projectOutputECRRepoPrefix):
// If the output starts with the ECR Repo Prefix,
// we'll pull the ARN out and construct a URL from it.
uri, err := ecr.URIFromARN(value)
if err != nil {
return nil, err
}
// The app name for this repo is the Logical ID without
// the ECR Repo prefix.
safeAppName := strings.TrimPrefix(key, projectOutputECRRepoPrefix)
// It's possible we had to sanitize the app name (removing dashes),
// so return it back to its original form.
originalAppName := safeLogicalIDToOriginal(safeAppName)
regionalResources.RepositoryURLs[originalAppName] = uri
}
}
// Check to make sure the KMS key and S3 bucket exist in the stack. There isn't guranteed
// to be any ECR repos (for a brand new env without any apps), so we don't validate that.
if regionalResources.KMSKeyARN == "" {
return nil, fmt.Errorf("couldn't find KMS output key %s in stack %s", projectOutputKMSKey, *stack.StackId)
}
if regionalResources.S3Bucket == "" {
return nil, fmt.Errorf("couldn't find S3 bucket output key %s in stack %s", projectOutputS3Bucket, *stack.StackId)
}
return ®ionalResources, nil
}
// DNSDelegatedAccountsForStack looks through a stack's parameters for
// the parameter which stores the comma seperated list of account IDs
// which are permitted for DNS delegation.
func DNSDelegatedAccountsForStack(stack *cloudformation.Stack) []string {
for _, parameter := range stack.Parameters {
if *parameter.ParameterKey == projectDNSDelegatedAccountsKey {
return strings.Split(*parameter.ParameterValue, ",")
}
}
return []string{}
}
func dnsDelegationRoleName(projectName string) string {
return fmt.Sprintf("%s-%s", projectName, projectDNSDelegationRoleName)
}