rastack 0.0.63 → 0.0.64
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/CHANGELOG.md +7 -0
- package/admin/AdminApp.tsx +56 -3
- package/admin/index.tsx +8 -2
- package/admin/ui/icons.tsx +9 -0
- package/admin/ui/styles.ts +3 -0
- package/admin/useAdmin.ts +41 -4
- package/dist/admin.js +17 -14
- package/dist/compile/analyze.js +11 -1
- package/dist/define/index.d.ts +8 -1
- package/dist/define/index.js +2 -2
- package/dist/define/manifest.js +7 -1
- package/dist/deploy/io.d.ts +50 -0
- package/dist/deploy/io.js +124 -0
- package/dist/rastack-bootstrap.js +18 -47
- package/dist/rastack-init.d.ts +6 -3
- package/dist/rastack-init.js +43 -30
- package/dist/wasm/rastack_wasm.js +1 -1
- package/dist/wasm/rastack_wasm_bg.wasm +0 -0
- package/package.json +1 -1
- package/provider/provider.tsx +23 -0
- package/provider/types.ts +9 -0
- package/src/compile/analyze.ts +11 -1
- package/src/define/index.ts +5 -1
- package/src/define/manifest.ts +7 -1
- package/src/deploy/io.ts +162 -1
- package/src/rastack-bootstrap.ts +24 -69
- package/src/rastack-init.ts +50 -41
package/src/compile/analyze.ts
CHANGED
|
@@ -179,7 +179,17 @@ function analyzeField(
|
|
|
179
179
|
const app = literalTypeProp(fieldType, "__app", checker, initializer);
|
|
180
180
|
const model = literalTypeProp(fieldType, "__model", checker, initializer);
|
|
181
181
|
if (app && model) {
|
|
182
|
-
|
|
182
|
+
const fk: FieldModel = { name, type: "fk", relation: { app, model } };
|
|
183
|
+
// `s.ref(() => X, { null: true })` — a nullable relation (a shared secret
|
|
184
|
+
// with no project, an unassigned task). The options object is the ref
|
|
185
|
+
// call's second argument, read the same way scalar constraints are.
|
|
186
|
+
if (ts.isCallExpression(initializer)) {
|
|
187
|
+
const opts = literalToValue(initializer.arguments[1]);
|
|
188
|
+
if (opts && typeof opts === "object" && opts.null === true) {
|
|
189
|
+
fk.null = true;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return fk;
|
|
183
193
|
}
|
|
184
194
|
return undefined;
|
|
185
195
|
}
|
package/src/define/index.ts
CHANGED
|
@@ -66,6 +66,9 @@ export interface Ref<App extends string, Model extends string> {
|
|
|
66
66
|
readonly __model: Model;
|
|
67
67
|
/** The authored `() => Resource` thunk — carried at runtime. */
|
|
68
68
|
readonly target?: () => ResourceType<App, Model, any>;
|
|
69
|
+
/** Optional FK constraints — `null: true` makes the relation nullable
|
|
70
|
+
* (e.g. a shared secret with no project, or an unassigned task). */
|
|
71
|
+
readonly options?: { null?: boolean };
|
|
69
72
|
}
|
|
70
73
|
|
|
71
74
|
export interface StringOptions {
|
|
@@ -153,8 +156,9 @@ export const s = {
|
|
|
153
156
|
*/
|
|
154
157
|
ref<A extends string, M extends string>(
|
|
155
158
|
thunk: () => ResourceType<A, M, any>,
|
|
159
|
+
options?: { null?: boolean },
|
|
156
160
|
): Ref<A, M> {
|
|
157
|
-
return { __rastackRef: true, target: thunk } as Ref<A, M>;
|
|
161
|
+
return { __rastackRef: true, target: thunk, options } as Ref<A, M>;
|
|
158
162
|
},
|
|
159
163
|
};
|
|
160
164
|
|
package/src/define/manifest.ts
CHANGED
|
@@ -121,11 +121,17 @@ function runtimeField(
|
|
|
121
121
|
`manifestFromResources: ${res.app}.${res.model}.${name} is an s.ref(...) whose target thunk did not return a resource() value`,
|
|
122
122
|
);
|
|
123
123
|
}
|
|
124
|
-
|
|
124
|
+
const fk: FieldModel = {
|
|
125
125
|
name,
|
|
126
126
|
type: "fk",
|
|
127
127
|
relation: { app: target.app, model: target.model },
|
|
128
128
|
};
|
|
129
|
+
// A nullable FK (`s.ref(() => X, { null: true })`) — e.g. a shared secret
|
|
130
|
+
// with no project — so the engine allows and stores a null relation.
|
|
131
|
+
if ((field.options as { null?: boolean } | undefined)?.null === true) {
|
|
132
|
+
fk.null = true;
|
|
133
|
+
}
|
|
134
|
+
return fk;
|
|
129
135
|
}
|
|
130
136
|
|
|
131
137
|
if (typeof field.__rastackScalar !== "string") return undefined;
|
package/src/deploy/io.ts
CHANGED
|
@@ -6,11 +6,20 @@
|
|
|
6
6
|
|
|
7
7
|
import { execFileSync, execSync } from "child_process";
|
|
8
8
|
import * as fs from "fs";
|
|
9
|
+
import * as https from "https";
|
|
9
10
|
import * as os from "os";
|
|
10
11
|
import * as path from "path";
|
|
11
12
|
import * as readline from "readline";
|
|
12
13
|
import { CONFIG_PATH } from "./workflows";
|
|
13
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
BOOTSTRAP_ARTIFACTS_OUTPUT,
|
|
16
|
+
BOOTSTRAP_TEMPLATES_OUTPUT,
|
|
17
|
+
DeployConfig,
|
|
18
|
+
PartialDeployConfig,
|
|
19
|
+
bootstrapStackArgs,
|
|
20
|
+
bootstrapStackName,
|
|
21
|
+
normalizeConfig,
|
|
22
|
+
} from "./config";
|
|
14
23
|
import { zipStore } from "./zip";
|
|
15
24
|
|
|
16
25
|
/** Absolute path to the deploy config for a project root. */
|
|
@@ -50,6 +59,65 @@ export function resolveDeployAsset(
|
|
|
50
59
|
return null;
|
|
51
60
|
}
|
|
52
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Base URL the framework's CloudFormation templates are published at. The same
|
|
64
|
+
* site that hosts the installer (`reactapistack.com/setup.sh`) serves the base
|
|
65
|
+
* templates, so a CLI whose bundled copy is missing (a partial install, or a
|
|
66
|
+
* globally-linked dev build) can still fetch them.
|
|
67
|
+
*/
|
|
68
|
+
export const TEMPLATE_BASE_URL = "https://reactapistack.com/cloudformation";
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Download a published CloudFormation template to a temp file, returning its
|
|
72
|
+
* path — or null on any failure (offline, 404, write error). Used as the
|
|
73
|
+
* fallback when the template is not bundled with the install.
|
|
74
|
+
*/
|
|
75
|
+
export function downloadTemplate(
|
|
76
|
+
name: string,
|
|
77
|
+
base = TEMPLATE_BASE_URL,
|
|
78
|
+
): Promise<string | null> {
|
|
79
|
+
const url = `${base}/${name}`;
|
|
80
|
+
return new Promise((resolve) => {
|
|
81
|
+
https
|
|
82
|
+
.get(url, (res) => {
|
|
83
|
+
if (res.statusCode !== 200) {
|
|
84
|
+
res.resume();
|
|
85
|
+
resolve(null);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const chunks: Buffer[] = [];
|
|
89
|
+
res.on("data", (c: Buffer) => chunks.push(c));
|
|
90
|
+
res.on("end", () => {
|
|
91
|
+
try {
|
|
92
|
+
const dest = path.join(
|
|
93
|
+
os.tmpdir(),
|
|
94
|
+
`rastack-${path.basename(name)}`,
|
|
95
|
+
);
|
|
96
|
+
fs.writeFileSync(dest, Buffer.concat(chunks));
|
|
97
|
+
resolve(dest);
|
|
98
|
+
} catch {
|
|
99
|
+
resolve(null);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
})
|
|
103
|
+
.on("error", () => resolve(null));
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Resolve a CloudFormation template to a local file path for `aws cloudformation
|
|
109
|
+
* deploy` (which accepts only `--template-file`). Prefers the copy bundled with
|
|
110
|
+
* the `rastack` install; when that is missing, downloads it from the React API
|
|
111
|
+
* Stack website. Returns null only when the template is neither bundled nor
|
|
112
|
+
* fetchable.
|
|
113
|
+
*/
|
|
114
|
+
export async function resolveTemplateFile(
|
|
115
|
+
name: string,
|
|
116
|
+
cwd = process.cwd(),
|
|
117
|
+
): Promise<string | null> {
|
|
118
|
+
return resolveDeployAsset(name, cwd) ?? (await downloadTemplate(name));
|
|
119
|
+
}
|
|
120
|
+
|
|
53
121
|
/** Read + normalize the deploy config, or null when it does not exist yet. */
|
|
54
122
|
export function loadConfig(cwd = process.cwd()): DeployConfig | null {
|
|
55
123
|
const file = configPath(cwd);
|
|
@@ -121,6 +189,26 @@ export function awsAvailable(): boolean {
|
|
|
121
189
|
return awsCapture(["--version"]) !== null;
|
|
122
190
|
}
|
|
123
191
|
|
|
192
|
+
/**
|
|
193
|
+
* The AWS account id the CLI's active credentials resolve to, or null when the
|
|
194
|
+
* CLI has no usable credentials (not logged in, an expired SSO session, or no
|
|
195
|
+
* configured profile). `init`/`bootstrap` call this to fail fast with an
|
|
196
|
+
* actionable "log in" message before attempting a stack deploy, instead of
|
|
197
|
+
* letting the first `cloudformation deploy` fail with a cryptic AWS error.
|
|
198
|
+
*/
|
|
199
|
+
export function awsIdentity(region?: string): string | null {
|
|
200
|
+
const args = [
|
|
201
|
+
"sts",
|
|
202
|
+
"get-caller-identity",
|
|
203
|
+
"--query",
|
|
204
|
+
"Account",
|
|
205
|
+
"--output",
|
|
206
|
+
"text",
|
|
207
|
+
];
|
|
208
|
+
if (region) args.push("--region", region);
|
|
209
|
+
return awsCapture(args);
|
|
210
|
+
}
|
|
211
|
+
|
|
124
212
|
/**
|
|
125
213
|
* A single Output value of a CloudFormation stack, or null when the stack does
|
|
126
214
|
* not exist or the key is unset. `describe-stacks` prints `None` for a missing
|
|
@@ -176,6 +264,79 @@ export function uploadPlaceholder(
|
|
|
176
264
|
return true;
|
|
177
265
|
}
|
|
178
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Deploy the tier-0 bootstrap stack, retrying once against an account that
|
|
269
|
+
* already has a GitHub OIDC provider — AWS allows only one provider per URL, so
|
|
270
|
+
* a second create is rejected and the retry tells the template to reuse the
|
|
271
|
+
* existing one. Throws on failure (after the retry). Shared by `rastack init`
|
|
272
|
+
* and `rastack bootstrap` so both stand the stack up identically.
|
|
273
|
+
*/
|
|
274
|
+
export function deployBootstrap(
|
|
275
|
+
cfg: DeployConfig,
|
|
276
|
+
reuseOidc: boolean,
|
|
277
|
+
template: string,
|
|
278
|
+
): void {
|
|
279
|
+
try {
|
|
280
|
+
awsRun(bootstrapStackArgs(cfg, reuseOidc, template));
|
|
281
|
+
} catch (err) {
|
|
282
|
+
if (reuseOidc) throw err;
|
|
283
|
+
console.log(
|
|
284
|
+
" • retrying bootstrap, reusing the account's existing GitHub OIDC provider",
|
|
285
|
+
);
|
|
286
|
+
awsRun(bootstrapStackArgs(cfg, true, template));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Ensure the tier-0 bootstrap stack exists for this account+region, **creating
|
|
292
|
+
* it when missing**, and return its artifacts/templates buckets. This is why
|
|
293
|
+
* `rastack init` is a single command that stands up *everything* — the
|
|
294
|
+
* account-level bootstrap stack and the per-app backend — with no separate
|
|
295
|
+
* `rastack bootstrap` step. Idempotent: an existing stack is reconciled in
|
|
296
|
+
* place. Throws with an actionable message when the template cannot be resolved,
|
|
297
|
+
* the deploy fails, or the buckets can't be read back.
|
|
298
|
+
*/
|
|
299
|
+
export async function ensureBootstrapStack(
|
|
300
|
+
cfg: DeployConfig,
|
|
301
|
+
reuseOidc = false,
|
|
302
|
+
): Promise<{ artifactsBucket: string; templatesBucket: string | null }> {
|
|
303
|
+
const stack = bootstrapStackName(cfg);
|
|
304
|
+
const existing = stackOutput(stack, BOOTSTRAP_ARTIFACTS_OUTPUT, cfg.region);
|
|
305
|
+
if (existing) {
|
|
306
|
+
console.log(` ✓ bootstrap stack ${stack} already exists — reconciling`);
|
|
307
|
+
} else {
|
|
308
|
+
console.log(
|
|
309
|
+
`\n▸ Creating the tier-0 bootstrap stack ${stack} (@ ${cfg.region})`,
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const template = await resolveTemplateFile("bootstrap.yaml");
|
|
314
|
+
if (!template) {
|
|
315
|
+
throw new Error(
|
|
316
|
+
"bootstrap.yaml template not found in the rastack package and could not be\n" +
|
|
317
|
+
" downloaded from reactapistack.com. Reinstall rastack (`npm i -g rastack`).",
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
deployBootstrap(cfg, reuseOidc, template);
|
|
321
|
+
|
|
322
|
+
const artifactsBucket = stackOutput(
|
|
323
|
+
stack,
|
|
324
|
+
BOOTSTRAP_ARTIFACTS_OUTPUT,
|
|
325
|
+
cfg.region,
|
|
326
|
+
);
|
|
327
|
+
const templatesBucket = stackOutput(
|
|
328
|
+
stack,
|
|
329
|
+
BOOTSTRAP_TEMPLATES_OUTPUT,
|
|
330
|
+
cfg.region,
|
|
331
|
+
);
|
|
332
|
+
if (!artifactsBucket) {
|
|
333
|
+
throw new Error(
|
|
334
|
+
`bootstrap stack ${stack} deployed but its ArtifactsBucket output could not be read.`,
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
return { artifactsBucket, templatesBucket };
|
|
338
|
+
}
|
|
339
|
+
|
|
179
340
|
// ── GitHub CLI boundary ──────────────────────────────────────────────────────
|
|
180
341
|
// After the stack is up `init` wires the two repo Variables CI reads
|
|
181
342
|
// (AWS_DEPLOY_ROLE_ARN / AWS_REGION) through the developer's own `gh` — already
|
package/src/rastack-bootstrap.ts
CHANGED
|
@@ -25,11 +25,8 @@
|
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
import {
|
|
28
|
-
BOOTSTRAP_ARTIFACTS_OUTPUT,
|
|
29
|
-
BOOTSTRAP_TEMPLATES_OUTPUT,
|
|
30
28
|
DeployConfig,
|
|
31
29
|
PartialDeployConfig,
|
|
32
|
-
bootstrapStackArgs,
|
|
33
30
|
bootstrapStackCommand,
|
|
34
31
|
bootstrapStackName,
|
|
35
32
|
mergeConfig,
|
|
@@ -40,12 +37,12 @@ import {
|
|
|
40
37
|
import {
|
|
41
38
|
ask,
|
|
42
39
|
awsAvailable,
|
|
43
|
-
|
|
40
|
+
awsIdentity,
|
|
44
41
|
canPrompt,
|
|
42
|
+
ensureBootstrapStack,
|
|
45
43
|
gitRemoteUrl,
|
|
46
44
|
loadConfig,
|
|
47
45
|
resolveDeployAsset,
|
|
48
|
-
stackOutput,
|
|
49
46
|
uploadPlaceholder,
|
|
50
47
|
withPrompt,
|
|
51
48
|
} from "./deploy/io";
|
|
@@ -164,47 +161,12 @@ function printPlan(cfg: DeployConfig, reuseOidc: boolean): void {
|
|
|
164
161
|
}
|
|
165
162
|
|
|
166
163
|
/**
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
164
|
+
* Stand the tier-0 bootstrap stack up and upload the Lambda placeholder. The
|
|
165
|
+
* deploy (with the GitHub-OIDC-reuse retry) and template resolution are shared
|
|
166
|
+
* with `rastack init` via `ensureBootstrapStack`, so both stand the stack up
|
|
167
|
+
* identically.
|
|
170
168
|
*/
|
|
171
|
-
function
|
|
172
|
-
const template = resolveDeployAsset("bootstrap.yaml");
|
|
173
|
-
if (!template) {
|
|
174
|
-
console.error(
|
|
175
|
-
" ✗ bootstrap.yaml template not found in the rastack package. Reinstall\n" +
|
|
176
|
-
" rastack (`npm i -g rastack`) or run from the framework repo.",
|
|
177
|
-
);
|
|
178
|
-
process.exit(1);
|
|
179
|
-
}
|
|
180
|
-
try {
|
|
181
|
-
awsRun(bootstrapStackArgs(cfg, reuseOidc, template));
|
|
182
|
-
} catch (err) {
|
|
183
|
-
if (reuseOidc) {
|
|
184
|
-
console.error(
|
|
185
|
-
`\n ✗ bootstrap stack deploy failed.\n ${(err as Error).message}`,
|
|
186
|
-
);
|
|
187
|
-
process.exit(1);
|
|
188
|
-
}
|
|
189
|
-
// The most common cause is an account that already has a GitHub OIDC
|
|
190
|
-
// provider. Retry telling the template to reuse it instead of creating a
|
|
191
|
-
// second (which CloudFormation rejects).
|
|
192
|
-
console.log(
|
|
193
|
-
" • retrying bootstrap, reusing the account's existing GitHub OIDC provider",
|
|
194
|
-
);
|
|
195
|
-
try {
|
|
196
|
-
awsRun(bootstrapStackArgs(cfg, true, template));
|
|
197
|
-
} catch (retryErr) {
|
|
198
|
-
console.error(
|
|
199
|
-
`\n ✗ bootstrap stack deploy failed.\n ${(retryErr as Error).message}`,
|
|
200
|
-
);
|
|
201
|
-
process.exit(1);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/** Stand the tier-0 bootstrap stack up and upload the Lambda placeholder. */
|
|
207
|
-
function standUp(cfg: DeployConfig, reuseOidc: boolean): void {
|
|
169
|
+
async function standUp(cfg: DeployConfig, reuseOidc: boolean): Promise<void> {
|
|
208
170
|
if (!awsAvailable()) {
|
|
209
171
|
console.error(
|
|
210
172
|
" ✗ the AWS CLI is not on PATH. Install it and configure credentials,\n" +
|
|
@@ -213,33 +175,26 @@ function standUp(cfg: DeployConfig, reuseOidc: boolean): void {
|
|
|
213
175
|
process.exit(1);
|
|
214
176
|
}
|
|
215
177
|
|
|
216
|
-
|
|
217
|
-
const
|
|
218
|
-
if (
|
|
219
|
-
console.log(
|
|
220
|
-
`\n▸ Bootstrap stack ${stack} already exists — reconciling (idempotent).`,
|
|
221
|
-
);
|
|
222
|
-
} else {
|
|
223
|
-
console.log(`\n▸ Creating bootstrap stack ${stack} (@ ${cfg.region})`);
|
|
224
|
-
}
|
|
225
|
-
deployBootstrap(cfg, reuseOidc);
|
|
226
|
-
|
|
227
|
-
const artifactsBucket = stackOutput(
|
|
228
|
-
stack,
|
|
229
|
-
BOOTSTRAP_ARTIFACTS_OUTPUT,
|
|
230
|
-
cfg.region,
|
|
231
|
-
);
|
|
232
|
-
const templatesBucket = stackOutput(
|
|
233
|
-
stack,
|
|
234
|
-
BOOTSTRAP_TEMPLATES_OUTPUT,
|
|
235
|
-
cfg.region,
|
|
236
|
-
);
|
|
237
|
-
if (!artifactsBucket) {
|
|
178
|
+
// Verify the AWS CLI is logged in before attempting a stack deploy.
|
|
179
|
+
const account = awsIdentity(cfg.region);
|
|
180
|
+
if (!account) {
|
|
238
181
|
console.error(
|
|
239
|
-
" ✗
|
|
182
|
+
" ✗ the AWS CLI has no usable credentials — it is not logged in.\n" +
|
|
183
|
+
" Run `aws configure` (or `aws sso login`), then re-run `rastack bootstrap`.\n" +
|
|
184
|
+
" Or re-run with --print to see the command.",
|
|
240
185
|
);
|
|
241
186
|
process.exit(1);
|
|
242
187
|
}
|
|
188
|
+
console.log(` ✓ AWS account ${account} (${cfg.region})`);
|
|
189
|
+
|
|
190
|
+
const stack = bootstrapStackName(cfg);
|
|
191
|
+
const { artifactsBucket, templatesBucket } = await ensureBootstrapStack(
|
|
192
|
+
cfg,
|
|
193
|
+
reuseOidc,
|
|
194
|
+
).catch((err: Error) => {
|
|
195
|
+
console.error(`\n ✗ ${err.message}`);
|
|
196
|
+
process.exit(1);
|
|
197
|
+
});
|
|
243
198
|
console.log(` ✓ artifacts bucket: ${artifactsBucket}`);
|
|
244
199
|
if (templatesBucket) console.log(` ✓ templates bucket: ${templatesBucket}`);
|
|
245
200
|
|
|
@@ -299,7 +254,7 @@ async function main(argv: string[]): Promise<void> {
|
|
|
299
254
|
}
|
|
300
255
|
}
|
|
301
256
|
|
|
302
|
-
standUp(cfg, flags.reuseOidc);
|
|
257
|
+
await standUp(cfg, flags.reuseOidc);
|
|
303
258
|
}
|
|
304
259
|
|
|
305
260
|
if (require.main === module) {
|
package/src/rastack-init.ts
CHANGED
|
@@ -12,14 +12,17 @@
|
|
|
12
12
|
* 1. writes `.rastack/deploy.json` + `.github/workflows/{ci,deploy-backend}.yml`
|
|
13
13
|
* (GitHub coordinates default from the `origin` remote; missing fields are
|
|
14
14
|
* prompted for on a TTY);
|
|
15
|
-
* 2.
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* 2. verifies the AWS CLI is logged in, then ensures the tier-0 bootstrap stack
|
|
16
|
+
* exists — creating it (the S3 template/placeholder buckets + GitHub OIDC
|
|
17
|
+
* provider) when missing, reconciling it when it already does;
|
|
18
18
|
* 3. uploads the Lambda placeholder bundle to that bucket;
|
|
19
19
|
* 4. deploys the per-app backend (app.yaml) with the resolved ArtifactsBucket;
|
|
20
20
|
* 5. reads the stack Outputs back into the config and prints the two GitHub repo
|
|
21
21
|
* Variables CI needs.
|
|
22
22
|
*
|
|
23
|
+
* The CloudFormation templates ship inside the `rastack` package; a partial
|
|
24
|
+
* install falls back to downloading them from reactapistack.com.
|
|
25
|
+
*
|
|
23
26
|
* Thereafter the recurring code deploy is CI's job — `rastack ci deploy`, which
|
|
24
27
|
* `rastack deploy` also runs. `--no-deploy` scaffolds only; `--print` shows the
|
|
25
28
|
* exact single-line `aws cloudformation deploy` commands without running them.
|
|
@@ -30,12 +33,12 @@ import * as path from "path";
|
|
|
30
33
|
import {
|
|
31
34
|
APP_STACK_OUTPUTS,
|
|
32
35
|
BOOTSTRAP_ARTIFACTS_OUTPUT,
|
|
33
|
-
BOOTSTRAP_TEMPLATES_OUTPUT,
|
|
34
36
|
DeployConfig,
|
|
35
37
|
DeployOutputs,
|
|
36
38
|
EnvName,
|
|
37
39
|
ENV_NAMES,
|
|
38
40
|
PartialDeployConfig,
|
|
41
|
+
bootstrapStackCommand,
|
|
39
42
|
bootstrapStackName,
|
|
40
43
|
deployStackCommandForEnv,
|
|
41
44
|
deployStackArgsForEnv,
|
|
@@ -58,8 +61,10 @@ import {
|
|
|
58
61
|
import {
|
|
59
62
|
ask,
|
|
60
63
|
awsAvailable,
|
|
64
|
+
awsIdentity,
|
|
61
65
|
awsRun,
|
|
62
66
|
canPrompt,
|
|
67
|
+
ensureBootstrapStack,
|
|
63
68
|
ghAuthLogin,
|
|
64
69
|
ghAuthenticated,
|
|
65
70
|
ghAvailable,
|
|
@@ -67,6 +72,7 @@ import {
|
|
|
67
72
|
gitRemoteUrl,
|
|
68
73
|
loadConfig,
|
|
69
74
|
resolveDeployAsset,
|
|
75
|
+
resolveTemplateFile,
|
|
70
76
|
saveConfig,
|
|
71
77
|
setRepoVariable,
|
|
72
78
|
stackOutput,
|
|
@@ -281,8 +287,10 @@ function envLabel(cfg: DeployConfig, env: EnvName): string {
|
|
|
281
287
|
|
|
282
288
|
/**
|
|
283
289
|
* Print the exact commands `init` would run, without touching the cloud
|
|
284
|
-
* (`--print`).
|
|
285
|
-
*
|
|
290
|
+
* (`--print`). init stands up *both* stacks, so both `aws cloudformation deploy`
|
|
291
|
+
* commands are shown: the tier-0 bootstrap stack (created only when missing) and
|
|
292
|
+
* the per-app backend. The artifacts bucket is read from an existing bootstrap
|
|
293
|
+
* stack when the AWS CLI is available, else shown as a placeholder.
|
|
286
294
|
*/
|
|
287
295
|
function printPlan(cfg: DeployConfig, envs: EnvName[]): void {
|
|
288
296
|
const bucket = awsAvailable()
|
|
@@ -295,16 +303,15 @@ function printPlan(cfg: DeployConfig, envs: EnvName[]): void {
|
|
|
295
303
|
const withBucket = mergeConfig(cfg, {
|
|
296
304
|
artifactsBucket: bucket ?? "<ArtifactsBucketName from bootstrap stack>",
|
|
297
305
|
});
|
|
306
|
+
const bootstrapTemplate = resolveDeployAsset("bootstrap.yaml") ?? undefined;
|
|
298
307
|
const appTemplate = resolveDeployAsset("app.yaml") ?? undefined;
|
|
299
308
|
console.log(`\n Deployment plan (${envs.join(" + ")} @ ${cfg.region})\n`);
|
|
300
309
|
console.log(
|
|
301
|
-
` ──
|
|
302
|
-
);
|
|
303
|
-
console.log(
|
|
304
|
-
` Create it once per account+region (init does not): rastack bootstrap --region ${cfg.region}\n`,
|
|
310
|
+
` ── 1. Tier-0 bootstrap stack (${bootstrapStackName(cfg)}) — created only when missing ──`,
|
|
305
311
|
);
|
|
312
|
+
console.log("\n " + bootstrapStackCommand(cfg, false, bootstrapTemplate) + "\n");
|
|
306
313
|
console.log(
|
|
307
|
-
" ──
|
|
314
|
+
" ── 2. Per-app backend (app.yaml) — one stack per environment ──",
|
|
308
315
|
);
|
|
309
316
|
for (const env of envs) {
|
|
310
317
|
const e = resolveEnvironment(withBucket, env);
|
|
@@ -330,9 +337,12 @@ function printPlan(cfg: DeployConfig, envs: EnvName[]): void {
|
|
|
330
337
|
}
|
|
331
338
|
|
|
332
339
|
/**
|
|
333
|
-
* Stand the infrastructure up:
|
|
334
|
-
*
|
|
335
|
-
*
|
|
340
|
+
* Stand the infrastructure up: verify the AWS CLI is logged in, ensure the
|
|
341
|
+
* tier-0 bootstrap stack (creating it when missing), upload the Lambda
|
|
342
|
+
* placeholder, deploy app.yaml, and read the Outputs back into the config.
|
|
343
|
+
* `init` is the single command that stands up *everything* — there is no
|
|
344
|
+
* separate manual bootstrap step. Returns the config enriched with the resolved
|
|
345
|
+
* buckets + outputs.
|
|
336
346
|
*/
|
|
337
347
|
async function standUp(
|
|
338
348
|
cfg: DeployConfig,
|
|
@@ -347,33 +357,32 @@ async function standUp(
|
|
|
347
357
|
process.exit(1);
|
|
348
358
|
}
|
|
349
359
|
|
|
350
|
-
//
|
|
351
|
-
//
|
|
352
|
-
//
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
// so a missing bootstrap stack is a clear, actionable error, not an implicit
|
|
356
|
-
// account mutation buried inside an app deploy.
|
|
357
|
-
const artifactsBucket = stackOutput(
|
|
358
|
-
bootstrapStackName(cfg),
|
|
359
|
-
BOOTSTRAP_ARTIFACTS_OUTPUT,
|
|
360
|
-
cfg.region,
|
|
361
|
-
);
|
|
362
|
-
const templatesBucket = stackOutput(
|
|
363
|
-
bootstrapStackName(cfg),
|
|
364
|
-
BOOTSTRAP_TEMPLATES_OUTPUT,
|
|
365
|
-
cfg.region,
|
|
366
|
-
);
|
|
367
|
-
if (!artifactsBucket) {
|
|
360
|
+
// Verify the AWS CLI is actually logged in before creating any stacks, so a
|
|
361
|
+
// missing/expired credential fails fast with an actionable message rather than
|
|
362
|
+
// a cryptic `cloudformation deploy` error partway through.
|
|
363
|
+
const account = awsIdentity(cfg.region);
|
|
364
|
+
if (!account) {
|
|
368
365
|
console.error(
|
|
369
|
-
|
|
370
|
-
"
|
|
371
|
-
"
|
|
372
|
-
"
|
|
373
|
-
|
|
366
|
+
" ✗ the AWS CLI has no usable credentials — it is not logged in.\n" +
|
|
367
|
+
" Configure credentials, then re-run `rastack init`:\n" +
|
|
368
|
+
" aws configure # static access key / secret\n" +
|
|
369
|
+
" aws sso login # or an existing SSO session\n" +
|
|
370
|
+
" Or re-run with --print to see the commands, or --no-deploy to skip.",
|
|
374
371
|
);
|
|
375
372
|
process.exit(1);
|
|
376
373
|
}
|
|
374
|
+
console.log(` ✓ AWS account ${account} (${cfg.region})`);
|
|
375
|
+
|
|
376
|
+
// 1. Ensure the tier-0 bootstrap stack exists — the S3 buckets that host the
|
|
377
|
+
// templates + Lambda placeholder and the GitHub OIDC provider. init creates
|
|
378
|
+
// it when missing (idempotent for an existing one), so a single command
|
|
379
|
+
// takes an account+repo from nothing to a deployed backend.
|
|
380
|
+
const { artifactsBucket, templatesBucket } = await ensureBootstrapStack(
|
|
381
|
+
cfg,
|
|
382
|
+
).catch((err: Error) => {
|
|
383
|
+
console.error(`\n ✗ ${err.message}`);
|
|
384
|
+
process.exit(1);
|
|
385
|
+
});
|
|
377
386
|
console.log(` ✓ artifacts bucket: ${artifactsBucket}`);
|
|
378
387
|
|
|
379
388
|
// 2. Upload the placeholder the function is created from (shared by all envs).
|
|
@@ -389,11 +398,11 @@ async function standUp(
|
|
|
389
398
|
artifactsBucket,
|
|
390
399
|
templatesBucket: templatesBucket ?? undefined,
|
|
391
400
|
});
|
|
392
|
-
const appTemplate =
|
|
401
|
+
const appTemplate = await resolveTemplateFile("app.yaml");
|
|
393
402
|
if (!appTemplate) {
|
|
394
403
|
console.error(
|
|
395
|
-
" ✗ app.yaml template not found in the rastack package
|
|
396
|
-
" (`npm i -g rastack`)
|
|
404
|
+
" ✗ app.yaml template not found in the rastack package and could not be\n" +
|
|
405
|
+
" downloaded from reactapistack.com. Reinstall rastack (`npm i -g rastack`).",
|
|
397
406
|
);
|
|
398
407
|
process.exit(1);
|
|
399
408
|
}
|
|
@@ -623,7 +632,7 @@ async function main(argv: string[]): Promise<void> {
|
|
|
623
632
|
.map((env) => resolveEnvironment(cfg, env).stackName)
|
|
624
633
|
.join(" + ");
|
|
625
634
|
const ok = await confirm(
|
|
626
|
-
`Deploy ${stacks} to AWS ${cfg.region} now? (
|
|
635
|
+
`Deploy ${stacks} to AWS ${cfg.region} now? (creates the bootstrap stack if missing) [y/N]`,
|
|
627
636
|
);
|
|
628
637
|
if (!ok) {
|
|
629
638
|
console.log(
|