-
Notifications
You must be signed in to change notification settings - Fork 279
Expand file tree
/
Copy pathdeploy_gen2.dart
More file actions
659 lines (590 loc) · 19 KB
/
deploy_gen2.dart
File metadata and controls
659 lines (590 loc) · 19 KB
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
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
#!/usr/bin/env dart
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
// ignore_for_file: avoid_print
import 'dart:convert';
import 'dart:io';
import 'package:amplify_core/amplify_core.dart' show UUID;
import 'package:args/args.dart';
import 'package:path/path.dart' as p;
/// This is the source of truth for the infra-gen2 backends
///
/// To add a backend:
/// 1. Create the backend in the infra-gen2/backends directory
/// 2. Run `npm create amplify@latest -y` in the backend directory to create the backend
/// 3. Add the backend to a category or create a new category
/// 4. Run `dart tool/deploy_gen2.dart` to deploy the backend
const List<AmplifyBackendGroup> infraConfig = [
AmplifyBackendGroup(
category: 'API',
defaultOutput: 'packages/api/amplify_api/example/lib',
backends: [
AmplifyBackend(
name: 'apiMultiAuth',
identifier: 'apiMultiAuth',
pathToSource: 'infra-gen2/backends/api/api-multi-auth',
),
],
),
AmplifyBackendGroup(
category: 'Auth',
defaultOutput: 'packages/auth/amplify_auth_cognito/example/lib',
sharedOutputs: [
'packages/auth/amplify_auth_cognito_dart/example/lib',
'packages/authenticator/amplify_authenticator/example/lib',
],
backends: [
AmplifyBackend(
name: 'email-sign-in',
identifier: 'email-sign-in',
pathToSource: 'infra-gen2/backends/auth/email-sign-in',
),
AmplifyBackend(
name: 'phone-sign-in',
identifier: 'phone-sign-in',
pathToSource: 'infra-gen2/backends/auth/phone-sign-in',
),
AmplifyBackend(
name: 'mfa-optional-sms',
identifier: 'mfa-opt-sms',
pathToSource: 'infra-gen2/backends/auth/mfa-optional-sms',
),
AmplifyBackend(
name: 'mfa-required-sms',
identifier: 'mfa-req-sms',
pathToSource: 'infra-gen2/backends/auth/mfa-required-sms',
),
AmplifyBackend(
name: 'mfa-required-email',
identifier: 'mfa-req-email',
pathToSource: 'infra-gen2/backends/auth/mfa-required-email',
),
AmplifyBackend(
name: 'mfa-required-email-sms',
identifier: 'mfa-req-ema-sms',
pathToSource: 'infra-gen2/backends/auth/mfa-required-email-sms',
),
AmplifyBackend(
name: 'mfa-optional-email',
identifier: 'mfa-opt-email',
pathToSource: 'infra-gen2/backends/auth/mfa-optional-email',
),
AmplifyBackend(
name: 'mfa-optional-email-sms',
identifier: 'mfa-opt-ema-sms',
pathToSource: 'infra-gen2/backends/auth/mfa-optional-email-sms',
),
AmplifyBackend(
name: 'mfa-required-email-totp',
identifier: 'mfa-req-ema-tot',
pathToSource: 'infra-gen2/backends/auth/mfa-required-email-totp',
),
AmplifyBackend(
name: 'mfa-optional-email-totp',
identifier: 'mfa-opt-ema-tot',
pathToSource: 'infra-gen2/backends/auth/mfa-optional-email-totp',
),
AmplifyBackend(
name: 'username-login-mfa',
identifier: 'user-login-mfa',
pathToSource: 'infra-gen2/backends/auth/username-login-mfa',
),
],
),
AmplifyBackendGroup(
category: 'Storage',
defaultOutput: 'packages/storage/amplify_storage_s3/example/lib',
backends: [
AmplifyBackend(
name: 'main',
identifier: 'main',
pathToSource: 'infra-gen2/backends/storage/main',
),
AmplifyBackend(
name: 'dots-in-name',
identifier: 'dots-in-name',
pathToSource: 'infra-gen2/backends/storage/dots-in-name',
),
],
),
AmplifyBackendGroup(
category: 'Analytics',
defaultOutput: 'packages/analytics/amplify_analytics_pinpoint/example/lib',
backends: [
AmplifyBackend(
name: 'main',
identifier: 'main',
pathToSource: 'infra-gen2/backends/analytics/main',
),
AmplifyBackend(
name: 'no-unauth-access',
identifier: 'no-unauth-acc',
pathToSource: 'infra-gen2/backends/analytics/no-unauth-access',
),
AmplifyBackend(
name: 'no-unauth-identities',
identifier: 'no-unauth-id',
pathToSource: 'infra-gen2/backends/analytics/no-unauth-identities',
),
],
),
AmplifyBackendGroup(
category: 'Kinesis',
defaultOutput: 'packages/kinesis/amplify_kinesis_dart/lib',
backends: [
AmplifyBackend(
name: 'main',
identifier: 'main',
pathToSource: 'infra-gen2/backends/kinesis/main',
),
],
),
];
const pathToBackends = 'infra-gen2/backends';
void main(List<String> arguments) async {
final args = _parseArgs(arguments);
final verbose = args.flag('verbose');
final categoryToDeploy = args['category'];
final bucketNames = <String>[];
print('🏃 Running build for infra-gen2');
await _buildProject();
print('🚀 Deploying Gen 2 backends!');
for (final backendGroup in infraConfig) {
if (categoryToDeploy != null && backendGroup.category != categoryToDeploy) {
continue;
}
// TODO(equartey): Could be removed when all backends are defined.
if (backendGroup.backends.isEmpty) {
continue;
}
var gen2Environments = <String, String>{};
var gen1Environments = <String, String>{};
final categoryName = backendGroup.category;
final outputPath = p.join(repoRoot.path, backendGroup.defaultOutput);
final amplifyOutputs = File(p.join(outputPath, 'amplify_outputs.dart'));
final amplifyConfiguration = File(
p.join(outputPath, 'amplifyconfiguration.dart'),
);
// create the output file if it does not exist
if (!amplifyOutputs.existsSync()) {
amplifyOutputs.createSync(recursive: true);
}
if (!amplifyConfiguration.existsSync()) {
amplifyConfiguration.createSync(recursive: true);
}
print('🏃 Running sandbox deployment for $categoryName');
for (final backend in backendGroup.backends) {
final backendName = backend.name;
final stackID = await _deployBackend(
backendGroup.category,
backend,
amplifyOutputs.path.replaceFirst('amplify_outputs.dart', ''),
verbose,
);
_generateGen1Config(
backendGroup.category,
backend,
amplifyConfiguration.path.replaceFirst('amplifyconfiguration.dart', ''),
stackID,
);
// Skip if there is only one backend
if (backendGroup.backends.length <= 1) {
continue;
}
// Cache the config contents to create environments map
gen2Environments = {
...gen2Environments,
..._cacheConfigContents(backendName, amplifyOutputs),
};
gen1Environments = {
...gen1Environments,
..._cacheConfigContents(backendName, amplifyConfiguration),
};
}
// Only append environments if there are multiple backends
if (backendGroup.backends.length > 1) {
_appendEnvironments(gen2Environments, backendGroup, amplifyOutputs);
_appendEnvironments(gen1Environments, backendGroup, amplifyConfiguration);
}
// Copy config files to shared paths
_copyConfigFile(backendGroup.sharedOutputs, [
amplifyOutputs,
amplifyConfiguration,
]);
// Check if the S3 bucket exists
var bucketName = _createBucketName(categoryName);
final remoteBucketName = _getS3BucketName(bucketName);
if (remoteBucketName != null && remoteBucketName.isNotEmpty) {
bucketName = remoteBucketName;
print('🔍 Using existing S3 bucket $bucketName');
} else {
_createS3Bucket(bucketName);
}
bucketNames.add(bucketName);
// Upload config files to S3 bucket
_uploadConfigFileToS3(bucketName, [amplifyOutputs, amplifyConfiguration]);
print('✅ Deployment for $categoryName Category complete');
}
print('🎉 All backends deployed successfully!');
print('🪣 S3 Bucket Names: $bucketNames');
}
Future<Process> _buildProject() async {
return Process.start('npm', ['run', 'build']);
}
ArgResults _parseArgs(List<String> args) {
final parser = ArgParser()
..addFlag(
'verbose',
abbr: 'v',
help: 'Run command in verbose mode',
defaultsTo: false,
)
..addOption(
'category',
abbr: 'c',
help: 'Specify the category to deploy.',
allowed: infraConfig.map((e) => e.category).toList(),
defaultsTo: null,
);
return parser.parse(args);
}
/// Deploy Sandbox for a given backend backend
Future<String> _deployBackend(
String category,
AmplifyBackend backend,
String outputPath,
bool verbose,
) async {
print('🏖️ Deploying $category ${backend.name}, this may take a while...');
final outputFile = File(p.join(outputPath, 'amplify_outputs.dart'));
if (outputFile.existsSync()) {
outputFile.deleteSync();
}
// Deploy the backend
final process = await Process.start('npx', [
'ampx',
'sandbox',
'--outputs-format',
'dart',
'--outputs-out-dir',
outputPath,
'--identifier',
backend.identifier,
'--profile=${Platform.environment['AWS_PROFILE'] ?? 'default'}',
'--once',
'--debug',
], workingDirectory: p.join(repoRoot.path, backend.pathToSource));
if (verbose) {
process.stderr.transform(const SystemEncoding().decoder).listen((data) {
print('❌ Error: $data');
});
}
var stackID = '';
// About deployment errors
var postedDeploymentError = false;
var postedDeploymentErrorReason = false;
var postedDeploymentUpdateFailed = false;
// About CDK build errors
var postingBackendBuildError = false;
var postedBackendBuildError = false;
var previousLine = '';
// Listen to stdout for stack ID
await for (final String line
in process.stdout
.transform(utf8.decoder)
.transform(const LineSplitter())) {
if (!line.startsWith(' at ') && line.trim().isNotEmpty) {
// This line does not belong to a cdk build error
postingBackendBuildError = false;
}
if (verbose) {
print(line);
} else if (line.contains(' [ERROR]')) {
print('❌ $line');
postedDeploymentError = true;
} else if (line.contains(' ∟ Caused by: ')) {
print(' $line');
postedDeploymentErrorReason = true;
} else if (line.contains(' | UPDATE_FAILED | ')) {
postedDeploymentUpdateFailed = true;
} else if (!postingBackendBuildError && line.startsWith(' at ')) {
// This line and the previous line log a cdk build error
print('❌ $previousLine');
print(' $line');
postedBackendBuildError = true;
postingBackendBuildError = true;
} else if (line.startsWith(' at ')) {
// This line logs a cdk build error
print(' $line');
}
// Save Stack ID
if (line.contains('Stack:')) {
stackID = line.split('Stack:').last.trim();
}
// Needed for printing cdk build errors
previousLine = line;
}
final exitCode = await process.exitCode;
if (exitCode != 0) {
throw Exception(
'❌ Error deploying $category ${backend.identifier} sandbox',
);
} else if (postedDeploymentError &&
postedDeploymentErrorReason &&
postedDeploymentUpdateFailed) {
throw Exception(
'❌ Error deploying $category ${backend.identifier} sandbox: Update failed',
);
} else if (postedBackendBuildError) {
throw Exception(
'❌ Error deploying $category ${backend.identifier} sandbox - CDK build failed',
);
} else if (!outputFile.existsSync()) {
throw Exception(
'❌ Error deploying $category ${backend.identifier} sandbox - Output file not generated',
);
} else {
print('👍 $category ${backend.identifier} sandbox deployed');
return stackID;
}
}
/// Cache the config contents to create environments map
Map<String, String> _cacheConfigContents(
String backendName,
File amplifyOutputs,
) {
// Ensure the amplify_outputs.dart file exists
if (!amplifyOutputs.existsSync()) {
throw Exception(
'❌ Error: ${amplifyOutputs.path} does not exist. '
'Please check the deployment logs for more information.',
);
}
// cache the config file contents to create environments map
final rawConfigContent = amplifyOutputs.readAsStringSync();
final exp = RegExp(r"'''(.*?)'''", dotAll: true);
final configMap = exp.firstMatch(rawConfigContent)?.group(1) ?? '';
return {backendName: 'r\'\'\'$configMap\'\'\''};
}
/// Append the environments to amplify_outputs.dart
void _appendEnvironments(
Map<String, String> amplifyEnvironments,
AmplifyBackendGroup category,
File amplifyOutputs,
) {
// Skip if there are no amplifyEnvironments
if (amplifyEnvironments.isEmpty) {
return;
}
print('👷 Building amplifyEnvironments');
var amplifyEnvironmentsOutput =
'\nconst amplifyEnvironments = <String, String>{\n';
amplifyEnvironments.forEach((key, value) {
amplifyEnvironmentsOutput += "'$key': $value,\n";
});
amplifyEnvironmentsOutput += '};';
// Append amplifyEnvironments to amplify_outputs.dart
amplifyOutputs.writeAsStringSync(
amplifyEnvironmentsOutput,
mode: FileMode.append,
);
}
/// Copy a given config file to a list of shared paths
void _copyConfigFile(List<String> outputPaths, List<File> configFiles) {
if (outputPaths.length <= 1) {
return;
}
for (final configFile in configFiles) {
final fileName = configFile.path.split('/').last;
print('👯 Copying $fileName to other shared paths');
for (final outputPath in outputPaths) {
final destination = p.join(repoRoot.path, outputPath);
final outputFile = File(p.join(destination, fileName));
if (!outputFile.existsSync()) {
outputFile.createSync(recursive: true);
}
final amplifyOutputsContents = configFile.readAsStringSync();
outputFile.writeAsStringSync(amplifyOutputsContents);
}
}
}
/// Create a unique bucket name
String _createBucketName(String base) {
final uniqueShort = UUID.getUUID().substring(0, 8);
return '${base.toLowerCase()}-gen2-integ-$uniqueShort';
}
String? _getS3BucketName(String bucketName) {
final checkBucket = Process.runSync(
'aws',
[
'--profile=${Platform.environment['AWS_PROFILE'] ?? 'default'}',
's3api',
'list-buckets',
'--query',
'Buckets[].Name',
'--output',
'text',
],
stdoutEncoding: utf8,
stderrEncoding: utf8,
);
if (checkBucket.exitCode != 0) {
throw Exception(
'❌ Error checking if S3 bucket $bucketName exists: '
'${checkBucket.stdout}\n${checkBucket.stderr}',
);
}
final output = checkBucket.stdout as String;
// Determine if bucket exists while ignoring the UUID
final bucketNameWithoutUUID = bucketName.substring(0, bucketName.length - 8);
const uuidMatcher = r'[a-f0-9]{8}$';
final pattern = '($bucketNameWithoutUUID)$uuidMatcher';
final bucketNames = output.split('\t').map((e) => e.trim()).toList();
final regex = RegExp(pattern);
final matchingBuckets = bucketNames.where(regex.hasMatch);
if (matchingBuckets.length > 1) {
throw Exception(
'❌ Error: Multiple buckets found with the same name: $matchingBuckets',
);
}
if (matchingBuckets.isEmpty) {
return null;
}
return matchingBuckets.single;
}
/// Create an S3 bucket
void _createS3Bucket(String bucketName) {
print('🪣 Creating S3 bucket: $bucketName');
final createBucket = Process.runSync(
'aws',
[
'--profile=${Platform.environment['AWS_PROFILE'] ?? 'default'}',
's3',
'mb',
's3://$bucketName',
],
stdoutEncoding: utf8,
stderrEncoding: utf8,
);
if (createBucket.exitCode != 0) {
throw Exception(
'❌ Error creating S3 bucket $bucketName: '
'${createBucket.stdout}\n${createBucket.stderr}',
);
}
print('👍 S3 bucket $bucketName successfully created');
}
/// Upload the amplify_outputs.dart file to the S3 bucket
void _uploadConfigFileToS3(String bucketName, List<File> configFiles) {
for (final configFile in configFiles) {
final fileName = configFile.path.split('/').last;
print('📲 Uploading $fileName to S3 bucket');
final downloadRes = Process.runSync(
'aws',
[
'--profile=${Platform.environment['AWS_PROFILE'] ?? 'default'}',
's3',
'cp',
configFile.path,
's3://$bucketName/$fileName',
],
stdoutEncoding: utf8,
stderrEncoding: utf8,
);
if (downloadRes.exitCode != 0) {
throw Exception(
'❌ Error downloading $bucketName config from S3: '
'${downloadRes.stdout}\n${downloadRes.stderr}',
);
}
print('👍 $fileName successfully uploaded to S3 bucket');
}
}
/// Generates gen 1 amplifyconfiguration.dart file
void _generateGen1Config(
String category,
AmplifyBackend backend,
String outputPath,
String stack,
) {
print('📁 Generating gen 1 config file for $category ${backend.name}...');
final outputFile = File(p.join(outputPath, 'amplifyconfiguration.dart'));
if (outputFile.existsSync()) {
outputFile.deleteSync();
}
// Deploy the backend
final process = Process.runSync('npx', [
'ampx',
'generate',
'outputs',
'--format',
'dart',
'--outputs-version',
'0',
'--out-dir',
outputPath,
'--profile=${Platform.environment['AWS_PROFILE'] ?? 'default'}',
'--stack',
stack,
'--debug',
'true',
], workingDirectory: p.join(repoRoot.path, backend.pathToSource));
if (process.exitCode != 0) {
throw Exception(
'❌ Error generating gen 1 config file for $category ${backend.name}:: ${process.stdout}',
);
} else if (!outputFile.existsSync()) {
throw Exception(
'❌ Error generating gen 1 config file for $category ${backend.identifier} - Output file not generated',
);
} else {
print('👍 Gen 1 config file for $category ${backend.name} generated');
}
}
class AmplifyBackendGroup {
const AmplifyBackendGroup({
required this.category,
required this.backends,
required this.defaultOutput,
this.sharedOutputs = const [],
});
/// This is the category of the integration group
final String category;
/// This is the list of backends for the group
final List<AmplifyBackend> backends;
/// This is the default output path for the group
final String defaultOutput;
/// This is the list of shared output paths for the group
final List<String> sharedOutputs;
}
class AmplifyBackend {
const AmplifyBackend({
required this.name,
required this.identifier,
required this.pathToSource,
});
/// This is the name of the backend
final String name;
/// This is the identifier for the backend
/// It is the name in "PascalCase"
/// Must be less than 15 characters
final String identifier;
/// This is the path to the source code for the backend
/// Root directory is `amplify-flutter/`
final String pathToSource;
}
final Directory repoRoot = () {
var dir = Directory.current;
Directory? rootDir;
while (p.absolute(dir.parent.path) != p.absolute(dir.path)) {
final files = dir.listSync().whereType<File>();
if (files.any((f) => p.basename(f.path) == 'pubspec.yaml')) {
rootDir = dir;
}
dir = dir.parent;
}
if (rootDir == null) {
throw StateError('Could not locate repo root');
}
return rootDir;
}();