hereya-cli 0.9.2 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -42
- package/dist/backend/common.d.ts +24 -4
- package/dist/backend/local.d.ts +3 -1
- package/dist/backend/local.js +112 -18
- package/dist/commands/workspace/env/set/index.d.ts +13 -0
- package/dist/commands/workspace/env/set/index.js +39 -0
- package/dist/commands/workspace/env/unset/index.d.ts +10 -0
- package/dist/commands/workspace/env/unset/index.js +33 -0
- package/dist/iac/common.d.ts +5 -0
- package/dist/iac/terraform.js +62 -19
- package/dist/infrastructure/aws.d.ts +5 -6
- package/dist/infrastructure/aws.js +114 -244
- package/dist/infrastructure/common.d.ts +24 -0
- package/dist/infrastructure/local.d.ts +3 -3
- package/dist/infrastructure/local.js +12 -28
- package/dist/lib/filesystem.d.ts +1 -0
- package/dist/lib/filesystem.js +10 -1
- package/dist/lib/package/index.d.ts +1 -0
- package/dist/lib/package/index.js +26 -8
- package/oclif.manifest.json +147 -82
- package/package.json +1 -1
- package/dist/commands/remote/exec/index.d.ts +0 -13
- package/dist/commands/remote/exec/index.js +0 -89
package/dist/iac/terraform.js
CHANGED
|
@@ -8,34 +8,63 @@ import { mapObject } from '../lib/object-utils.js';
|
|
|
8
8
|
import { runShell } from '../lib/shell.js';
|
|
9
9
|
export class Terraform {
|
|
10
10
|
async apply(input) {
|
|
11
|
+
if (input.infraConfig &&
|
|
12
|
+
input.infraConfig.terraformStateBucketName &&
|
|
13
|
+
input.infraConfig.terraformStateLockTableName &&
|
|
14
|
+
input.infraConfig.terraformStateBucketRegion) {
|
|
15
|
+
const backendConfig = `
|
|
16
|
+
terraform {
|
|
17
|
+
backend "s3" {
|
|
18
|
+
bucket = "${input.infraConfig.terraformStateBucketName}"
|
|
19
|
+
key = "${input.id}/terraform.tfstate"
|
|
20
|
+
region = "${input.infraConfig.terraformStateBucketRegion}"
|
|
21
|
+
dynamodb_table = "${input.infraConfig.terraformStateLockTableName}"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
`;
|
|
25
|
+
const backendFile = path.join(input.pkgPath, 'hereya_terraform_backend.tf');
|
|
26
|
+
await fs.promises.writeFile(backendFile, backendConfig);
|
|
27
|
+
}
|
|
11
28
|
try {
|
|
12
29
|
const terraform = await this.getTerraformBinary();
|
|
13
30
|
runShell(terraform, ['init'], {
|
|
14
31
|
directory: input.pkgPath,
|
|
15
32
|
env: {
|
|
16
|
-
...mapObject(input.env ?? {}, (key, value) => [
|
|
33
|
+
...mapObject(input.env ?? {}, (key, value) => [
|
|
34
|
+
`TF_VAR_${key}`,
|
|
35
|
+
typeof value === 'object' ? JSON.stringify(value) : value,
|
|
36
|
+
]),
|
|
17
37
|
...input.env,
|
|
18
|
-
...mapObject(input.parameters ?? {}, (key, value) => [
|
|
19
|
-
|
|
38
|
+
...mapObject(input.parameters ?? {}, (key, value) => [
|
|
39
|
+
`TF_VAR_${key}`,
|
|
40
|
+
typeof value === 'object' ? JSON.stringify(value) : value,
|
|
41
|
+
]),
|
|
42
|
+
},
|
|
20
43
|
});
|
|
21
44
|
runShell(terraform, ['apply', '-auto-approve'], {
|
|
22
45
|
directory: input.pkgPath,
|
|
23
46
|
env: {
|
|
24
|
-
...mapObject(input.env ?? {}, (key, value) => [
|
|
47
|
+
...mapObject(input.env ?? {}, (key, value) => [
|
|
48
|
+
`TF_VAR_${key}`,
|
|
49
|
+
typeof value === 'object' ? JSON.stringify(value) : value,
|
|
50
|
+
]),
|
|
25
51
|
...input.env,
|
|
26
|
-
...mapObject(input.parameters ?? {}, (key, value) => [
|
|
27
|
-
|
|
52
|
+
...mapObject(input.parameters ?? {}, (key, value) => [
|
|
53
|
+
`TF_VAR_${key}`,
|
|
54
|
+
typeof value === 'object' ? JSON.stringify(value) : value,
|
|
55
|
+
]),
|
|
56
|
+
},
|
|
28
57
|
});
|
|
29
58
|
const env = await this.getEnv(input.pkgPath);
|
|
30
59
|
return {
|
|
31
60
|
env,
|
|
32
|
-
success: true
|
|
61
|
+
success: true,
|
|
33
62
|
};
|
|
34
63
|
}
|
|
35
64
|
catch (error) {
|
|
36
65
|
return {
|
|
37
66
|
reason: error.message,
|
|
38
|
-
success: false
|
|
67
|
+
success: false,
|
|
39
68
|
};
|
|
40
69
|
}
|
|
41
70
|
}
|
|
@@ -45,29 +74,41 @@ export class Terraform {
|
|
|
45
74
|
runShell(terraform, ['init'], {
|
|
46
75
|
directory: input.pkgPath,
|
|
47
76
|
env: {
|
|
48
|
-
...mapObject(input.env ?? {}, (key, value) => [
|
|
77
|
+
...mapObject(input.env ?? {}, (key, value) => [
|
|
78
|
+
`TF_VAR_${key}`,
|
|
79
|
+
typeof value === 'object' ? JSON.stringify(value) : value,
|
|
80
|
+
]),
|
|
49
81
|
...input.env,
|
|
50
|
-
...mapObject(input.parameters ?? {}, (key, value) => [
|
|
51
|
-
|
|
82
|
+
...mapObject(input.parameters ?? {}, (key, value) => [
|
|
83
|
+
`TF_VAR_${key}`,
|
|
84
|
+
typeof value === 'object' ? JSON.stringify(value) : value,
|
|
85
|
+
]),
|
|
86
|
+
},
|
|
52
87
|
});
|
|
53
88
|
const env = await this.getEnv(input.pkgPath);
|
|
54
89
|
runShell(terraform, ['destroy', '-auto-approve'], {
|
|
55
90
|
directory: input.pkgPath,
|
|
56
91
|
env: {
|
|
57
|
-
...mapObject(input.env ?? {}, (key, value) => [
|
|
92
|
+
...mapObject(input.env ?? {}, (key, value) => [
|
|
93
|
+
`TF_VAR_${key}`,
|
|
94
|
+
typeof value === 'object' ? JSON.stringify(value) : value,
|
|
95
|
+
]),
|
|
58
96
|
...input.env,
|
|
59
|
-
...mapObject(input.parameters ?? {}, (key, value) => [
|
|
60
|
-
|
|
97
|
+
...mapObject(input.parameters ?? {}, (key, value) => [
|
|
98
|
+
`TF_VAR_${key}`,
|
|
99
|
+
typeof value === 'object' ? JSON.stringify(value) : value,
|
|
100
|
+
]),
|
|
101
|
+
},
|
|
61
102
|
});
|
|
62
103
|
return {
|
|
63
104
|
env,
|
|
64
|
-
success: true
|
|
105
|
+
success: true,
|
|
65
106
|
};
|
|
66
107
|
}
|
|
67
108
|
catch (error) {
|
|
68
109
|
return {
|
|
69
110
|
reason: error.message,
|
|
70
|
-
success: false
|
|
111
|
+
success: false,
|
|
71
112
|
};
|
|
72
113
|
}
|
|
73
114
|
}
|
|
@@ -114,7 +155,8 @@ export class Terraform {
|
|
|
114
155
|
}
|
|
115
156
|
}
|
|
116
157
|
await new Promise((resolve, reject) => {
|
|
117
|
-
https
|
|
158
|
+
https
|
|
159
|
+
.get(url, async (response) => {
|
|
118
160
|
try {
|
|
119
161
|
await pipeline(response, decompress({ path: path.dirname(tfPath) }));
|
|
120
162
|
await fs.promises.chmod(tfPath, '0755');
|
|
@@ -123,7 +165,8 @@ export class Terraform {
|
|
|
123
165
|
throw new Error(`could not download terraform: ${error}`);
|
|
124
166
|
}
|
|
125
167
|
resolve(null);
|
|
126
|
-
})
|
|
168
|
+
})
|
|
169
|
+
.on('error', (error) => reject(error));
|
|
127
170
|
});
|
|
128
171
|
return true;
|
|
129
172
|
}
|
|
@@ -131,7 +174,7 @@ export class Terraform {
|
|
|
131
174
|
const terraform = await this.getTerraformBinary();
|
|
132
175
|
const resourceOut = runShell(terraform, ['output', '--json'], {
|
|
133
176
|
directory: pkgPath,
|
|
134
|
-
stdio: 'pipe'
|
|
177
|
+
stdio: 'pipe',
|
|
135
178
|
});
|
|
136
179
|
let outStr = resourceOut.output.toString().trim();
|
|
137
180
|
const start = outStr.indexOf('{');
|
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
import { BootstrapInput, DeployInput, DeployOutput, DestroyInput, DestroyOutput, Infrastructure, ProvisionInput, ProvisionOutput, ResolveEnvInput, ResolveEnvOutput, SaveEnvInput, SaveEnvOutput, UndeployInput, UndeployOutput } from './common.js';
|
|
1
|
+
import { BootstrapInput, DeployInput, DeployOutput, DestroyInput, DestroyOutput, Infrastructure, ProvisionInput, ProvisionOutput, ResolveEnvInput, ResolveEnvOutput, SaveEnvInput, SaveEnvOutput, StoreEnvInput, StoreEnvOutput, UndeployInput, UndeployOutput, UnstoreEnvInput, UnstoreEnvOutput } from './common.js';
|
|
2
2
|
export declare class AwsInfrastructure implements Infrastructure {
|
|
3
|
+
private configKey;
|
|
3
4
|
bootstrap(_: BootstrapInput): Promise<void>;
|
|
4
5
|
deploy(input: DeployInput): Promise<DeployOutput>;
|
|
5
6
|
destroy(input: DestroyInput): Promise<DestroyOutput>;
|
|
6
7
|
provision(input: ProvisionInput): Promise<ProvisionOutput>;
|
|
7
8
|
resolveEnv(input: ResolveEnvInput): Promise<ResolveEnvOutput>;
|
|
8
9
|
saveEnv(input: SaveEnvInput): Promise<SaveEnvOutput>;
|
|
10
|
+
storeEnv(input: StoreEnvInput): Promise<StoreEnvOutput>;
|
|
9
11
|
unbootstrap(_: BootstrapInput): Promise<void>;
|
|
10
12
|
undeploy(input: UndeployInput): Promise<UndeployOutput>;
|
|
11
|
-
|
|
12
|
-
private
|
|
13
|
-
private removeEnv;
|
|
14
|
-
private runCodeBuild;
|
|
15
|
-
private uploadProjectFiles;
|
|
13
|
+
unstoreEnv(input: UnstoreEnvInput): Promise<UnstoreEnvOutput>;
|
|
14
|
+
private getConfig;
|
|
16
15
|
}
|
|
@@ -1,20 +1,16 @@
|
|
|
1
|
-
import { BatchGetBuildsCommand, CodeBuildClient, StartBuildCommand } from '@aws-sdk/client-codebuild';
|
|
2
|
-
import { DeleteObjectsCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
|
|
3
1
|
import { GetSecretValueCommand, SecretsManagerClient } from '@aws-sdk/client-secrets-manager';
|
|
4
2
|
import { DeleteParameterCommand, GetParameterCommand, PutParameterCommand, SSMClient } from '@aws-sdk/client-ssm';
|
|
5
3
|
import { GetCallerIdentityCommand, STSClient } from '@aws-sdk/client-sts';
|
|
6
|
-
import { glob } from 'glob';
|
|
7
|
-
import ignore from 'ignore';
|
|
8
4
|
import { randomUUID } from 'node:crypto';
|
|
9
5
|
import fs from 'node:fs/promises';
|
|
6
|
+
import os from 'node:os';
|
|
10
7
|
import path from 'node:path';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import { objectToBase64 } from '../lib/object-utils.js';
|
|
8
|
+
import { getIac } from '../iac/index.js';
|
|
9
|
+
import { downloadPackage } from '../lib/package/index.js';
|
|
14
10
|
import { runShell } from '../lib/shell.js';
|
|
15
|
-
import { InfrastructureType } from './common.js';
|
|
16
11
|
import { destroyPackage, provisionPackage } from './index.js';
|
|
17
12
|
export class AwsInfrastructure {
|
|
13
|
+
configKey = '/hereya-bootstrap/config';
|
|
18
14
|
async bootstrap(_) {
|
|
19
15
|
const stsClient = new STSClient({});
|
|
20
16
|
const { Account: accountId } = await stsClient.send(new GetCallerIdentityCommand({}));
|
|
@@ -26,65 +22,88 @@ export class AwsInfrastructure {
|
|
|
26
22
|
throw new Error(output.reason);
|
|
27
23
|
}
|
|
28
24
|
const { env } = output;
|
|
29
|
-
const key =
|
|
25
|
+
const key = this.configKey;
|
|
30
26
|
const ssmClient = new SSMClient({});
|
|
31
27
|
const value = JSON.stringify(env);
|
|
32
28
|
await ssmClient.send(new PutParameterCommand({
|
|
33
29
|
Name: key,
|
|
34
30
|
Overwrite: true,
|
|
35
31
|
Type: 'String',
|
|
36
|
-
Value: value
|
|
32
|
+
Value: value,
|
|
37
33
|
}));
|
|
38
34
|
}
|
|
39
35
|
async deploy(input) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
input.parameters = {
|
|
47
|
-
...input.parameters,
|
|
48
|
-
hereyaProjectEnv: objectToBase64(input.projectEnv),
|
|
49
|
-
};
|
|
50
|
-
const output = await this.runCodeBuild({
|
|
51
|
-
...input,
|
|
52
|
-
deploy: true,
|
|
53
|
-
sourceS3Key: s3Key
|
|
54
|
-
});
|
|
55
|
-
if (!output.success) {
|
|
56
|
-
return output;
|
|
57
|
-
}
|
|
58
|
-
const env = await this.getEnv(input.id);
|
|
59
|
-
return { env, success: true };
|
|
60
|
-
}
|
|
61
|
-
finally {
|
|
62
|
-
if (s3Key && files.length > 0) {
|
|
63
|
-
await s3Client.send(new DeleteObjectsCommand({
|
|
64
|
-
Bucket: s3Bucket,
|
|
65
|
-
Delete: {
|
|
66
|
-
Objects: files.map(file => ({ Key: `${s3Key}/${file}` }))
|
|
67
|
-
}
|
|
68
|
-
}));
|
|
69
|
-
}
|
|
70
|
-
}
|
|
36
|
+
input.parameters = {
|
|
37
|
+
...input.parameters,
|
|
38
|
+
hereyaProjectEnv: JSON.stringify(input.projectEnv ?? {}),
|
|
39
|
+
hereyaProjectRootDir: input.projectRootDir,
|
|
40
|
+
};
|
|
41
|
+
return this.provision(input);
|
|
71
42
|
}
|
|
72
43
|
async destroy(input) {
|
|
73
|
-
const
|
|
74
|
-
const
|
|
44
|
+
const destPath = path.join(os.homedir(), '.hereya', input.id, input.canonicalName);
|
|
45
|
+
const downloadPath = await downloadPackage(input.pkgUrl, destPath);
|
|
46
|
+
const region = process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION;
|
|
47
|
+
const infraConfig = {
|
|
48
|
+
...await this.getConfig(),
|
|
49
|
+
region,
|
|
50
|
+
};
|
|
51
|
+
if (!infraConfig.terraformStateBucketName || !infraConfig.terraformStateLockTableName) {
|
|
52
|
+
return {
|
|
53
|
+
reason: 'could not find AWS infrastructure config. Did you run `hereya bootstrap aws`?',
|
|
54
|
+
success: false,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const iac$ = getIac({ type: input.iacType });
|
|
58
|
+
if (!iac$.supported) {
|
|
59
|
+
return { reason: iac$.reason, success: false };
|
|
60
|
+
}
|
|
61
|
+
const { iac } = iac$;
|
|
62
|
+
const output = await iac.destroy({
|
|
63
|
+
env: input.env ?? {},
|
|
64
|
+
id: input.id,
|
|
65
|
+
infraConfig,
|
|
66
|
+
parameters: input.parameters,
|
|
67
|
+
pkgPath: downloadPath,
|
|
68
|
+
});
|
|
75
69
|
if (!output.success) {
|
|
76
|
-
return output;
|
|
70
|
+
return { reason: output.reason, success: false };
|
|
77
71
|
}
|
|
78
|
-
|
|
79
|
-
|
|
72
|
+
// Remove downloaded package
|
|
73
|
+
await fs.rm(downloadPath, { recursive: true });
|
|
74
|
+
return { env: output.env, success: true };
|
|
80
75
|
}
|
|
81
76
|
async provision(input) {
|
|
82
|
-
const
|
|
77
|
+
const destPath = path.join(os.homedir(), '.hereya', input.id, input.canonicalName);
|
|
78
|
+
const downloadPath = await downloadPackage(input.pkgUrl, destPath);
|
|
79
|
+
const config = await this.getConfig();
|
|
80
|
+
const terraformStateBucketRegion = config.terraformStateBucketRegion || process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION;
|
|
81
|
+
const infraConfig = {
|
|
82
|
+
...config,
|
|
83
|
+
terraformStateBucketRegion,
|
|
84
|
+
};
|
|
85
|
+
if (!infraConfig.terraformStateBucketName || !infraConfig.terraformStateLockTableName) {
|
|
86
|
+
return {
|
|
87
|
+
reason: 'could not find AWS infrastructure config. Did you run `hereya bootstrap aws`?',
|
|
88
|
+
success: false,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const iac$ = getIac({ type: input.iacType });
|
|
92
|
+
if (!iac$.supported) {
|
|
93
|
+
return { reason: iac$.reason, success: false };
|
|
94
|
+
}
|
|
95
|
+
const { iac } = iac$;
|
|
96
|
+
const output = await iac.apply({
|
|
97
|
+
env: input.env ?? {},
|
|
98
|
+
id: input.id,
|
|
99
|
+
infraConfig,
|
|
100
|
+
parameters: input.parameters,
|
|
101
|
+
pkgPath: downloadPath,
|
|
102
|
+
});
|
|
83
103
|
if (!output.success) {
|
|
84
|
-
return output;
|
|
104
|
+
return { reason: output.reason, success: false };
|
|
85
105
|
}
|
|
86
|
-
|
|
87
|
-
return { env, success: true };
|
|
106
|
+
return { env: output.env, success: true };
|
|
88
107
|
}
|
|
89
108
|
async resolveEnv(input) {
|
|
90
109
|
try {
|
|
@@ -97,7 +116,7 @@ export class AwsInfrastructure {
|
|
|
97
116
|
}));
|
|
98
117
|
return {
|
|
99
118
|
isSecret: response.Parameter?.Type === 'SecureString',
|
|
100
|
-
value: response.Parameter?.Value ?? input.value
|
|
119
|
+
value: response.Parameter?.Value ?? input.value,
|
|
101
120
|
};
|
|
102
121
|
}
|
|
103
122
|
const secretManagerArnPattern = /^arn:aws:secretsmanager:[\da-z-]+:\d{12}:secret:[\w-]+/;
|
|
@@ -108,7 +127,7 @@ export class AwsInfrastructure {
|
|
|
108
127
|
}));
|
|
109
128
|
return {
|
|
110
129
|
isSecret: true,
|
|
111
|
-
value: response.SecretString ?? input.value
|
|
130
|
+
value: response.SecretString ?? input.value,
|
|
112
131
|
};
|
|
113
132
|
}
|
|
114
133
|
return { value: input.value };
|
|
@@ -127,7 +146,7 @@ export class AwsInfrastructure {
|
|
|
127
146
|
Name: key,
|
|
128
147
|
Overwrite: true,
|
|
129
148
|
Type: 'String',
|
|
130
|
-
Value: value
|
|
149
|
+
Value: value,
|
|
131
150
|
}));
|
|
132
151
|
return { success: true };
|
|
133
152
|
}
|
|
@@ -135,6 +154,27 @@ export class AwsInfrastructure {
|
|
|
135
154
|
return { reason: error.message, success: false };
|
|
136
155
|
}
|
|
137
156
|
}
|
|
157
|
+
async storeEnv(input) {
|
|
158
|
+
if (!input.sensitive) {
|
|
159
|
+
return { success: true, value: input.value };
|
|
160
|
+
}
|
|
161
|
+
const ssmClient = new SSMClient({});
|
|
162
|
+
const id = randomUUID();
|
|
163
|
+
const ssmParameterName = input.oldValue ?? `/hereya/${input.name}/${id}`;
|
|
164
|
+
await ssmClient.send(new PutParameterCommand({
|
|
165
|
+
Name: ssmParameterName,
|
|
166
|
+
Overwrite: true,
|
|
167
|
+
Type: 'SecureString',
|
|
168
|
+
Value: input.value,
|
|
169
|
+
}));
|
|
170
|
+
const parameter = await ssmClient.send(new GetParameterCommand({
|
|
171
|
+
Name: ssmParameterName,
|
|
172
|
+
}));
|
|
173
|
+
if (!parameter.Parameter?.ARN) {
|
|
174
|
+
throw new Error(`Could not store env var ${input.name} for AWS infrastructure`);
|
|
175
|
+
}
|
|
176
|
+
return { success: true, value: parameter.Parameter.ARN };
|
|
177
|
+
}
|
|
138
178
|
async unbootstrap(_) {
|
|
139
179
|
const ssmClient = new SSMClient({});
|
|
140
180
|
const key = '/hereya-bootstrap/config';
|
|
@@ -153,206 +193,36 @@ export class AwsInfrastructure {
|
|
|
153
193
|
}
|
|
154
194
|
}
|
|
155
195
|
async undeploy(input) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
env = await this.getEnv(input.id);
|
|
163
|
-
}
|
|
164
|
-
catch (error) {
|
|
165
|
-
console.log(`Could not get env for ${input.id}: ${error.message}. Continuing with undeployment...`);
|
|
166
|
-
}
|
|
167
|
-
try {
|
|
168
|
-
({ files, s3Bucket, s3Client, s3Key } = await this.uploadProjectFiles(input));
|
|
169
|
-
input.parameters = {
|
|
170
|
-
...input.parameters,
|
|
171
|
-
hereyaProjectEnv: objectToBase64(input.projectEnv),
|
|
172
|
-
};
|
|
173
|
-
const output = await this.runCodeBuild({
|
|
174
|
-
...input,
|
|
175
|
-
deploy: true,
|
|
176
|
-
destroy: true,
|
|
177
|
-
sourceS3Key: s3Key
|
|
178
|
-
});
|
|
179
|
-
if (!output.success) {
|
|
180
|
-
return output;
|
|
181
|
-
}
|
|
182
|
-
return { env, success: true };
|
|
183
|
-
}
|
|
184
|
-
finally {
|
|
185
|
-
if (s3Key && files.length > 0) {
|
|
186
|
-
await s3Client.send(new DeleteObjectsCommand({
|
|
187
|
-
Bucket: s3Bucket,
|
|
188
|
-
Delete: {
|
|
189
|
-
Objects: files.map(file => ({ Key: `${s3Key}/${file}` }))
|
|
190
|
-
}
|
|
191
|
-
}));
|
|
192
|
-
}
|
|
193
|
-
}
|
|
196
|
+
input.parameters = {
|
|
197
|
+
...input.parameters,
|
|
198
|
+
hereyaProjectEnv: JSON.stringify(input.projectEnv ?? {}),
|
|
199
|
+
hereyaProjectRootDir: input.projectRootDir,
|
|
200
|
+
};
|
|
201
|
+
return this.destroy(input);
|
|
194
202
|
}
|
|
195
|
-
async
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const ssmParameter = await ssmClient.send(new GetParameterCommand({
|
|
200
|
-
Name: ssmParameterName,
|
|
201
|
-
}));
|
|
202
|
-
return JSON.parse(ssmParameter.Parameter?.Value ?? '{}');
|
|
203
|
-
}
|
|
204
|
-
catch (error) {
|
|
205
|
-
if (error.name === "ParameterNotFound") {
|
|
206
|
-
console.debug(`Parameter "${ssmParameterName}" does not exist.`);
|
|
207
|
-
return {};
|
|
208
|
-
}
|
|
209
|
-
throw error;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
async getFilesToUpload(rootDir) {
|
|
213
|
-
const ig = ignore.default();
|
|
214
|
-
const ignoreFilePath = await getAnyPath(`${rootDir}/.hereyaignore`, `${rootDir}/.gitignore`);
|
|
215
|
-
if (await fileExists(ignoreFilePath)) {
|
|
216
|
-
const ignoreFileContent = await fs.readFile(ignoreFilePath, 'utf8');
|
|
217
|
-
ig.add(ignoreFileContent);
|
|
203
|
+
async unstoreEnv(input) {
|
|
204
|
+
const parameterStoreArnPattern = /^arn:aws:ssm:[\da-z-]+:\d{12}:parameter\/[\w./-]+$/;
|
|
205
|
+
if (!parameterStoreArnPattern.test(input.value)) {
|
|
206
|
+
return { success: true };
|
|
218
207
|
}
|
|
219
|
-
const files = glob.sync('**/*', { cwd: rootDir, nodir: true });
|
|
220
|
-
return files.filter(file => !ig.ignores(file));
|
|
221
|
-
}
|
|
222
|
-
async removeEnv(id) {
|
|
223
208
|
const ssmClient = new SSMClient({});
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
Name: ssmParameterName,
|
|
209
|
+
const parameter = await ssmClient.send(new GetParameterCommand({
|
|
210
|
+
Name: input.value,
|
|
227
211
|
}));
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
let codebuildProjectName = '';
|
|
232
|
-
switch (input.iacType) {
|
|
233
|
-
case IacType.cdk: {
|
|
234
|
-
codebuildProjectName = 'hereyaCdk';
|
|
235
|
-
break;
|
|
236
|
-
}
|
|
237
|
-
case IacType.terraform: {
|
|
238
|
-
codebuildProjectName = 'hereyaTerraform';
|
|
239
|
-
break;
|
|
240
|
-
}
|
|
241
|
-
default: {
|
|
242
|
-
return { reason: `IAC type ${input.iacType} is not supported yet!`, success: false };
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
const ssmClient = new SSMClient({});
|
|
246
|
-
const parameterName = `/hereya/package-parameters/${input.id}`;
|
|
247
|
-
const parameterValue = Object.entries(input.parameters ?? {}).map(([key, value]) => `${key}=${typeof value === "object" ? objectToBase64(value) : value}`).join(',');
|
|
248
|
-
if (parameterValue) {
|
|
249
|
-
await ssmClient.send(new PutParameterCommand({
|
|
250
|
-
Name: parameterName,
|
|
251
|
-
Overwrite: true,
|
|
252
|
-
Type: 'SecureString',
|
|
253
|
-
Value: parameterValue,
|
|
254
|
-
}));
|
|
212
|
+
const ssmParameterName = parameter.Parameter?.Name;
|
|
213
|
+
if (!ssmParameterName) {
|
|
214
|
+
return { success: true };
|
|
255
215
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
{
|
|
259
|
-
name: 'HEREYA_ID',
|
|
260
|
-
type: 'PLAINTEXT',
|
|
261
|
-
value: input.id,
|
|
262
|
-
},
|
|
263
|
-
{
|
|
264
|
-
name: 'HEREYA_IAC_TYPE',
|
|
265
|
-
type: 'PLAINTEXT',
|
|
266
|
-
value: input.iacType,
|
|
267
|
-
},
|
|
268
|
-
{
|
|
269
|
-
name: 'HEREYA_INFRA_TYPE',
|
|
270
|
-
type: 'PLAINTEXT',
|
|
271
|
-
value: InfrastructureType.aws,
|
|
272
|
-
},
|
|
273
|
-
{
|
|
274
|
-
name: 'HEREYA_PARAMETERS',
|
|
275
|
-
type: parameterValue ? 'PARAMETER_STORE' : 'PLAINTEXT',
|
|
276
|
-
value: parameterValue ? parameterName : '',
|
|
277
|
-
},
|
|
278
|
-
{
|
|
279
|
-
name: 'HEREYA_WORKSPACE_ENV',
|
|
280
|
-
type: 'PLAINTEXT',
|
|
281
|
-
value: Object.entries(input.env ?? {}).map(([key, value]) => `${key}=${typeof value === "object" ? objectToBase64(value) : value}`).join(','),
|
|
282
|
-
},
|
|
283
|
-
{
|
|
284
|
-
name: 'PKG_REPO_URL',
|
|
285
|
-
type: 'PLAINTEXT',
|
|
286
|
-
value: input.pkgUrl,
|
|
287
|
-
},
|
|
288
|
-
{
|
|
289
|
-
name: 'HEREYA_DESTROY',
|
|
290
|
-
type: 'PLAINTEXT',
|
|
291
|
-
value: input.destroy ? 'true' : '',
|
|
292
|
-
},
|
|
293
|
-
{
|
|
294
|
-
name: 'HEREYA_DEPLOY',
|
|
295
|
-
type: 'PLAINTEXT',
|
|
296
|
-
value: input.deploy ? 'true' : '',
|
|
297
|
-
},
|
|
298
|
-
{
|
|
299
|
-
name: 'HEREYA_PROJECT_S3_KEY',
|
|
300
|
-
type: 'PLAINTEXT',
|
|
301
|
-
value: input.deploy ? input.sourceS3Key : '',
|
|
302
|
-
}
|
|
303
|
-
],
|
|
304
|
-
projectName: codebuildProjectName,
|
|
216
|
+
await ssmClient.send(new DeleteParameterCommand({
|
|
217
|
+
Name: ssmParameterName,
|
|
305
218
|
}));
|
|
306
|
-
console.log(`Deployment ${response.build?.id} started successfully.`);
|
|
307
|
-
const command = new BatchGetBuildsCommand({
|
|
308
|
-
ids: [response.build?.id ?? ''],
|
|
309
|
-
});
|
|
310
|
-
const deploymentResult = await new Promise((resolve) => {
|
|
311
|
-
const handle = setInterval(async () => {
|
|
312
|
-
const buildResponse = await codebuildClient.send(command);
|
|
313
|
-
const build = buildResponse.builds?.[0];
|
|
314
|
-
if (build?.buildStatus === 'IN_PROGRESS') {
|
|
315
|
-
console.log(`Deployment ${response.build?.id} still in progress...`);
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
clearInterval(handle);
|
|
319
|
-
console.log(`Deployment ${response.build?.id} finished with status ${build?.buildStatus}.`);
|
|
320
|
-
resolve(build);
|
|
321
|
-
}, 10_000); // 10 seconds
|
|
322
|
-
});
|
|
323
|
-
if (deploymentResult?.buildStatus !== 'SUCCEEDED') {
|
|
324
|
-
return { reason: `Deployment failed with status ${deploymentResult?.buildStatus}`, success: false };
|
|
325
|
-
}
|
|
326
|
-
// remove the parameter
|
|
327
|
-
if (parameterValue) {
|
|
328
|
-
await ssmClient.send(new DeleteParameterCommand({
|
|
329
|
-
Name: parameterName,
|
|
330
|
-
}));
|
|
331
|
-
}
|
|
332
219
|
return { success: true };
|
|
333
220
|
}
|
|
334
|
-
async
|
|
335
|
-
const key = '/hereya-bootstrap/config';
|
|
221
|
+
async getConfig() {
|
|
336
222
|
const ssmClient = new SSMClient({});
|
|
337
|
-
const
|
|
338
|
-
Name:
|
|
339
|
-
}));
|
|
340
|
-
const bootstrapConfig = JSON.parse(response.Parameter?.Value ?? '{}');
|
|
341
|
-
if (!bootstrapConfig.hereyaSourceCodeBucketName) {
|
|
342
|
-
throw new Error('hereyaSourceCodeBucketName not found in bootstrap config');
|
|
343
|
-
}
|
|
344
|
-
const s3Key = `${input.id}/${randomUUID()}`;
|
|
345
|
-
const s3Bucket = bootstrapConfig.hereyaSourceCodeBucketName;
|
|
346
|
-
const files = await this.getFilesToUpload(input.projectRootDir);
|
|
347
|
-
const s3Client = new S3Client({});
|
|
348
|
-
await Promise.all(files.map(async (file) => {
|
|
349
|
-
console.log(`Uploading ${file} to s3://${s3Bucket}/${s3Key}`);
|
|
350
|
-
await s3Client.send(new PutObjectCommand({
|
|
351
|
-
Body: await fs.readFile(path.join(input.projectRootDir, file)),
|
|
352
|
-
Bucket: s3Bucket,
|
|
353
|
-
Key: `${s3Key}/${file}`,
|
|
354
|
-
}));
|
|
223
|
+
const ssmParameter = await ssmClient.send(new GetParameterCommand({
|
|
224
|
+
Name: this.configKey,
|
|
355
225
|
}));
|
|
356
|
-
return
|
|
226
|
+
return JSON.parse(ssmParameter.Parameter?.Value ?? '{}');
|
|
357
227
|
}
|
|
358
228
|
}
|
|
@@ -12,8 +12,10 @@ export interface Infrastructure {
|
|
|
12
12
|
provision(input: ProvisionInput): Promise<ProvisionOutput>;
|
|
13
13
|
resolveEnv(input: ResolveEnvInput): Promise<ResolveEnvOutput>;
|
|
14
14
|
saveEnv(input: SaveEnvInput): Promise<SaveEnvOutput>;
|
|
15
|
+
storeEnv(input: StoreEnvInput): Promise<StoreEnvOutput>;
|
|
15
16
|
unbootstrap(input: BootstrapInput): Promise<void>;
|
|
16
17
|
undeploy(input: UndeployInput): Promise<UndeployOutput>;
|
|
18
|
+
unstoreEnv(input: UnstoreEnvInput): Promise<UnstoreEnvOutput>;
|
|
17
19
|
}
|
|
18
20
|
export type BootstrapInput = {
|
|
19
21
|
force?: boolean;
|
|
@@ -70,3 +72,25 @@ export type SaveEnvOutput = {
|
|
|
70
72
|
} | {
|
|
71
73
|
success: true;
|
|
72
74
|
};
|
|
75
|
+
export type StoreEnvInput = {
|
|
76
|
+
name: string;
|
|
77
|
+
oldValue?: string;
|
|
78
|
+
sensitive?: boolean;
|
|
79
|
+
value: string;
|
|
80
|
+
};
|
|
81
|
+
export type StoreEnvOutput = {
|
|
82
|
+
reason: string;
|
|
83
|
+
success: false;
|
|
84
|
+
} | {
|
|
85
|
+
success: true;
|
|
86
|
+
value: string;
|
|
87
|
+
};
|
|
88
|
+
export type UnstoreEnvInput = {
|
|
89
|
+
value: string;
|
|
90
|
+
};
|
|
91
|
+
export type UnstoreEnvOutput = {
|
|
92
|
+
reason: string;
|
|
93
|
+
success: false;
|
|
94
|
+
} | {
|
|
95
|
+
success: true;
|
|
96
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DeployInput, DeployOutput, Infrastructure, ProvisionInput, ProvisionOutput, SaveEnvInput, SaveEnvOutput, UndeployInput, UndeployOutput } from './common.js';
|
|
1
|
+
import { DeployInput, DeployOutput, Infrastructure, ProvisionInput, ProvisionOutput, SaveEnvInput, SaveEnvOutput, StoreEnvInput, StoreEnvOutput, UndeployInput, UndeployOutput, UnstoreEnvInput, UnstoreEnvOutput } from './common.js';
|
|
2
2
|
export declare class LocalInfrastructure implements Infrastructure {
|
|
3
3
|
bootstrap(): Promise<void>;
|
|
4
4
|
deploy(input: DeployInput): Promise<DeployOutput>;
|
|
@@ -10,8 +10,8 @@ export declare class LocalInfrastructure implements Infrastructure {
|
|
|
10
10
|
value: string;
|
|
11
11
|
}>;
|
|
12
12
|
saveEnv(input: SaveEnvInput): Promise<SaveEnvOutput>;
|
|
13
|
+
storeEnv(input: StoreEnvInput): Promise<StoreEnvOutput>;
|
|
13
14
|
unbootstrap(): Promise<void>;
|
|
14
15
|
undeploy(input: UndeployInput): Promise<UndeployOutput>;
|
|
15
|
-
|
|
16
|
-
private isNotEmpty;
|
|
16
|
+
unstoreEnv(_: UnstoreEnvInput): Promise<UnstoreEnvOutput>;
|
|
17
17
|
}
|