@aws/nx-plugin 1.1.1 → 1.3.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 +31 -29
- package/generators.json +16 -0
- package/migrations.json +6 -1
- package/package.json +1 -1
- package/src/agentcore-harness/generator.d.ts +1 -1
- package/src/greengrass/component/common.d.ts +60 -0
- package/src/greengrass/component/common.js +191 -0
- package/src/greengrass/component/common.js.map +1 -0
- package/src/greengrass/component/dependencies.d.ts +101 -0
- package/src/greengrass/component/dependencies.js +37 -0
- package/src/greengrass/component/dependencies.js.map +1 -0
- package/src/greengrass/component/generator.d.ts +174 -0
- package/src/greengrass/component/generator.js +32 -0
- package/src/greengrass/component/generator.js.map +1 -0
- package/src/greengrass/component/metadata.d.ts +2 -0
- package/src/greengrass/component/metadata.js +14 -0
- package/src/greengrass/component/metadata.js.map +1 -0
- package/src/greengrass/component/python/__snapshots__/generator.spec.ts.snap +450 -0
- package/src/greengrass/component/python/files/component/main.py.template +60 -0
- package/src/greengrass/component/python/files/component/recipe.yaml.template +30 -0
- package/src/greengrass/component/python/files/gdk/gdk-config.json.template +13 -0
- package/src/greengrass/component/python/files/tests/test___componentSnakeCase__.py.template +37 -0
- package/src/greengrass/component/python/generator.d.ts +15 -0
- package/src/greengrass/component/python/generator.js +192 -0
- package/src/greengrass/component/python/generator.js.map +1 -0
- package/src/greengrass/component/schema.d.js +6 -0
- package/src/greengrass/component/schema.d.js.map +1 -0
- package/src/greengrass/component/schema.d.ts +18 -0
- package/src/greengrass/component/schema.json +82 -0
- package/src/greengrass/component/typescript/__snapshots__/generator.spec.ts.snap +275 -0
- package/src/greengrass/component/typescript/files/component/recipe.yaml.template +41 -0
- package/src/greengrass/component/typescript/files/entrypoint/main.ts.template +58 -0
- package/src/greengrass/component/typescript/files/gdk/gdk-config.json.template +13 -0
- package/src/greengrass/component/typescript/files/scripts/vendor-crt-addon.ts.template +115 -0
- package/src/greengrass/component/typescript/generator.d.ts +16 -0
- package/src/greengrass/component/typescript/generator.js +194 -0
- package/src/greengrass/component/typescript/generator.js.map +1 -0
- package/src/infra/app/generator.d.ts +1 -1
- package/src/init/generator.d.ts +1 -1
- package/src/internal/test-matrix/generator.js +48 -1
- package/src/internal/test-matrix/generator.js.map +1 -1
- package/src/license/dependency-check/pre-approved.js +2 -1
- package/src/license/dependency-check/pre-approved.js.map +1 -1
- package/src/migrations/latest/ts-agent-guardrails-terraform-version-retention/metadata.json +3 -0
- package/src/migrations/latest/ts-agent-guardrails-terraform-version-retention/migration.d.ts +6 -0
- package/src/migrations/latest/ts-agent-guardrails-terraform-version-retention/migration.js +118 -0
- package/src/migrations/latest/ts-agent-guardrails-terraform-version-retention/migration.js.map +1 -0
- package/src/open-api/py-client/generator.d.ts +1 -1
- package/src/preset/__snapshots__/generator.spec.ts.snap +4 -0
- package/src/preset/generator.d.ts +1 -1
- package/src/py/agent/a2a-connection/generator.d.ts +1 -1
- package/src/py/agent/gateway-connection/generator.d.ts +1 -1
- package/src/py/agent/guardrails/__snapshots__/generator.spec.ts.snap +3813 -0
- package/src/py/agent/guardrails/files/langchain/guardrail.py.template +135 -0
- package/src/py/agent/guardrails/files/strands/guardrail.py.template +93 -0
- package/src/py/agent/guardrails/generator.d.ts +10 -0
- package/src/py/agent/guardrails/generator.js +175 -0
- package/src/py/agent/guardrails/generator.js.map +1 -0
- package/src/py/agent/guardrails/runtime-check/run_checks.py +250 -0
- package/src/py/agent/guardrails/schema.d.js +6 -0
- package/src/py/agent/guardrails/schema.d.js.map +1 -0
- package/src/py/agent/guardrails/schema.d.ts +9 -0
- package/src/py/agent/guardrails/schema.json +25 -0
- package/src/py/agent/mcp-connection/generator.d.ts +1 -1
- package/src/py/agent/react-connection/generator.d.ts +1 -1
- package/src/py/fast-api/react/generator.d.ts +1 -1
- package/src/py/project/generator.js +2 -0
- package/src/py/project/generator.js.map +1 -1
- package/src/sdk/greengrass.d.ts +6 -0
- package/src/sdk/greengrass.js +6 -0
- package/src/sdk/greengrass.js.map +1 -0
- package/src/sdk/py.d.ts +2 -0
- package/src/sdk/py.js +1 -0
- package/src/sdk/py.js.map +1 -1
- package/src/smithy/project/generator.d.ts +1 -1
- package/src/smithy/react-connection/generator.d.ts +1 -1
- package/src/terraform/project/generator.d.ts +1 -1
- package/src/trpc/react/generator.d.ts +1 -1
- package/src/ts/agent/a2a-connection/generator.d.ts +1 -1
- package/src/ts/agent/gateway-connection/generator.d.ts +1 -1
- package/src/ts/agent/guardrails/__snapshots__/generator.spec.ts.snap +54 -3
- package/src/ts/agent/guardrails/generator.js +2 -1
- package/src/ts/agent/guardrails/generator.js.map +1 -1
- package/src/ts/agent/mcp-connection/generator.d.ts +1 -1
- package/src/ts/agent/react-connection/generator.d.ts +1 -1
- package/src/ts/astro-docs/generator.d.ts +1 -1
- package/src/ts/dcr-proxy/generator.d.ts +1 -1
- package/src/ts/dynamodb/generator.d.ts +1 -1
- package/src/ts/lambda-function/generator.d.ts +1 -1
- package/src/ts/lib/generator.d.ts +1 -1
- package/src/ts/nx-generator/generator.d.ts +1 -1
- package/src/ts/nx-migration/generator.d.ts +1 -1
- package/src/ts/nx-plugin/generator.d.ts +1 -1
- package/src/ts/rdb/generator.d.ts +1 -1
- package/src/ts/react-website/agui/generator.d.ts +1 -1
- package/src/utils/__snapshots__/shared-greengrass-scripts.spec.ts.snap +1279 -0
- package/src/utils/files/common/scripts/src/greengrass/build-artifact.ts.template +236 -0
- package/src/utils/files/common/scripts/src/greengrass/component-logs.ts.template +48 -0
- package/src/utils/files/common/scripts/src/greengrass/deploy-local.ts.template +497 -0
- package/src/utils/files/common/scripts/src/greengrass/recipe-utils.ts.template +356 -0
- package/src/utils/files/common/scripts/src/greengrass/zip-writer.ts.template +121 -0
- package/src/utils/files/terraform/src/core/runtime-config/read/read.tf.template +3 -3
- package/src/utils/files/terraform/src/metrics/metrics.tf.template +1 -1
- package/src/utils/greengrass/constants.d.ts +58 -0
- package/src/utils/greengrass/constants.js +63 -0
- package/src/utils/greengrass/constants.js.map +1 -0
- package/src/utils/greengrass/naming.d.ts +46 -0
- package/src/utils/greengrass/naming.js +67 -0
- package/src/utils/greengrass/naming.js.map +1 -0
- package/src/utils/greengrass/recipe.d.ts +143 -0
- package/src/utils/greengrass/recipe.js +119 -0
- package/src/utils/greengrass/recipe.js.map +1 -0
- package/src/utils/greengrass-constructs/files/cdk/app/greengrass-component/__componentDirName__.ts.template +42 -0
- package/src/utils/greengrass-constructs/files/cdk/core/greengrass/artifact-bucket.ts.template +82 -0
- package/src/utils/greengrass-constructs/files/cdk/core/greengrass/component-version.ts.template +370 -0
- package/src/utils/greengrass-constructs/files/cdk/core/greengrass/deployment.ts.template +276 -0
- package/src/utils/greengrass-constructs/files/cdk/core/greengrass/index.ts.template +5 -0
- package/src/utils/greengrass-constructs/files/cdk/core/greengrass/next-version.ts.template +119 -0
- package/src/utils/greengrass-constructs/files/cdk/core/greengrass/recipe.ts.template +90 -0
- package/src/utils/greengrass-constructs/files/next-version/__nextVersionFileName__.template +195 -0
- package/src/utils/greengrass-constructs/files/terraform/app/greengrass-component/__componentDirName__/__componentDirName__.tf.template +26 -0
- package/src/utils/greengrass-constructs/files/terraform/core/greengrass/artifact-bucket/main.tf.template +204 -0
- package/src/utils/greengrass-constructs/files/terraform/core/greengrass/component-version/main.tf.template +156 -0
- package/src/utils/greengrass-constructs/files/terraform/core/greengrass/deployment/main.tf.template +186 -0
- package/src/utils/greengrass-constructs/greengrass-constructs.d.ts +57 -0
- package/src/utils/greengrass-constructs/greengrass-constructs.js +154 -0
- package/src/utils/greengrass-constructs/greengrass-constructs.js.map +1 -0
- package/src/utils/guardrail-constructs/files/terraform/app/guardrails/__nameKebabCase__/__nameKebabCase__-guardrail.tf.template +18 -1
- package/src/utils/guardrail-constructs/guardrail-constructs.d.ts +5 -0
- package/src/utils/guardrail-constructs/guardrail-constructs.js +8 -0
- package/src/utils/guardrail-constructs/guardrail-constructs.js.map +1 -1
- package/src/utils/guardrail-constructs/guardrail-wiring.d.ts +7 -0
- package/src/utils/guardrail-constructs/guardrail-wiring.js +6 -5
- package/src/utils/guardrail-constructs/guardrail-wiring.js.map +1 -1
- package/src/utils/nx.d.ts +15 -0
- package/src/utils/nx.js +32 -0
- package/src/utils/nx.js.map +1 -1
- package/src/utils/shared-greengrass-scripts.d.ts +21 -0
- package/src/utils/shared-greengrass-scripts.js +43 -0
- package/src/utils/shared-greengrass-scripts.js.map +1 -0
- package/src/utils/test/fake-aws/fake_aws.py +205 -0
- package/src/utils/test/python-dependencies.d.ts +6 -0
- package/src/utils/test/python-dependencies.js +19 -0
- package/src/utils/test/python-dependencies.js.map +1 -1
- package/src/utils/test/warm-python-cache.js +5 -1
- package/src/utils/test/warm-python-cache.js.map +1 -1
- package/src/utils/versions.d.ts +7 -0
- package/src/utils/versions.js +7 -0
- package/src/utils/versions.js.map +1 -1
- /package/src/{ts/agent/guardrails/files/agent → utils/guardrail-constructs/files/policy}/guardrail.json.template +0 -0
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
import { type SpawnSyncReturns, spawnSync } from 'node:child_process';
|
|
2
|
+
import {
|
|
3
|
+
closeSync,
|
|
4
|
+
cpSync,
|
|
5
|
+
fstatSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
openSync,
|
|
8
|
+
readSync,
|
|
9
|
+
readdirSync,
|
|
10
|
+
rmSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from 'node:fs';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import yaml from 'js-yaml';
|
|
15
|
+
import {
|
|
16
|
+
isNextVersionSentinel,
|
|
17
|
+
readResolvedRecipe,
|
|
18
|
+
resolveLocalNextVersion,
|
|
19
|
+
} from './recipe-utils<% if (esm) { %>.js<% } %>';
|
|
20
|
+
|
|
21
|
+
/*
|
|
22
|
+
* Usage: deploy-local.ts <dist>
|
|
23
|
+
*
|
|
24
|
+
* <dist> the component's dist root written by the "-artifact" target. Reads
|
|
25
|
+
* <dist>/greengrass-build/{recipes,artifacts}. A NEXT_* recipe stages
|
|
26
|
+
* the resolved recipe and artifact under greengrass-build/local/.
|
|
27
|
+
*
|
|
28
|
+
* Creates one local deployment via greengrass-cli, then polls the component's
|
|
29
|
+
* own lifecycle state until it settles. A deployment the nucleus reports as
|
|
30
|
+
* SUCCEEDED is not evidence the component runs, so this exits non-zero when the
|
|
31
|
+
* component ends up BROKEN or ERRORED, or never settles inside the timeout -
|
|
32
|
+
* printing the state it reached and the tail of its log.
|
|
33
|
+
*
|
|
34
|
+
* GREENGRASS_ROOT overrides the /greengrass/v2 nucleus root, and
|
|
35
|
+
* GREENGRASS_DEPLOY_TIMEOUT_SECONDS the 120 second poll bound.
|
|
36
|
+
*
|
|
37
|
+
* A NEXT_* ComponentVersion resolves on the device, not against the cloud
|
|
38
|
+
* registry: the next version above the highest of the installed version and
|
|
39
|
+
* the version directories the nucleus keeps under
|
|
40
|
+
* <GREENGRASS_ROOT>/packages/{artifacts,artifacts-unarchived}/<ComponentName>/.
|
|
41
|
+
*
|
|
42
|
+
* Re-deploying a concrete ComponentVersion already RUNNING on this device can
|
|
43
|
+
* read the outgoing instance's state, so bump that version in recipe.yaml to
|
|
44
|
+
* keep this check meaningful. A NEXT_* version resolves a new local version,
|
|
45
|
+
* so this warning does not apply to it.
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
const log = (msg: string): void => console.error(`[deploy-local] ${msg}`);
|
|
49
|
+
|
|
50
|
+
const POLL_INTERVAL_MS = 2000;
|
|
51
|
+
const DEFAULT_TIMEOUT_SECONDS = 120;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* How many consecutive checks a running state must survive before this script
|
|
55
|
+
* calls the deployment good. The nucleus reports RUNNING the moment it starts
|
|
56
|
+
* the Run lifecycle, so a component that dies seconds later - the usual shape
|
|
57
|
+
* of a missing dependency or a failed IPC connect - is RUNNING at the first
|
|
58
|
+
* check and BROKEN by the third. One check is not evidence.
|
|
59
|
+
*/
|
|
60
|
+
const REQUIRED_STABLE_CHECKS = 3;
|
|
61
|
+
|
|
62
|
+
const LOG_TAIL_LINES = 40;
|
|
63
|
+
/** Read from the END of the log file, so a large log cannot exhaust memory. */
|
|
64
|
+
const LOG_TAIL_BYTES = 64 * 1024;
|
|
65
|
+
/** Room for `component list` on a device carrying many components. */
|
|
66
|
+
const CLI_MAX_BUFFER_BYTES = 8 * 1024 * 1024;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* A component that started. FINISHED counts: a one-shot component that ran to
|
|
70
|
+
* completion is a success, and the nucleus never moves it to RUNNING. It is
|
|
71
|
+
* reported explicitly, because a generated component is not one-shot.
|
|
72
|
+
*/
|
|
73
|
+
const SETTLED_RUNNING_STATES = ['RUNNING', 'FINISHED'];
|
|
74
|
+
/** BROKEN follows three failed restarts, ERRORED a single failed one. */
|
|
75
|
+
const SETTLED_FAILED_STATES = ['BROKEN', 'ERRORED'];
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Log lines a component leaves when the device's `sudo` is sudo-rs, which
|
|
79
|
+
* accepts the nucleus's `sudo -E` and then discards the environment.
|
|
80
|
+
*/
|
|
81
|
+
const SUDO_RS_LOG_SIGNATURES = [
|
|
82
|
+
'AWS_GG_NUCLEUS_DOMAIN_SOCKET_FILEPATH_FOR_COMPONENT',
|
|
83
|
+
'preserving the entire environment is not supported',
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
interface ComponentStatus {
|
|
87
|
+
version?: string;
|
|
88
|
+
state?: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function usage(): never {
|
|
92
|
+
log('Usage: deploy-local.ts <dist>');
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const sleep = (ms: number): Promise<void> =>
|
|
97
|
+
new Promise((resolve) => setTimeout(resolve, ms));
|
|
98
|
+
|
|
99
|
+
/** Seconds to wait for the component to settle, from the environment. */
|
|
100
|
+
const resolveTimeoutSeconds = (): number => {
|
|
101
|
+
const raw = process.env.GREENGRASS_DEPLOY_TIMEOUT_SECONDS;
|
|
102
|
+
if (raw === undefined || raw.trim() === '') {
|
|
103
|
+
return DEFAULT_TIMEOUT_SECONDS;
|
|
104
|
+
}
|
|
105
|
+
const parsed = Number(raw);
|
|
106
|
+
// Rejects Infinity (which would poll forever) and anything <= 0 (which would
|
|
107
|
+
// skip polling and report a timeout the deployment never had).
|
|
108
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
109
|
+
log(
|
|
110
|
+
`Ignoring GREENGRASS_DEPLOY_TIMEOUT_SECONDS="${raw}": it must be a ` +
|
|
111
|
+
`finite number of seconds greater than 0. Waiting ` +
|
|
112
|
+
`${DEFAULT_TIMEOUT_SECONDS}s instead.`,
|
|
113
|
+
);
|
|
114
|
+
return DEFAULT_TIMEOUT_SECONDS;
|
|
115
|
+
}
|
|
116
|
+
return parsed;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Runs greengrass-cli, bounded: a wedged nucleus must not outlive the caller's
|
|
121
|
+
* deadline, and its output must not overflow the default 1 MB pipe buffer.
|
|
122
|
+
*/
|
|
123
|
+
const runCli = (
|
|
124
|
+
cli: string,
|
|
125
|
+
args: string[],
|
|
126
|
+
timeoutMs: number,
|
|
127
|
+
): SpawnSyncReturns<string> =>
|
|
128
|
+
spawnSync(cli, args, {
|
|
129
|
+
encoding: 'utf-8',
|
|
130
|
+
timeout: Math.max(1000, Math.trunc(timeoutMs)),
|
|
131
|
+
maxBuffer: CLI_MAX_BUFFER_BYTES,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Pulls one component's Version and State out of `greengrass-cli component
|
|
136
|
+
* list` output. Deliberately tolerant of indentation and of extra fields: the
|
|
137
|
+
* CLI's report is human-facing text with no documented layout, so anything
|
|
138
|
+
* unrecognised is skipped rather than parsed. First value wins for each field,
|
|
139
|
+
* so a later `State:` line inside the same block's serialized Configuration
|
|
140
|
+
* cannot overwrite the component's own state.
|
|
141
|
+
*/
|
|
142
|
+
const parseComponentStatus = (
|
|
143
|
+
output: string,
|
|
144
|
+
componentName: string,
|
|
145
|
+
): ComponentStatus | undefined => {
|
|
146
|
+
let found: ComponentStatus | undefined;
|
|
147
|
+
let inTargetBlock = false;
|
|
148
|
+
for (const line of output.split('\n')) {
|
|
149
|
+
const name = /^\s*Component Name:\s*(\S+)\s*$/.exec(line);
|
|
150
|
+
if (name) {
|
|
151
|
+
inTargetBlock = name[1] === componentName;
|
|
152
|
+
if (inTargetBlock) {
|
|
153
|
+
found = found ?? {};
|
|
154
|
+
}
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (!inTargetBlock || !found) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const version = /^\s*Version:\s*(\S+)\s*$/.exec(line);
|
|
161
|
+
if (version && found.version === undefined) {
|
|
162
|
+
found.version = version[1];
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const state = /^\s*State:\s*(\S+)\s*$/.exec(line);
|
|
166
|
+
if (state && found.state === undefined) {
|
|
167
|
+
found.state = state[1];
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return found;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
/** The nucleus package stores that keep one directory per component version. */
|
|
174
|
+
const LOCAL_VERSION_STORES = ['artifacts', 'artifacts-unarchived'];
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Versions this device has held for the component: the directory names under
|
|
178
|
+
* <GREENGRASS_ROOT>/packages/{artifacts,artifacts-unarchived}/<ComponentName>/.
|
|
179
|
+
* The recipe store (packages/recipes) is not read - it is root-only and names
|
|
180
|
+
* its files by a hashed component name. The nucleus garbage-collects unused
|
|
181
|
+
* versions, so this history is best-effort; the caller always counts the
|
|
182
|
+
* installed version too. A store that is missing or unreadable is skipped with
|
|
183
|
+
* one log line.
|
|
184
|
+
*/
|
|
185
|
+
const readLocalVersionHistory = (
|
|
186
|
+
greengrassRoot: string,
|
|
187
|
+
componentName: string,
|
|
188
|
+
): string[] => {
|
|
189
|
+
const versions: string[] = [];
|
|
190
|
+
for (const store of LOCAL_VERSION_STORES) {
|
|
191
|
+
const dir = join(greengrassRoot, 'packages', store, componentName);
|
|
192
|
+
try {
|
|
193
|
+
versions.push(
|
|
194
|
+
...readdirSync(dir, { withFileTypes: true })
|
|
195
|
+
.filter((entry) => entry.isDirectory())
|
|
196
|
+
.map((entry) => entry.name),
|
|
197
|
+
);
|
|
198
|
+
} catch (e) {
|
|
199
|
+
log(`Skipping ${dir}: ${(e as Error).message}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return versions;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Reads the last {@link LOG_TAIL_BYTES} of the component's log, returning
|
|
207
|
+
* undefined when it cannot be read.
|
|
208
|
+
*/
|
|
209
|
+
const readComponentLogTail = (logPath: string): string | undefined => {
|
|
210
|
+
let fd: number | undefined;
|
|
211
|
+
try {
|
|
212
|
+
fd = openSync(logPath, 'r');
|
|
213
|
+
const { size } = fstatSync(fd);
|
|
214
|
+
const length = Math.min(size, LOG_TAIL_BYTES);
|
|
215
|
+
const buffer = Buffer.alloc(length);
|
|
216
|
+
readSync(fd, buffer, 0, length, size - length);
|
|
217
|
+
const text = buffer.toString('utf-8');
|
|
218
|
+
// A truncated window starts mid-line, and possibly mid-character.
|
|
219
|
+
return size > length ? text.slice(text.indexOf('\n') + 1) : text;
|
|
220
|
+
} catch (e) {
|
|
221
|
+
log(
|
|
222
|
+
`Cannot read ${logPath}: ${(e as Error).message}. Run the component's ` +
|
|
223
|
+
'"-logs" target on this device to tail it.',
|
|
224
|
+
);
|
|
225
|
+
return undefined;
|
|
226
|
+
} finally {
|
|
227
|
+
if (fd !== undefined) {
|
|
228
|
+
closeSync(fd);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
/** Prints the last {@link LOG_TAIL_LINES} lines, plus any recognised remedy. */
|
|
234
|
+
const reportComponentLog = (
|
|
235
|
+
greengrassRoot: string,
|
|
236
|
+
componentName: string,
|
|
237
|
+
): void => {
|
|
238
|
+
const logPath = join(greengrassRoot, 'logs', `${componentName}.log`);
|
|
239
|
+
const content = readComponentLogTail(logPath);
|
|
240
|
+
if (content === undefined) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const lines = content.split('\n').filter((line) => line.length > 0);
|
|
244
|
+
const tail = lines.slice(-LOG_TAIL_LINES);
|
|
245
|
+
log(`Last ${tail.length} line(s) of ${logPath}:`);
|
|
246
|
+
for (const line of tail) {
|
|
247
|
+
console.error(line);
|
|
248
|
+
}
|
|
249
|
+
if (SUDO_RS_LOG_SIGNATURES.some((signature) => content.includes(signature))) {
|
|
250
|
+
log(
|
|
251
|
+
'This log matches sudo-rs discarding the component environment. The ' +
|
|
252
|
+
'nucleus launches every component through "sudo -E" to pass ' +
|
|
253
|
+
'AWS_GG_NUCLEUS_DOMAIN_SOCKET_FILEPATH_FOR_COMPONENT and SVCUID, and ' +
|
|
254
|
+
'sudo-rs (the default sudo on Ubuntu 25.10 and later) accepts -E and ' +
|
|
255
|
+
'then discards the environment, so an IPC component never receives ' +
|
|
256
|
+
'them. Classic sudo is already installed - select it and restart the ' +
|
|
257
|
+
'nucleus: sudo update-alternatives --set sudo /usr/bin/sudo.ws && ' +
|
|
258
|
+
'sudo systemctl restart greengrass',
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
/** Reports a component that started, then exits 0. */
|
|
264
|
+
const exitStarted = (
|
|
265
|
+
componentName: string,
|
|
266
|
+
componentVersion: string,
|
|
267
|
+
state: string,
|
|
268
|
+
): never => {
|
|
269
|
+
log(`${componentName}=${componentVersion} is ${state}.`);
|
|
270
|
+
if (state === 'FINISHED') {
|
|
271
|
+
log(
|
|
272
|
+
'FINISHED means the Run lifecycle exited 0 rather than staying up. A ' +
|
|
273
|
+
'generated component subscribes to IPC and runs until it is stopped, ' +
|
|
274
|
+
'so unless you made this one deliberately one-shot, read its log with ' +
|
|
275
|
+
"the component's \"-logs\" target.",
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
process.exit(0);
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
async function main(): Promise<void> {
|
|
282
|
+
const [dist] = process.argv.slice(2);
|
|
283
|
+
if (!dist) {
|
|
284
|
+
usage();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const greengrassRoot = process.env.GREENGRASS_ROOT || '/greengrass/v2';
|
|
288
|
+
let recipeDir = join(dist, 'greengrass-build', 'recipes');
|
|
289
|
+
let artifactDir = join(dist, 'greengrass-build', 'artifacts');
|
|
290
|
+
|
|
291
|
+
let componentName: string;
|
|
292
|
+
let componentVersion: string;
|
|
293
|
+
let recipe: Record<string, unknown>;
|
|
294
|
+
try {
|
|
295
|
+
({ componentName, componentVersion, recipe } = readResolvedRecipe(recipeDir));
|
|
296
|
+
} catch (e) {
|
|
297
|
+
log((e as Error).message);
|
|
298
|
+
process.exit(1);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const timeoutSeconds = resolveTimeoutSeconds();
|
|
303
|
+
const timeoutMs = timeoutSeconds * 1000;
|
|
304
|
+
const cli = join(greengrassRoot, 'bin', 'greengrass-cli');
|
|
305
|
+
|
|
306
|
+
// Read BEFORE submitting. The state read afterwards can only be attributed
|
|
307
|
+
// to this deployment if the device was not already running this exact
|
|
308
|
+
// version - `component list` reports no instance identity to tell the
|
|
309
|
+
// incoming instance from the outgoing one.
|
|
310
|
+
const before = runCli(cli, ['component', 'list'], timeoutMs);
|
|
311
|
+
const installed =
|
|
312
|
+
!before.error && before.status === 0
|
|
313
|
+
? parseComponentStatus(before.stdout ?? '', componentName)
|
|
314
|
+
: undefined;
|
|
315
|
+
|
|
316
|
+
if (isNextVersionSentinel(componentVersion)) {
|
|
317
|
+
const sentinel = componentVersion;
|
|
318
|
+
if (before.error || before.status !== 0) {
|
|
319
|
+
const detail = (before.error?.message ?? before.stderr ?? '').trim();
|
|
320
|
+
log(
|
|
321
|
+
`Cannot resolve ${sentinel} locally because "${cli} component list" failed${detail ? `: ${detail}` : ''}. Local NEXT_VERSION resolution needs the installed component version.`,
|
|
322
|
+
);
|
|
323
|
+
process.exit(1);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const history = readLocalVersionHistory(greengrassRoot, componentName);
|
|
328
|
+
let resolved: string;
|
|
329
|
+
try {
|
|
330
|
+
resolved = resolveLocalNextVersion(installed?.version, history, sentinel);
|
|
331
|
+
} catch (e) {
|
|
332
|
+
log((e as Error).message);
|
|
333
|
+
process.exit(1);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const localDir = join(dist, 'greengrass-build', 'local');
|
|
338
|
+
const localRecipeDir = join(localDir, 'recipes');
|
|
339
|
+
const localArtifactDir = join(
|
|
340
|
+
localDir,
|
|
341
|
+
'artifacts',
|
|
342
|
+
componentName,
|
|
343
|
+
resolved,
|
|
344
|
+
);
|
|
345
|
+
const sourceArtifactDir = join(
|
|
346
|
+
artifactDir,
|
|
347
|
+
componentName,
|
|
348
|
+
componentVersion,
|
|
349
|
+
);
|
|
350
|
+
try {
|
|
351
|
+
rmSync(localDir, { recursive: true, force: true });
|
|
352
|
+
mkdirSync(localRecipeDir, { recursive: true });
|
|
353
|
+
mkdirSync(localArtifactDir, { recursive: true });
|
|
354
|
+
writeFileSync(
|
|
355
|
+
join(localRecipeDir, `${componentName}-${resolved}.yaml`),
|
|
356
|
+
yaml.dump(
|
|
357
|
+
{ ...recipe, ComponentVersion: resolved },
|
|
358
|
+
{ lineWidth: -1 },
|
|
359
|
+
),
|
|
360
|
+
);
|
|
361
|
+
for (const artifact of readdirSync(sourceArtifactDir)) {
|
|
362
|
+
cpSync(
|
|
363
|
+
join(sourceArtifactDir, artifact),
|
|
364
|
+
join(localArtifactDir, artifact),
|
|
365
|
+
{ recursive: true },
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
} catch (e) {
|
|
369
|
+
log(
|
|
370
|
+
`Cannot stage ${sentinel} as ${resolved} for local deployment: ${(e as Error).message}`,
|
|
371
|
+
);
|
|
372
|
+
process.exit(1);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
log(
|
|
377
|
+
`Resolved ${sentinel} to ${resolved} (installed: ${installed?.version ?? 'none'}; package stores: ${history.length > 0 ? history.join(', ') : 'none'}).`,
|
|
378
|
+
);
|
|
379
|
+
componentVersion = resolved;
|
|
380
|
+
recipeDir = localRecipeDir;
|
|
381
|
+
artifactDir = join(localDir, 'artifacts');
|
|
382
|
+
} else if (installed?.version === componentVersion) {
|
|
383
|
+
log(
|
|
384
|
+
`This device already reports ${componentName}=${componentVersion}. The ` +
|
|
385
|
+
'state check below can then read the outgoing instance and pass while ' +
|
|
386
|
+
'the incoming one fails - bump ComponentVersion in recipe.yaml before ' +
|
|
387
|
+
're-deploying.',
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const args = [
|
|
392
|
+
'deployment',
|
|
393
|
+
'create',
|
|
394
|
+
'--recipeDir',
|
|
395
|
+
recipeDir,
|
|
396
|
+
'--artifactDir',
|
|
397
|
+
artifactDir,
|
|
398
|
+
'--merge',
|
|
399
|
+
`${componentName}=${componentVersion}`,
|
|
400
|
+
];
|
|
401
|
+
|
|
402
|
+
log(`Deploying ${componentName}=${componentVersion} via ${cli}`);
|
|
403
|
+
const result = runCli(cli, args, timeoutMs);
|
|
404
|
+
process.stdout.write(result.stdout ?? '');
|
|
405
|
+
process.stderr.write(result.stderr ?? '');
|
|
406
|
+
if (result.error) {
|
|
407
|
+
log(
|
|
408
|
+
(result.error as NodeJS.ErrnoException).code === 'ETIMEDOUT'
|
|
409
|
+
? `"${cli} deployment create" did not return within ${timeoutSeconds}s.`
|
|
410
|
+
: `Cannot run "${cli}": ${result.error.message}. Run this target on ` +
|
|
411
|
+
'the Greengrass core device, and set GREENGRASS_ROOT when the ' +
|
|
412
|
+
'nucleus is not installed at /greengrass/v2.',
|
|
413
|
+
);
|
|
414
|
+
process.exit(1);
|
|
415
|
+
}
|
|
416
|
+
if (result.status !== 0) {
|
|
417
|
+
log(`greengrass-cli exited with ${result.status ?? 'no status'}.`);
|
|
418
|
+
process.exit(result.status ?? 1);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
log(
|
|
422
|
+
`Deployment submitted. Waiting up to ${timeoutSeconds}s for ` +
|
|
423
|
+
`${componentName} to hold ${SETTLED_RUNNING_STATES.join(' or ')} across ` +
|
|
424
|
+
`${REQUIRED_STABLE_CHECKS} checks.`,
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
const deadline = Date.now() + timeoutMs;
|
|
428
|
+
let lastState: string | undefined;
|
|
429
|
+
let stableChecks = 0;
|
|
430
|
+
let listFailureReported = false;
|
|
431
|
+
while (Date.now() < deadline) {
|
|
432
|
+
const list = runCli(cli, ['component', 'list'], deadline - Date.now());
|
|
433
|
+
if (list.error || list.status !== 0) {
|
|
434
|
+
if (!listFailureReported) {
|
|
435
|
+
listFailureReported = true;
|
|
436
|
+
log(
|
|
437
|
+
`"${cli} component list" failed: ${(
|
|
438
|
+
list.error?.message ??
|
|
439
|
+
list.stderr ??
|
|
440
|
+
''
|
|
441
|
+
).trim()} - retrying until the timeout.`,
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
} else {
|
|
445
|
+
const listed = parseComponentStatus(list.stdout ?? '', componentName);
|
|
446
|
+
// Only a report of the version just deployed counts. An entry still
|
|
447
|
+
// carrying the previous version means the nucleus has not applied the
|
|
448
|
+
// deployment yet.
|
|
449
|
+
if (listed?.version === componentVersion && listed.state) {
|
|
450
|
+
lastState = listed.state;
|
|
451
|
+
if (SETTLED_FAILED_STATES.includes(lastState)) {
|
|
452
|
+
log(
|
|
453
|
+
`${componentName}=${componentVersion} is ${lastState} - the ` +
|
|
454
|
+
'component did not start. The local deployment itself can still ' +
|
|
455
|
+
'report success, so this state is the evidence that matters.',
|
|
456
|
+
);
|
|
457
|
+
reportComponentLog(greengrassRoot, componentName);
|
|
458
|
+
process.exit(1);
|
|
459
|
+
}
|
|
460
|
+
if (SETTLED_RUNNING_STATES.includes(lastState)) {
|
|
461
|
+
stableChecks += 1;
|
|
462
|
+
if (stableChecks >= REQUIRED_STABLE_CHECKS) {
|
|
463
|
+
exitStarted(componentName, componentVersion, lastState);
|
|
464
|
+
}
|
|
465
|
+
} else {
|
|
466
|
+
// STARTING, NEW, INSTALLED, STOPPING: on its way somewhere.
|
|
467
|
+
stableChecks = 0;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
await sleep(POLL_INTERVAL_MS);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// The deadline caught a component that started and never went bad. That is a
|
|
475
|
+
// pass: only the stability window is unfinished, and waiting longer is the
|
|
476
|
+
// caller's choice to make.
|
|
477
|
+
if (lastState !== undefined && SETTLED_RUNNING_STATES.includes(lastState)) {
|
|
478
|
+
log(
|
|
479
|
+
`The ${timeoutSeconds}s timeout ended after ${stableChecks} of ` +
|
|
480
|
+
`${REQUIRED_STABLE_CHECKS} stable checks. Raise ` +
|
|
481
|
+
'GREENGRASS_DEPLOY_TIMEOUT_SECONDS to confirm the state holds.',
|
|
482
|
+
);
|
|
483
|
+
exitStarted(componentName, componentVersion, lastState);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
log(
|
|
487
|
+
`${componentName}=${componentVersion} did not settle within ` +
|
|
488
|
+
`${timeoutSeconds}s - last reported state ${
|
|
489
|
+
lastState ?? 'none (the component was not listed at this version)'
|
|
490
|
+
}. Raise GREENGRASS_DEPLOY_TIMEOUT_SECONDS if this component is slow to ` +
|
|
491
|
+
'start.',
|
|
492
|
+
);
|
|
493
|
+
reportComponentLog(greengrassRoot, componentName);
|
|
494
|
+
process.exit(1);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
void main();
|