forked from dan-v/rattlesnakeos-stack
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathdeploy.go
337 lines (289 loc) · 14 KB
/
deploy.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
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
package cmd
import (
"context"
"errors"
"fmt"
"github.com/dan-v/rattlesnakeos-stack/internal/cloudaws"
"github.com/dan-v/rattlesnakeos-stack/internal/stack"
"github.com/dan-v/rattlesnakeos-stack/internal/templates"
"github.com/dan-v/rattlesnakeos-stack/internal/terraform"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/manifoldco/promptui"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
yaml "gopkg.in/yaml.v2"
)
const (
minimumChromiumVersion = 86
)
var (
name, region, email, device, sshKey, maxPrice, skipPrice, schedule, cloud string
instanceType, instanceRegions, chromiumVersion, releasesURL string
saveConfig, dryRun, chromiumBuildDisabled bool
coreConfigRepo, customConfigRepo string
coreConfigRepoBranch, customConfigRepoBranch string
outputDir string
instanceDebugDelayTermination bool
// TODO: apv workaround - remove once alternative is built
apvRemote, apvBranch, apvRevision string
)
func deployInit() {
rootCmd.AddCommand(deployCmd)
flags := deployCmd.Flags()
flags.StringVarP(&name, "name", "n", "",
"name for stack. note: this must be a valid/unique S3 bucket name.")
_ = viper.BindPFlag("name", flags.Lookup("name"))
flags.StringVarP(®ion, "region", "r", "",
"aws region for stack deployment (e.g. us-west-2)")
_ = viper.BindPFlag("region", flags.Lookup("region"))
flags.StringVarP(&device, "device", "d", "",
"device you want to build for (e.g. crosshatch)")
_ = viper.BindPFlag("device", flags.Lookup("device"))
flags.StringVarP(&email, "email", "e", "",
"email address you want to use for build notifications")
_ = viper.BindPFlag("email", flags.Lookup("email"))
flags.StringVar(&sshKey, "ssh-key", "",
"aws ssh key to add to ec2 spot instances. this is optional but is useful for debugging build issues on the instance.")
_ = viper.BindPFlag("ssh-key", flags.Lookup("ssh-key"))
flags.StringVar(&skipPrice, "skip-price", "0.68",
"skip requesting ec2 spot instance if price is above this value to begin with.")
_ = viper.BindPFlag("skip-price", flags.Lookup("skip-price"))
flags.StringVar(&maxPrice, "max-price", "1.00",
"max ec2 spot instance price. if this value is too low, you may not obtain an instance or it may terminate during a build.")
_ = viper.BindPFlag("max-price", flags.Lookup("max-price"))
flags.StringVar(&instanceType, "instance-type", "c5.4xlarge", "EC2 instance type (e.g. c5.4xlarge) to use for the build.")
_ = viper.BindPFlag("instance-type", flags.Lookup("instance-type"))
flags.StringVar(&instanceRegions, "instance-regions", cloudaws.DefaultInstanceRegions,
"possible regions to launch spot instance. the region with cheapest spot instance price will be used.")
_ = viper.BindPFlag("instance-regions", flags.Lookup("instance-regions"))
flags.StringVar(&schedule, "schedule", "cron(0 0 10 * ? *)",
"cron expression that defines when to kick off builds. by default this is set to build on the 10th of every month. you can also set to empty string to disable cron."+
"note: if you give an invalid expression it will fail to deploy the stack. "+
"see this for cron format details: https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html#CronExpressions")
_ = viper.BindPFlag("schedule", flags.Lookup("schedule"))
flags.BoolVar(&chromiumBuildDisabled, "chromium-build-disabled", false, "control whether chromium builds are enabled or disabled.")
_ = viper.BindPFlag("chromium-build-disabled", flags.Lookup("chromium-build-disabled"))
flags.StringVar(&chromiumVersion, "chromium-version", "",
"specify the version of Chromium you want (e.g. 80.0.3971.4) to pin to. if not specified, the latest stable version of Chromium is used.")
_ = viper.BindPFlag("chromium-version", flags.Lookup("chromium-version"))
flags.StringVar(&coreConfigRepo, "core-config-repo", templates.DefaultCoreConfigRepo, "a specially formatted repo that contains core customizations on top of AOSP.")
_ = viper.BindPFlag("core-config-repo", flags.Lookup("core-config-repo"))
flags.StringVar(&coreConfigRepoBranch, "core-config-repo-branch", aospVersion, "the branch to use for the core config repo.")
_ = viper.BindPFlag("core-config-repo-branch", flags.Lookup("core-config-repo-branch"))
flags.StringVar(&customConfigRepo, "custom-config-repo", "", "a specially formatted repo that contains customizations on top of core.")
_ = viper.BindPFlag("custom-config-repo", flags.Lookup("custom-config-repo"))
flags.StringVar(&customConfigRepoBranch, "custom-config-repo-branch", "", "the branch to use for the custom config repo. if left blanked the default branch will be checked out.")
_ = viper.BindPFlag("custom-config-repo-branch", flags.Lookup("custom-config-repo-branch"))
flags.StringVar(&releasesURL, "releases-url", fmt.Sprintf(templates.DefaultReleasesURLTemplate, aospVersion), "url that is used to check versions of aosp/chromium and whether build is required.")
_ = viper.BindPFlag("releases-url", flags.Lookup("releases-url"))
flags.StringVar(&cloud, "cloud", "aws", "cloud (aws only right now)")
_ = viper.BindPFlag("cloud", flags.Lookup("cloud"))
flags.StringVar(&outputDir, "output-dir", "", "where to generate all files used for the deployment")
_ = viper.BindPFlag("output-dir", flags.Lookup("output-dir"))
flags.BoolVar(&saveConfig, "save-config", false, "allows you to save all passed CLI flags to config file")
flags.BoolVar(&dryRun, "dry-run", false, "only generate the output files, but do not deploy with terraform.")
flags.BoolVar(&instanceDebugDelayTermination, "instance-debug-delay-termination", false, "delay instance shutdown/termination if there are active SSH sessions")
_ = viper.BindPFlag("instance-debug-delay-termination", flags.Lookup("instance-debug-delay-termination"))
// TODO: apv workaround - remove once alternative is built
flags.StringVar(&apvRemote, "apv-remote", "", "remote that contains android-prepare-vendor repo (e.g. https://github.com/example/)")
_ = viper.BindPFlag("apv-remote", flags.Lookup("apv-remote"))
flags.StringVar(&apvBranch, "apv-branch", "", "the branch to use for android-prepare-vendor repo (e.g. 12)")
_ = viper.BindPFlag("apv-branch", flags.Lookup("apv-branch"))
flags.StringVar(&apvRevision, "apv-revision", "", "the git revision to use for android-prepare-vendor repo.")
_ = viper.BindPFlag("apv-revision", flags.Lookup("apv-revision"))
}
var deployCmd = &cobra.Command{
Use: "deploy",
Short: "deploy or update the cloud infrastructure used for OS building",
Args: func(cmd *cobra.Command, args []string) error {
if viper.GetString("name") == "" {
return fmt.Errorf("must provide a stack name")
}
if viper.GetString("region") == "" {
return fmt.Errorf("must provide a region")
}
if viper.GetString("email") == "" {
return errors.New("must specify email")
}
if viper.GetString("ssh-key") == "" {
return fmt.Errorf("must provide ssh key name")
}
if viper.GetString("device") == "" {
return errors.New("must specify device type")
}
if viper.GetString("chromium-version") != "" {
chromiumVersionSplit := strings.Split(viper.GetString("chromium-version"), ".")
if len(chromiumVersionSplit) != 4 {
return errors.New("invalid chromium-version specified")
}
chromiumMajorNumber, err := strconv.Atoi(chromiumVersionSplit[0])
if err != nil {
return fmt.Errorf("unable to parse specified chromium-version: %v", err)
}
if chromiumMajorNumber < minimumChromiumVersion {
return fmt.Errorf("pinned chromium-version must have major version of at least %v", minimumChromiumVersion)
}
}
if !supportedDevices.IsSupportedDevice(viper.GetString("device")) {
return fmt.Errorf("must specify a supported device: %v", strings.Join(supportedDevices.GetDeviceCodeNames(), ", "))
}
// TODO: apv workaround - remove once alternative is built
if viper.Get("apv-remote") == "" {
return fmt.Errorf("TEMPORARY: need to specify apv-remote in config (e.g. https://github.com/example/)")
}
if viper.Get("apv-branch") == "" {
return fmt.Errorf("TEMPORARY: need to specify apv-branch in config (e.g. 12)")
}
if viper.Get("apv-revision") == "" {
return fmt.Errorf("TEMPORARY: need to specify apv-revision in config (e.g. f1d2d2f924e986ac86fdf7b36c94bcdf32beec15)")
}
// deprecated checks
if viper.GetBool("encrypted-keys") {
return fmt.Errorf("encrypted-keys functionality has been removed (it may return in the future). migration required to use non encrypted keys for now")
}
if viper.GetString("core-config-repo-branch") != aospVersion {
log.Warnf("core-config-repo-branch '%v' does not match aosp version '%v' - if this is not intended, update your config file",
viper.GetString("core-config-repo-branch"), aospVersion)
}
if viper.GetString("hosts-file") != "" {
log.Warn("hosts-file functionality has been removed - it can be removed from config file")
}
if viper.Get("custom-manifest-remotes") != nil {
return fmt.Errorf("custom-manifest-remotes has been deprecated in favor of custom-config-repo option")
}
if viper.Get("custom-manifest-projects") != nil {
return fmt.Errorf("custom-manifest-projects has been deprecated in favor of custom-config-repo option")
}
if viper.Get("custom-patches") != nil {
return fmt.Errorf("custom-patches has been deprecated in favor of custom-config-repo option")
}
if viper.Get("custom-prebuilts") != nil {
return fmt.Errorf("custom-prebuilts has been deprecated in favor of custom-config-repo option")
}
return nil
},
Run: func(cmd *cobra.Command, args []string) {
configFileFullPath, err := filepath.Abs(cfgFile)
if err != nil {
log.Fatal(err)
}
c := viper.AllSettings()
bs, err := yaml.Marshal(c)
if err != nil {
log.Fatalf("unable to marshal config to YAML: %v", err)
}
log.Println("Current settings:")
fmt.Println(string(bs))
if !dryRun {
prompt := promptui.Prompt{
Label: "Do you want to continue ",
IsConfirm: true,
}
_, err = prompt.Run()
if err != nil {
log.Fatalf("exiting: %v", err)
}
}
configuredOutputDir, err := getOutputDir()
if err != nil {
log.Fatal(err)
}
log.Infof("all generated files will be placed in %v", configuredOutputDir)
templateConfig := &templates.Config{
Version: stackVersion,
Name: viper.GetString("name"),
Region: viper.GetString("region"),
Device: viper.GetString("device"),
DeviceDetails: supportedDevices.GetDeviceDetails(viper.GetString("device")),
Email: viper.GetString("email"),
InstanceType: viper.GetString("instance-type"),
InstanceRegions: viper.GetString("instance-regions"),
SkipPrice: viper.GetString("skip-price"),
MaxPrice: viper.GetString("max-price"),
SSHKey: viper.GetString("ssh-key"),
Schedule: viper.GetString("schedule"),
ChromiumBuildDisabled: viper.GetBool("chromium-build-disabled"),
ChromiumVersion: viper.GetString("chromium-version"),
CoreConfigRepo: viper.GetString("core-config-repo"),
CoreConfigRepoBranch: viper.GetString("core-config-repo-branch"),
CustomConfigRepo: viper.GetString("custom-config-repo"),
CustomConfigRepoBranch: viper.GetString("custom-config-repo-branch"),
ReleasesURL: viper.GetString("releases-url"),
Cloud: viper.GetString("cloud"),
InstanceDebugDelayTermination: viper.GetBool("instance-debug-delay-termination"),
ApvRemote: viper.GetString("apv-remote"),
ApvBranch: viper.GetString("apv-branch"),
ApvRevision: viper.GetString("apv-revision"),
}
templateRenderer, err := templates.New(templateConfig, templatesFiles, configuredOutputDir)
if err != nil {
log.Fatalf("failed to create template client: %v", err)
}
if saveConfig {
log.Printf("Saved settings to config file %v.", configFileFullPath)
err := viper.WriteConfigAs(configFileFullPath)
if err != nil {
log.Fatalf("Failed to write config file %v", configFileFullPath)
}
}
if dryRun {
log.Infof("rendering all templates to '%v'", configuredOutputDir)
err = templateRenderer.RenderAll()
if err != nil {
log.Fatal(err)
}
log.Info("skipping deployment as skip deploy option was specified")
return
}
if viper.GetString("cloud") != "aws" {
log.Fatal("'aws' is only supported option for cloud at the moment")
}
awsSetupClient, err := cloudaws.NewSetupClient(
viper.GetString("name"),
viper.GetString("region"),
configFileFullPath,
)
if err != nil {
log.Fatalf("failed to create aws setup client: %v", err)
}
awsSubscribeClient, err := cloudaws.NewSubscribeClient(
viper.GetString("name"),
viper.GetString("region"),
viper.GetString("email"),
)
if err != nil {
log.Fatalf("failed to create aws subscribe client: %v", err)
}
terraformClient, err := terraform.New(configuredOutputDir)
if err != nil {
log.Fatalf("failed to create terraform client: %v", err)
}
s := stack.New(viper.GetString("name"), templateRenderer, awsSetupClient, awsSubscribeClient, terraformClient)
if err != nil {
log.Fatal(err)
}
ctx, cancel := context.WithTimeout(context.Background(), stack.DefaultDeployTimeout)
defer cancel()
if err := s.Deploy(ctx); err != nil {
log.Fatal(err)
}
},
}
func getOutputDir() (string, error) {
configuredOutputDir := viper.GetString("output-dir")
if configuredOutputDir == "" {
configuredOutputDir = fmt.Sprintf("output_%v", viper.GetString("name"))
}
configuredOutputDir, err := filepath.Abs(configuredOutputDir)
if err != nil {
return "", err
}
if err := os.MkdirAll(configuredOutputDir, os.ModePerm); err != nil {
log.Fatal(err)
}
return configuredOutputDir, nil
}