/
database_upsert.go
228 lines (189 loc) · 8.13 KB
/
database_upsert.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
package workflows
import (
"crypto/rand"
"fmt"
"os"
"strconv"
"strings"
"github.com/stelligent/mu/common"
"github.com/stelligent/mu/templates"
)
// NewDatabaseUpserter create a new workflow for deploying a database in an environment
func NewDatabaseUpserter(ctx *common.Context, environmentName string) Executor {
workflow := new(databaseWorkflow)
workflow.codeRevision = ctx.Config.Repo.Revision
workflow.repoName = ctx.Config.Repo.Slug
cliExtension := new(common.CliAdditions)
ecsImportParams := make(map[string]string)
return newPipelineExecutor(
workflow.databaseInput(ctx, "", environmentName),
newConditionalExecutor(workflow.hasDatabase(),
newPipelineExecutor(
workflow.databaseEnvironmentLoader(ctx.Config.Namespace, environmentName, ctx.StackManager, ecsImportParams, ctx.ElbManager),
workflow.databaseRolesetUpserter(ctx.RolesetManager, ctx.RolesetManager, environmentName),
workflow.databaseMasterPassword(ctx.Config.Namespace, &ctx.Config.Service, &ecsImportParams, environmentName, ctx.ParamManager, cliExtension),
workflow.databaseDeployer(ctx.Config.Namespace, &ctx.Config.Service, ecsImportParams, environmentName, ctx.StackManager, ctx.StackManager, ctx.RdsManager),
),
nil),
)
}
func (workflow *databaseWorkflow) databaseEnvironmentLoader(namespace string, environmentName string, stackWaiter common.StackWaiter, ecsImportParams map[string]string, elbRuleLister common.ElbRuleLister) Executor {
return func() error {
ecsStackName := common.CreateStackName(namespace, common.StackTypeEnv, environmentName)
ecsStack := stackWaiter.AwaitFinalStatus(ecsStackName)
if ecsStack == nil {
return fmt.Errorf("Unable to find stack '%s' for environment '%s'", ecsStackName, environmentName)
}
ecsImportParams["VpcId"] = fmt.Sprintf("%s-VpcId", ecsStackName)
ecsImportParams["InstanceSecurityGroup"] = fmt.Sprintf("%s-InstanceSecurityGroup", ecsStackName)
ecsImportParams["InstanceSubnetIds"] = fmt.Sprintf("%s-InstanceSubnetIds", ecsStackName)
return nil
}
}
func (workflow *databaseWorkflow) databaseRolesetUpserter(rolesetUpserter common.RolesetUpserter, rolesetGetter common.RolesetGetter, environmentName string) Executor {
return func() error {
err := rolesetUpserter.UpsertCommonRoleset()
if err != nil {
return err
}
commonRoleset, err := rolesetGetter.GetCommonRoleset()
if err != nil {
return err
}
workflow.cloudFormationRoleArn = commonRoleset["CloudFormationRoleArn"]
err = rolesetUpserter.UpsertServiceRoleset(environmentName, workflow.serviceName, workflow.appRevisionBucket, workflow.databaseName)
if err != nil {
return err
}
serviceRoleset, err := rolesetGetter.GetServiceRoleset(environmentName, workflow.serviceName)
if err != nil {
return err
}
workflow.databaseKeyArn = serviceRoleset["DatabaseKeyArn"]
if workflow.databaseKeyArn == "" {
return fmt.Errorf("Missing `DatabaseKeyArn`...maybe you need to run `mu pipeline up` again to add the KMS key to the IAM stack?")
}
return nil
}
}
// Fetch password parameter if needed
func (workflow *databaseWorkflow) databaseMasterPassword(namespace string,
service *common.Service, params *map[string]string, environmentName string,
paramManager common.ParamManager, cliExtension common.CliExtension) Executor {
return func() error {
//DatabaseMasterPassword:
if workflow.ssmParamIsManaged {
dbPassVersion, err := paramManager.ParamVersion(workflow.ssmParamName)
if err != nil {
log.Warningf("Error with ParamVersion for DatabaseMasterPassword, assuming empty: %s", err)
answer, err := cliExtension.Prompt("Error retrieving DatabaseMasterPassword. Set a new DatabaseMasterPassword", false)
if err != nil {
log.Errorf("Error with command input: %s", err)
os.Exit(1)
}
if !answer {
os.Exit(126)
}
}
if dbPassVersion == 0 {
dbPass := randomPassword(32)
err = paramManager.SetParam(workflow.ssmParamName, dbPass, workflow.databaseKeyArn)
if err != nil {
return err
}
dbPassVersion = 1
}
(*params)["DatabaseMasterPassword"] = fmt.Sprintf("{{resolve:ssm-secure:%s:%d}}", workflow.ssmParamName, dbPassVersion)
} else {
(*params)["DatabaseMasterPassword"] = fmt.Sprintf("{{resolve:ssm-secure:%s}}", workflow.ssmParamName)
}
return nil
}
}
func (workflow *databaseWorkflow) databaseDeployer(namespace string, service *common.Service, stackParams map[string]string, environmentName string, stackUpserter common.StackUpserter, stackWaiter common.StackWaiter, rdsSetter common.RdsIamAuthenticationSetter) Executor {
return func() error {
if service.Database.Name == "" {
log.Noticef("Skipping database since database.name is unset")
return nil
}
log.Noticef("Deploying database '%s' to '%s'", workflow.serviceName, environmentName)
dbStackName := common.CreateStackName(namespace, common.StackTypeDatabase, workflow.serviceName, environmentName)
dbConfig := service.Database.GetDatabaseConfig(environmentName)
stackParams["DatabaseName"] = dbConfig.Name
stackParams["Namespace"] = namespace
stackParams["EnvironmentName"] = environmentName
common.NewMapElementIfNotEmpty(stackParams, "DatabaseEngine", dbConfig.Engine)
common.NewMapElementIfNotEmpty(stackParams, "DatabaseEngineMode", dbConfig.EngineMode)
common.NewMapElementIfNotEmpty(stackParams, "DatabaseInstanceClass", dbConfig.InstanceClass)
common.NewMapElementIfNotEmpty(stackParams, "DatabaseStorage", dbConfig.AllocatedStorage)
common.NewMapElementIfNotEmpty(stackParams, "ScalingMinCapacity", dbConfig.MinSize)
common.NewMapElementIfNotEmpty(stackParams, "ScalingMaxCapacity", dbConfig.MaxSize)
common.NewMapElementIfNotEmpty(stackParams, "SecondsUntilAutoPause", dbConfig.SecondsUntilAutoPause)
stackParams["DatabaseMasterUsername"] = "admin"
common.NewMapElementIfNotEmpty(stackParams, "DatabaseMasterUsername", dbConfig.MasterUsername)
stackParams["DatabaseKeyArn"] = workflow.databaseKeyArn
tags := createTagMap(&DatabaseTags{
Environment: environmentName,
Type: common.StackTypeDatabase,
Service: workflow.serviceName,
Revision: workflow.codeRevision,
Repo: workflow.repoName,
})
policy, err := templates.GetAsset(common.TemplatePolicyDefault)
if err != nil {
return err
}
err = stackUpserter.UpsertStack(dbStackName, common.TemplateDatabase, service, stackParams, tags, policy, workflow.cloudFormationRoleArn)
if err != nil {
return err
}
log.Debugf("Waiting for stack '%s' to complete", dbStackName)
stack := stackWaiter.AwaitFinalStatus(dbStackName)
if stack == nil {
return fmt.Errorf("Unable to create stack %s", dbStackName)
}
if strings.HasSuffix(stack.Status, "ROLLBACK_COMPLETE") || !strings.HasSuffix(stack.Status, "_COMPLETE") {
return fmt.Errorf("Ended in failed status %s %s", stack.Status, stack.StatusReason)
}
// update IAM Authentication
if stack.Outputs["DatabaseIdentifier"] != "" && dbConfig.EngineMode != "serverless" {
enableIamAuthentication, _ := strconv.ParseBool(dbConfig.IamAuthentication)
return rdsSetter.SetIamAuthentication(stack.Outputs["DatabaseIdentifier"], enableIamAuthentication, dbConfig.Engine)
}
return nil
}
}
func randomPassword(length int) string {
availableCharBytes := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
// Compute bitMask
availableCharLength := len(availableCharBytes)
if availableCharLength == 0 || availableCharLength > 256 {
panic("availableCharBytes length must be greater than 0 and less than or equal to 256")
}
var bitLength byte
var bitMask byte
for bits := availableCharLength - 1; bits != 0; {
bits = bits >> 1
bitLength++
}
bitMask = 1<<bitLength - 1
// Compute bufferSize
bufferSize := length + length/3
// Create random string
result := make([]byte, length)
for i, j, randomBytes := 0, 0, []byte{}; i < length; j++ {
if j%bufferSize == 0 {
randomBytes = make([]byte, length)
_, err := rand.Read(randomBytes)
if err != nil {
log.Fatal("Unable to generate random bytes")
}
}
// Mask bytes to get an index into the character slice
if idx := int(randomBytes[j%length] & bitMask); idx < availableCharLength {
result[i] = availableCharBytes[idx]
i++
}
}
return string(result)
}