dataiku-sdk 4.0.0 → 4.2.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 +1 -0
- package/dist/build-metadata.json +1 -1
- package/dist/packages/types/src/index.d.ts +5 -1
- package/dist/packages/types/src/index.js +5 -1
- package/dist/src/cli/command-syntax.json +4136 -2756
- package/dist/src/cli/commands/code-env.js +5 -3
- package/dist/src/cli/commands/dataset.js +6 -4
- package/dist/src/cli/commands/future.js +7 -2
- package/dist/src/cli/commands/index.js +4 -0
- package/dist/src/cli/commands/job.js +12 -9
- package/dist/src/cli/commands/messaging-channel.d.ts +17 -0
- package/dist/src/cli/commands/messaging-channel.js +385 -0
- package/dist/src/cli/commands/project-deployer.js +33 -0
- package/dist/src/cli/commands/project.js +30 -0
- package/dist/src/cli/commands/recipe.js +23 -2
- package/dist/src/cli/commands/scenario.d.ts +6 -0
- package/dist/src/cli/commands/scenario.js +495 -17
- package/dist/src/cli/contract.js +20 -4
- package/dist/src/cli/env.d.ts +6 -0
- package/dist/src/cli/env.js +8 -0
- package/dist/src/cli/flags.js +34 -0
- package/dist/src/cli/helpers/cleanup.js +11 -0
- package/dist/src/cli/list-shapes.js +5 -0
- package/dist/src/cli/output.d.ts +2 -0
- package/dist/src/cli/output.js +24 -4
- package/dist/src/cli/plans.js +94 -2
- package/dist/src/cli/read-shapes.js +92 -1
- package/dist/src/cli/result-shapes.d.ts +19 -1
- package/dist/src/cli/result-shapes.js +228 -4
- package/dist/src/cli/selector-suggestions.d.ts +14 -0
- package/dist/src/cli/selector-suggestions.js +56 -0
- package/dist/src/cli/usage.d.ts +1 -1
- package/dist/src/cli/usage.js +36 -4
- package/dist/src/cli/version.d.ts +2 -2
- package/dist/src/cli/version.js +2 -2
- package/dist/src/cli.js +62 -13
- package/dist/src/client.d.ts +16 -4
- package/dist/src/client.js +45 -9
- package/dist/src/errors.d.ts +2 -0
- package/dist/src/errors.js +12 -2
- package/dist/src/generated/action-output-schemas.json +296 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/resources/bundles.d.ts +18 -0
- package/dist/src/resources/bundles.js +26 -0
- package/dist/src/resources/futures.d.ts +3 -0
- package/dist/src/resources/futures.js +5 -1
- package/dist/src/resources/messaging-channels.d.ts +83 -0
- package/dist/src/resources/messaging-channels.js +108 -0
- package/dist/src/resources/projects.d.ts +24 -0
- package/dist/src/resources/projects.js +36 -0
- package/dist/src/resources/recipe-create.d.ts +4 -1
- package/dist/src/resources/recipe-create.js +11 -2
- package/dist/src/resources/scenario-authoring.d.ts +52 -0
- package/dist/src/resources/scenario-authoring.js +216 -0
- package/dist/src/resources/scenarios.d.ts +128 -11
- package/dist/src/resources/scenarios.js +273 -43
- package/dist/src/resources/sql.js +3 -3
- package/dist/src/utils/messaging-channel-secrets.d.ts +17 -0
- package/dist/src/utils/messaging-channel-secrets.js +114 -0
- package/package.json +2 -1
- package/plugin.json +1 -1
- package/skills/dataiku-dss/SKILL.md +15 -16
- package/skills/dataiku-dss/references/discovery.md +2 -2
- package/skills/dataiku-dss/references/notifications.md +29 -0
- package/skills/dataiku-dss/references/troubleshooting.md +1 -0
|
@@ -1,8 +1,287 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolve, } from "node:path";
|
|
2
|
+
import { mailReporter, SCENARIO_TRIGGER_FREQUENCIES, selectScenarioListIndex, temporalTrigger, } from "../../resources/scenario-authoring.js";
|
|
3
|
+
import { messagingChannelSecretsFromBody, sanitizeMessagingChannelError, sanitizeMessagingChannelSecrets, } from "../../utils/messaging-channel-secrets.js";
|
|
4
|
+
import { asRecord, } from "../../utils/records.js";
|
|
5
|
+
import { json, jsonInput, num, parseBooleanOption, parseJsonObject, readInputText, splitCsvFlag, } from "../coerce.js";
|
|
2
6
|
import { executionMode, } from "../flags.js";
|
|
3
|
-
import { encodedProjectEndpoint, readIfExists, skipResult, } from "../output.js";
|
|
7
|
+
import { encodedProjectEndpoint, enqueueCliWarning, readIfExists, skipResult, } from "../output.js";
|
|
8
|
+
import { scenarioRunDiagnosis, shapeCommandResult, } from "../result-shapes.js";
|
|
4
9
|
import { commandUsage, withUsage, } from "../syntax.js";
|
|
5
10
|
import { requireArgs, UsageError, } from "../usage.js";
|
|
11
|
+
/**
|
|
12
|
+
* The recipe a failed job was running and the datasets it read, from one
|
|
13
|
+
* `job get` (its FAILED activity). Undefined when the job cannot be read or
|
|
14
|
+
* names no failed recipe: the diagnosis then stays job-level.
|
|
15
|
+
*/
|
|
16
|
+
async function failedActivity(c, jobId, projectKey) {
|
|
17
|
+
if (typeof jobId !== "string")
|
|
18
|
+
return undefined;
|
|
19
|
+
try {
|
|
20
|
+
const job = asRecord(await c.jobs.get(jobId, projectKey));
|
|
21
|
+
const activities = asRecord(asRecord(job?.["baseStatus"])?.["activities"]) ?? {};
|
|
22
|
+
for (const value of Object.values(activities)) {
|
|
23
|
+
const activity = asRecord(value);
|
|
24
|
+
if (activity?.["state"] !== "FAILED" || typeof activity["recipeName"] !== "string")
|
|
25
|
+
continue;
|
|
26
|
+
const sources = Array.isArray(activity["sources"]) ? activity["sources"] : [];
|
|
27
|
+
const inputs = sources.flatMap((source) => {
|
|
28
|
+
const id = asRecord(source)?.["id"];
|
|
29
|
+
if (typeof id !== "string")
|
|
30
|
+
return [];
|
|
31
|
+
// Same-project inputs are named bare; foreign ones keep PROJECT.name.
|
|
32
|
+
const dot = id.indexOf(".");
|
|
33
|
+
return [dot > 0 && id.slice(0, dot) === projectKey ? id.slice(dot + 1) : id,];
|
|
34
|
+
});
|
|
35
|
+
return { recipe: activity["recipeName"], inputs, };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// A diagnosis aid only: the run report itself is already in hand.
|
|
40
|
+
}
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
/** Diagnosis of a raw run report, located at the failed recipe when its job names one. */
|
|
44
|
+
async function locatedDiagnosis(c, details, projectKey) {
|
|
45
|
+
const plain = scenarioRunDiagnosis(details);
|
|
46
|
+
const runProject = asRecord(asRecord(asRecord(details)?.["scenarioRun"])?.["trigger"])?.["projectKey"];
|
|
47
|
+
const pk = projectKey ?? (typeof runProject === "string" ? runProject : undefined);
|
|
48
|
+
const activity = await failedActivity(c, plain.failure?.["jobId"], pk);
|
|
49
|
+
return activity ? scenarioRunDiagnosis(details, activity) : plain;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Run a scenario and wait; a failed (not timed-out) run gains the first
|
|
53
|
+
* failure, the recipe that failed, and the next commands. Extra reads happen
|
|
54
|
+
* only on failure; a read error leaves the wait result untouched. A cancelled
|
|
55
|
+
* trigger fire has no run to diagnose: it says why instead.
|
|
56
|
+
*/
|
|
57
|
+
async function runAndWaitWithFailure(c, scenarioId, options) {
|
|
58
|
+
const result = await c.scenarios.runAndWait(scenarioId, options);
|
|
59
|
+
if (result.cancelled === true) {
|
|
60
|
+
return {
|
|
61
|
+
...result,
|
|
62
|
+
failure: {
|
|
63
|
+
message: `DSS cancelled this run before it started (trigger run ${result.triggerRunId}): the scenario was already running, or another trigger fire replaced this one. No scenario run exists; wait for or abort the current run (scenario status, scenario abort), then run again.`,
|
|
64
|
+
},
|
|
65
|
+
next: [["dss", "scenario", "status", scenarioId, ...projectKeyArgs(options.projectKey),],],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (result.success || result.timedOut === true || !result.runId)
|
|
69
|
+
return result;
|
|
70
|
+
try {
|
|
71
|
+
const details = await c.scenarios.getRunDetails(scenarioId, result.runId, {
|
|
72
|
+
projectKey: options.projectKey,
|
|
73
|
+
});
|
|
74
|
+
return { ...result, ...await locatedDiagnosis(c, details, options.projectKey), };
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function projectKeyArgs(projectKey) {
|
|
81
|
+
return projectKey === undefined ? [] : ["--project-key", projectKey,];
|
|
82
|
+
}
|
|
83
|
+
/** `scenario run` result: the trigger fire, with the explicit pointer to the scenario run it will start. */
|
|
84
|
+
function triggerRunResult(fire, scenarioId, projectKey) {
|
|
85
|
+
if (fire.cancelled) {
|
|
86
|
+
return {
|
|
87
|
+
...fire,
|
|
88
|
+
hint: "DSS cancelled this trigger fire: the scenario was already running (or another fire replaced it), so no scenario run exists for this triggerRunId.",
|
|
89
|
+
next: [["dss", "scenario", "status", scenarioId, ...projectKeyArgs(projectKey),],],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
...fire,
|
|
94
|
+
hint: "triggerRunId identifies this trigger firing, not a scenario run: get-run and log need the scenario run id that get-run-for-trigger returns (or use --wait).",
|
|
95
|
+
next: [[
|
|
96
|
+
"dss",
|
|
97
|
+
"scenario",
|
|
98
|
+
"get-run-for-trigger",
|
|
99
|
+
scenarioId,
|
|
100
|
+
fire.triggerRunId,
|
|
101
|
+
...(fire.triggerId === "manual" ? [] : ["--trigger-id", fire.triggerId,]),
|
|
102
|
+
...projectKeyArgs(projectKey),
|
|
103
|
+
],],
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/** `scenario get-run-for-trigger` result: the fire's state and the commands that follow from it. */
|
|
107
|
+
function triggerRunStateResult(state, scenarioId, projectKey) {
|
|
108
|
+
const pk = projectKeyArgs(projectKey);
|
|
109
|
+
const next = state.runId !== undefined
|
|
110
|
+
? [
|
|
111
|
+
["dss", "scenario", "get-run", scenarioId, state.runId, ...pk,],
|
|
112
|
+
["dss", "scenario", "log", scenarioId, state.runId, ...pk,],
|
|
113
|
+
]
|
|
114
|
+
: state.state === "PENDING"
|
|
115
|
+
? [[
|
|
116
|
+
"dss",
|
|
117
|
+
"scenario",
|
|
118
|
+
"get-run-for-trigger",
|
|
119
|
+
scenarioId,
|
|
120
|
+
state.triggerRunId,
|
|
121
|
+
...(state.triggerId === "manual" ? [] : ["--trigger-id", state.triggerId,]),
|
|
122
|
+
...pk,
|
|
123
|
+
],]
|
|
124
|
+
: [["dss", "scenario", "status", scenarioId, ...pk,],];
|
|
125
|
+
return {
|
|
126
|
+
...state,
|
|
127
|
+
...(state.cancelled
|
|
128
|
+
? {
|
|
129
|
+
hint: "DSS cancelled this trigger fire (the scenario was already running, or another fire replaced it); no scenario run will exist.",
|
|
130
|
+
}
|
|
131
|
+
: {}),
|
|
132
|
+
next,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/** Run parameters from `--params JSON` or `--params-file PATH` (the POST /run body). */
|
|
136
|
+
export function scenarioRunParams(flags) {
|
|
137
|
+
const file = flags["params-file"];
|
|
138
|
+
if (flags["params"] !== undefined && file !== undefined) {
|
|
139
|
+
throw new UsageError("Pass --params or --params-file, not both.");
|
|
140
|
+
}
|
|
141
|
+
return typeof file === "string"
|
|
142
|
+
? parseJsonObject(readInputText(file, "--params-file"), "--params-file")
|
|
143
|
+
: json(flags["params"], "--params");
|
|
144
|
+
}
|
|
145
|
+
/** The trigger `trigger-add` appends: a raw `--data` object, or a schedule built from the --frequency flags. */
|
|
146
|
+
export function scenarioTriggerFromFlags(flags) {
|
|
147
|
+
const usage = commandUsage("scenario", "trigger-add");
|
|
148
|
+
const raw = jsonInput(flags);
|
|
149
|
+
if (raw !== undefined) {
|
|
150
|
+
if (flags["frequency"] !== undefined) {
|
|
151
|
+
throw new UsageError(`--frequency builds a schedule; pass it or --data/--data-file/--stdin, not both. Usage: ${usage}`);
|
|
152
|
+
}
|
|
153
|
+
return raw;
|
|
154
|
+
}
|
|
155
|
+
const frequency = SCENARIO_TRIGGER_FREQUENCIES.find((candidate) => candidate === flags["frequency"]);
|
|
156
|
+
if (frequency === undefined) {
|
|
157
|
+
throw new UsageError(`--frequency ${SCENARIO_TRIGGER_FREQUENCIES.join("|")} (or a raw trigger via --data) is required. Usage: ${usage}`);
|
|
158
|
+
}
|
|
159
|
+
const text = (name) => typeof flags[name] === "string" ? flags[name] : undefined;
|
|
160
|
+
return temporalTrigger({
|
|
161
|
+
frequency,
|
|
162
|
+
every: num(flags["every"], "--every"),
|
|
163
|
+
hour: num(flags["hour"], "--hour"),
|
|
164
|
+
minute: num(flags["minute"], "--minute"),
|
|
165
|
+
days: flags["days"] === undefined ? undefined : splitCsvFlag(flags["days"]),
|
|
166
|
+
dayOfMonth: num(flags["day-of-month"], "--day-of-month"),
|
|
167
|
+
runOn: text("run-on"),
|
|
168
|
+
startDate: text("start-date"),
|
|
169
|
+
timezone: text("timezone"),
|
|
170
|
+
name: text("name"),
|
|
171
|
+
active: parseBooleanOption(flags["active"], "--active"),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
/** The reporter `reporter-add` appends: a raw `--data` object, or a mail reporter built from the --channel flags. */
|
|
175
|
+
export function scenarioReporterFromFlags(flags) {
|
|
176
|
+
const usage = commandUsage("scenario", "reporter-add");
|
|
177
|
+
const raw = jsonInput(flags);
|
|
178
|
+
const mailFlags = ["channel", "recipient", "subject", "message", "condition", "phase",];
|
|
179
|
+
if (raw !== undefined) {
|
|
180
|
+
const clash = mailFlags.find((name) => flags[name] !== undefined);
|
|
181
|
+
if (clash !== undefined) {
|
|
182
|
+
throw new UsageError(`--${clash} builds a mail reporter; pass it or --data/--data-file/--stdin, not both. Usage: ${usage}`);
|
|
183
|
+
}
|
|
184
|
+
return raw;
|
|
185
|
+
}
|
|
186
|
+
const text = (name) => typeof flags[name] === "string" ? flags[name] : undefined;
|
|
187
|
+
const [channel, recipient, subject, message,] = ["channel", "recipient", "subject", "message",]
|
|
188
|
+
.map((name) => {
|
|
189
|
+
const value = text(name);
|
|
190
|
+
if (value === undefined) {
|
|
191
|
+
throw new UsageError(`--${name} is required (or pass a raw reporter via --data). Usage: ${usage}`);
|
|
192
|
+
}
|
|
193
|
+
return value;
|
|
194
|
+
});
|
|
195
|
+
return mailReporter({
|
|
196
|
+
channel,
|
|
197
|
+
recipient,
|
|
198
|
+
subject,
|
|
199
|
+
message,
|
|
200
|
+
condition: text("condition"),
|
|
201
|
+
phase: text("phase"),
|
|
202
|
+
name: text("name"),
|
|
203
|
+
active: parseBooleanOption(flags["active"], "--active"),
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
/** Reporter reduced to what a list needs: identity, phase, condition, channel, recipient, subject. */
|
|
207
|
+
function compactReporter(reporter, index) {
|
|
208
|
+
const fields = asRecord(reporter);
|
|
209
|
+
const messaging = asRecord(fields?.["messaging"]);
|
|
210
|
+
const configuration = asRecord(messaging?.["configuration"]);
|
|
211
|
+
return {
|
|
212
|
+
index,
|
|
213
|
+
id: fields?.["id"],
|
|
214
|
+
name: fields?.["name"],
|
|
215
|
+
active: fields?.["active"],
|
|
216
|
+
phase: fields?.["phase"],
|
|
217
|
+
runCondition: fields?.["runConditionEnabled"] === true ? fields["runCondition"] : undefined,
|
|
218
|
+
type: messaging?.["type"],
|
|
219
|
+
channel: configuration?.["channelId"],
|
|
220
|
+
recipient: configuration?.["recipient"],
|
|
221
|
+
subject: configuration?.["subject"],
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
/** Shared `trigger-add`/`reporter-add`: dry-run previews the appended element, otherwise it is written and verified. */
|
|
225
|
+
async function addScenarioItem(c, kind, scenarioId, item, flags) {
|
|
226
|
+
const projectKey = flags["project-key"];
|
|
227
|
+
const listAction = `${kind}-list`;
|
|
228
|
+
if (executionMode(flags).dryRun) {
|
|
229
|
+
const items = await c.scenarios.listItems(kind, scenarioId, { projectKey, });
|
|
230
|
+
return {
|
|
231
|
+
dryRun: true,
|
|
232
|
+
action: `${kind}-add`,
|
|
233
|
+
resource: "scenario",
|
|
234
|
+
id: scenarioId,
|
|
235
|
+
[kind]: sanitizeMessagingChannelSecrets(item),
|
|
236
|
+
index: items.length,
|
|
237
|
+
count: items.length + 1,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
// A raw reporter can carry a webhook URL or token that DSS may echo back in an error.
|
|
241
|
+
const secrets = messagingChannelSecretsFromBody(item);
|
|
242
|
+
const result = await c.scenarios.addItem(kind, scenarioId, item, { projectKey, }).catch((error) => {
|
|
243
|
+
throw sanitizeMessagingChannelError(error, secrets);
|
|
244
|
+
});
|
|
245
|
+
return {
|
|
246
|
+
added: kind,
|
|
247
|
+
resource: "scenario",
|
|
248
|
+
id: scenarioId,
|
|
249
|
+
index: result.index,
|
|
250
|
+
[kind]: sanitizeMessagingChannelSecrets(result.item),
|
|
251
|
+
count: result.count,
|
|
252
|
+
next: [["dss", "scenario", listAction, scenarioId, ...projectKeyArgs(projectKey),],],
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
/** Shared `trigger-remove`/`reporter-remove`: the element is chosen by --index or --name; the others stay. */
|
|
256
|
+
async function removeScenarioItem(c, kind, scenarioId, flags) {
|
|
257
|
+
const projectKey = flags["project-key"];
|
|
258
|
+
const selector = {
|
|
259
|
+
index: num(flags["index"], "--index"),
|
|
260
|
+
name: flags["name"],
|
|
261
|
+
};
|
|
262
|
+
if (executionMode(flags).dryRun) {
|
|
263
|
+
const items = await c.scenarios.listItems(kind, scenarioId, { projectKey, });
|
|
264
|
+
const index = selectScenarioListIndex(items, selector, kind);
|
|
265
|
+
return {
|
|
266
|
+
dryRun: true,
|
|
267
|
+
action: `${kind}-remove`,
|
|
268
|
+
resource: "scenario",
|
|
269
|
+
id: scenarioId,
|
|
270
|
+
index,
|
|
271
|
+
[kind]: sanitizeMessagingChannelSecrets(items[index]),
|
|
272
|
+
count: items.length - 1,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
const result = await c.scenarios.removeItem(kind, scenarioId, selector, { projectKey, });
|
|
276
|
+
return {
|
|
277
|
+
removed: kind,
|
|
278
|
+
resource: "scenario",
|
|
279
|
+
id: scenarioId,
|
|
280
|
+
index: result.index,
|
|
281
|
+
[kind]: sanitizeMessagingChannelSecrets(result.item),
|
|
282
|
+
count: result.count,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
6
285
|
export const scenarioCommands = withUsage("scenario", {
|
|
7
286
|
list: {
|
|
8
287
|
handler: (c, _a, f) => c.scenarios.list(f["project-key"]),
|
|
@@ -21,6 +300,7 @@ export const scenarioCommands = withUsage("scenario", {
|
|
|
21
300
|
handler: async (c, a, f) => {
|
|
22
301
|
requireArgs(a, 1, commandUsage("scenario", "run"));
|
|
23
302
|
const pk = f["project-key"];
|
|
303
|
+
const params = scenarioRunParams(f);
|
|
24
304
|
const options = {
|
|
25
305
|
pollIntervalMs: num(f["poll-interval"], "--poll-interval"),
|
|
26
306
|
timeoutMs: num(f["timeout"], "--timeout"),
|
|
@@ -32,22 +312,28 @@ export const scenarioCommands = withUsage("scenario", {
|
|
|
32
312
|
resource: "scenario",
|
|
33
313
|
id: a[0],
|
|
34
314
|
...options,
|
|
315
|
+
...(params === undefined ? {} : { params, }),
|
|
35
316
|
endpoint: encodedProjectEndpoint(c, pk, `/scenarios/${encodeURIComponent(a[0])}/run/`),
|
|
36
317
|
method: "POST",
|
|
37
318
|
};
|
|
38
319
|
}
|
|
39
320
|
if (f["wait"] === true) {
|
|
40
|
-
return c
|
|
321
|
+
return runAndWaitWithFailure(c, a[0], { ...options, params, projectKey: pk, });
|
|
41
322
|
}
|
|
42
|
-
return c.scenarios.run(a[0], pk);
|
|
323
|
+
return triggerRunResult(await c.scenarios.run(a[0], { params, projectKey: pk, }), a[0], pk);
|
|
43
324
|
},
|
|
44
|
-
description: "Trigger a scenario run,
|
|
45
|
-
examples: [
|
|
325
|
+
description: "Trigger a scenario run; --wait waits for it. --params JSON/--params-file PATH are the run parameters. Without --wait the result is the trigger fire: triggerRunId is NOT a scenario run id, resolve it with get-run-for-trigger (see `next`). cancelled: true means the scenario was already running; no run starts.",
|
|
326
|
+
examples: [
|
|
327
|
+
"dss scenario run my_scenario",
|
|
328
|
+
"dss scenario run my_scenario --wait",
|
|
329
|
+
'dss scenario run my_scenario --params \'{"region":"EU"}\' --wait',
|
|
330
|
+
],
|
|
46
331
|
},
|
|
47
332
|
"run-and-wait": {
|
|
48
333
|
handler: async (c, a, f) => {
|
|
49
334
|
requireArgs(a, 1, commandUsage("scenario", "run-and-wait"));
|
|
50
335
|
const pk = f["project-key"];
|
|
336
|
+
const params = scenarioRunParams(f);
|
|
51
337
|
const options = {
|
|
52
338
|
pollIntervalMs: num(f["poll-interval"], "--poll-interval"),
|
|
53
339
|
timeoutMs: num(f["timeout"], "--timeout"),
|
|
@@ -59,16 +345,18 @@ export const scenarioCommands = withUsage("scenario", {
|
|
|
59
345
|
resource: "scenario",
|
|
60
346
|
id: a[0],
|
|
61
347
|
...options,
|
|
348
|
+
...(params === undefined ? {} : { params, }),
|
|
62
349
|
endpoint: encodedProjectEndpoint(c, pk, `/scenarios/${encodeURIComponent(a[0])}/run/`),
|
|
63
350
|
method: "POST",
|
|
64
351
|
};
|
|
65
352
|
}
|
|
66
|
-
return c
|
|
353
|
+
return runAndWaitWithFailure(c, a[0], { ...options, params, projectKey: pk, });
|
|
67
354
|
},
|
|
68
|
-
description: "Run a scenario and wait for completion.",
|
|
355
|
+
description: "Run a scenario and wait for completion (--params JSON / --params-file PATH as run parameters). Outcome CANCELLED (exit 4) means DSS cancelled the trigger fire because the scenario was already running; TIMEOUT means the wait ran out while the run continues.",
|
|
69
356
|
examples: [
|
|
70
357
|
"dss scenario run-and-wait my_scenario",
|
|
71
358
|
"dss scenario run-and-wait my_scenario --timeout 300000",
|
|
359
|
+
"dss scenario run-and-wait my_scenario --params-file params.json",
|
|
72
360
|
],
|
|
73
361
|
},
|
|
74
362
|
status: {
|
|
@@ -101,11 +389,13 @@ export const scenarioCommands = withUsage("scenario", {
|
|
|
101
389
|
handler: async (c, a, f) => {
|
|
102
390
|
requireArgs(a, 2, commandUsage("scenario", "create"));
|
|
103
391
|
const pk = f["project-key"];
|
|
392
|
+
const data = jsonInput(f);
|
|
104
393
|
const payload = {
|
|
105
394
|
scenarioId: a[0],
|
|
106
395
|
name: a[1],
|
|
107
396
|
scenarioType: f["type"],
|
|
108
397
|
projectKey: pk,
|
|
398
|
+
...(data === undefined ? {} : { data, }),
|
|
109
399
|
};
|
|
110
400
|
if (f["if-not-exists"] === true || executionMode(f).dryRun) {
|
|
111
401
|
const list = await c.scenarios.list(pk);
|
|
@@ -126,14 +416,16 @@ export const scenarioCommands = withUsage("scenario", {
|
|
|
126
416
|
}
|
|
127
417
|
await c.scenarios.create(a[0], a[1], {
|
|
128
418
|
scenarioType: payload.scenarioType,
|
|
419
|
+
data,
|
|
129
420
|
projectKey: pk,
|
|
130
421
|
});
|
|
131
422
|
return { created: a[0], name: a[1], resource: "scenario", };
|
|
132
423
|
},
|
|
133
|
-
description: "Create a new scenario.",
|
|
424
|
+
description: "Create a new scenario. --data/--data-file/--stdin merge extra top-level fields into the create body (e.g. triggers, reporters, params.steps, runAsUser).",
|
|
134
425
|
examples: [
|
|
135
426
|
'dss scenario create my_scenario "My Scenario"',
|
|
136
427
|
'dss scenario create my_scenario "My Scenario" --type custom_python --dry-run',
|
|
428
|
+
'dss scenario create nightly "Nightly" --data \'{"triggers":[{"active":true,"type":"temporal","params":{"frequency":"Daily","hour":2,"minute":0,"startingFrom":"2026-01-01T00:00:00.000+0000","repeatFrequency":1,"timezone":"SERVER"}}]}\' --dry-run',
|
|
137
429
|
],
|
|
138
430
|
},
|
|
139
431
|
update: {
|
|
@@ -206,20 +498,31 @@ export const scenarioCommands = withUsage("scenario", {
|
|
|
206
498
|
requireArgs(a, 1, commandUsage("scenario", "last-runs"));
|
|
207
499
|
return c.scenarios.getLastRuns(a[0], {
|
|
208
500
|
limit: num(f["limit"], "--limit"),
|
|
501
|
+
onlyFinished: f["only-finished"] === true,
|
|
209
502
|
projectKey: f["project-key"],
|
|
210
503
|
});
|
|
211
504
|
},
|
|
212
|
-
description: "List
|
|
213
|
-
examples: [
|
|
505
|
+
description: "List a scenario's most recent runs, newest first (compact: run id, outcome, timing, trigger, failure; --full for DSS objects). --only-finished skips in-progress (and, observed live, aborted) runs: `--only-finished --limit 1` is the last finished run; read its outcome.",
|
|
506
|
+
examples: [
|
|
507
|
+
"dss scenario last-runs my_scenario --limit 10",
|
|
508
|
+
"dss scenario last-runs my_scenario --only-finished --limit 1",
|
|
509
|
+
],
|
|
214
510
|
},
|
|
215
511
|
"get-run": {
|
|
216
|
-
handler: (c, a, f) => {
|
|
512
|
+
handler: async (c, a, f) => {
|
|
217
513
|
requireArgs(a, 2, commandUsage("scenario", "get-run"));
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
514
|
+
const projectKey = f["project-key"];
|
|
515
|
+
const details = await c.scenarios.getRunDetails(a[0], a[1], { projectKey, });
|
|
516
|
+
if (f["full"] === true || typeof f["fields"] === "string")
|
|
517
|
+
return details;
|
|
518
|
+
// Compact view, located at the failed recipe (one extra job read, only on failure).
|
|
519
|
+
// The shaped object has no `scenarioRun`, so the dispatcher's shaping passes it through.
|
|
520
|
+
const shaped = asRecord(shapeCommandResult("scenario", "get-run", details, f));
|
|
521
|
+
if (!shaped?.["failure"])
|
|
522
|
+
return shaped ?? details;
|
|
523
|
+
return { ...shaped, ...await locatedDiagnosis(c, details, projectKey), };
|
|
221
524
|
},
|
|
222
|
-
description: "Get
|
|
525
|
+
description: "Get a scenario run: outcome, timing, trigger, per-step datasets and jobs, plus `failure` {stepId, jobId, dataset, recipe, inputs, message} and `next` argv (recipe code, input schemas, job errors) for the first failure (compact by default; --full returns the raw DSS run report).",
|
|
223
526
|
examples: ["dss scenario get-run my_scenario 2024-09-01-01-02-03-123",],
|
|
224
527
|
},
|
|
225
528
|
log: {
|
|
@@ -233,7 +536,6 @@ export const scenarioCommands = withUsage("scenario", {
|
|
|
233
536
|
const outputFile = f["output"]
|
|
234
537
|
?? f["output-file"];
|
|
235
538
|
if (outputFile) {
|
|
236
|
-
const { resolve, } = await import("node:path");
|
|
237
539
|
const outputPath = resolve(outputFile);
|
|
238
540
|
await Bun.write(outputPath, result.text);
|
|
239
541
|
return { path: outputPath, truncated: result.truncated, };
|
|
@@ -309,4 +611,180 @@ export const scenarioCommands = withUsage("scenario", {
|
|
|
309
611
|
"dss scenario active-set my_scenario true --dry-run",
|
|
310
612
|
],
|
|
311
613
|
},
|
|
614
|
+
"get-run-for-trigger": {
|
|
615
|
+
handler: async (c, a, f) => {
|
|
616
|
+
requireArgs(a, 2, commandUsage("scenario", "get-run-for-trigger"));
|
|
617
|
+
const pk = f["project-key"];
|
|
618
|
+
const state = await c.scenarios.getRunForTrigger(a[0], a[1], {
|
|
619
|
+
triggerId: f["trigger-id"],
|
|
620
|
+
projectKey: pk,
|
|
621
|
+
});
|
|
622
|
+
return triggerRunStateResult(state, a[0], pk);
|
|
623
|
+
},
|
|
624
|
+
description: "Resolve a trigger run (the triggerRunId `scenario run` returns) to its scenario run: state PENDING (no run yet; repeat), RUNNING, FINISHED (outcome), or CANCELLED (DSS cancelled the fire because the scenario was already running; no run will exist). `runId` is what get-run and log take.",
|
|
625
|
+
examples: ["dss scenario get-run-for-trigger my_scenario 2024-09-01-01-02-03-123",],
|
|
626
|
+
},
|
|
627
|
+
"runs-by-date": {
|
|
628
|
+
handler: (c, a, f) => {
|
|
629
|
+
requireArgs(a, 1, commandUsage("scenario", "runs-by-date"));
|
|
630
|
+
const from = f["from"];
|
|
631
|
+
if (typeof from !== "string") {
|
|
632
|
+
throw new UsageError(`--from is required. Usage: ${commandUsage("scenario", "runs-by-date")}`);
|
|
633
|
+
}
|
|
634
|
+
return c.scenarios.getRunsByDate(a[0], {
|
|
635
|
+
from,
|
|
636
|
+
to: f["to"],
|
|
637
|
+
projectKey: f["project-key"],
|
|
638
|
+
});
|
|
639
|
+
},
|
|
640
|
+
description: "List the runs of a scenario that started from --from (inclusive) to --to (exclusive: DSS compares the start of that day, so --from D --to D is empty), each YYYY-MM-DD or an ISO date-time (time dropped). --to defaults to tomorrow, i.e. up to now. Compact like last-runs; --full returns the DSS run objects.",
|
|
641
|
+
examples: ["dss scenario runs-by-date my_scenario --from 2026-10-01 --to 2026-10-31",],
|
|
642
|
+
},
|
|
643
|
+
"run-report": {
|
|
644
|
+
handler: async (c, a, f) => {
|
|
645
|
+
requireArgs(a, 2, commandUsage("scenario", "run-report"));
|
|
646
|
+
const result = await c.scenarios.getRunReport(a[0], a[1], {
|
|
647
|
+
stepId: f["step-id"],
|
|
648
|
+
maxBytes: num(f["max-bytes"], "--max-bytes"),
|
|
649
|
+
projectKey: f["project-key"],
|
|
650
|
+
});
|
|
651
|
+
const outputFile = f["output"]
|
|
652
|
+
?? f["output-file"];
|
|
653
|
+
if (outputFile) {
|
|
654
|
+
const outputPath = resolve(outputFile);
|
|
655
|
+
await Bun.write(outputPath, result.text);
|
|
656
|
+
return { path: outputPath, truncated: result.truncated, };
|
|
657
|
+
}
|
|
658
|
+
return result.text;
|
|
659
|
+
},
|
|
660
|
+
description: "Get the JUnit XML report of a test scenario run (or of one step via --step-id). Only scenarios marked as test (markedAsTest) have reports. Byte-bounded at --max-bytes (default 1 MiB); --output PATH writes it to a file (stdout returns the path).",
|
|
661
|
+
examples: [
|
|
662
|
+
"dss scenario run-report my_test_scenario RUN_ID --output report.xml",
|
|
663
|
+
"dss scenario run-report my_test_scenario RUN_ID --step-id run_tests",
|
|
664
|
+
],
|
|
665
|
+
},
|
|
666
|
+
"trigger-list": {
|
|
667
|
+
handler: async (c, a, f) => {
|
|
668
|
+
requireArgs(a, 1, commandUsage("scenario", "trigger-list"));
|
|
669
|
+
const triggers = await c.scenarios.listItems("trigger", a[0], {
|
|
670
|
+
projectKey: f["project-key"],
|
|
671
|
+
});
|
|
672
|
+
return triggers.map((trigger, index) => ({ index, ...asRecord(trigger), }));
|
|
673
|
+
},
|
|
674
|
+
description: "List the automatic triggers of a scenario with their 0-based index (the --index of trigger-remove). Triggers only fire while the scenario is active (scenario active-set).",
|
|
675
|
+
examples: ["dss scenario trigger-list my_scenario",],
|
|
676
|
+
},
|
|
677
|
+
"trigger-add": {
|
|
678
|
+
handler: (c, a, f) => {
|
|
679
|
+
requireArgs(a, 1, commandUsage("scenario", "trigger-add"));
|
|
680
|
+
return addScenarioItem(c, "trigger", a[0], scenarioTriggerFromFlags(f), f);
|
|
681
|
+
},
|
|
682
|
+
description: "Append a trigger, keeping the others. Schedule: --frequency (dataikuapi builders) with --every, --hour, --minute, --days (weekly), --day-of-month/--run-on (monthly), --start-date, --timezone (default SERVER), --name, --active. Other trigger types: JSON via --data.",
|
|
683
|
+
examples: [
|
|
684
|
+
"dss scenario trigger-add my_scenario --frequency daily --hour 6 --minute 30",
|
|
685
|
+
"dss scenario trigger-add my_scenario --frequency weekly --days Monday,Friday --hour 8",
|
|
686
|
+
"dss scenario trigger-add my_scenario --frequency monthly --day-of-month 5 --run-on ON_THE_DAY --dry-run",
|
|
687
|
+
'dss scenario trigger-add my_scenario --data \'{"type":"temporal","active":true,"params":{"frequency":"Minutely","repeatFrequency":15}}\'',
|
|
688
|
+
],
|
|
689
|
+
},
|
|
690
|
+
"trigger-remove": {
|
|
691
|
+
handler: (c, a, f) => {
|
|
692
|
+
requireArgs(a, 1, commandUsage("scenario", "trigger-remove"));
|
|
693
|
+
return removeScenarioItem(c, "trigger", a[0], f);
|
|
694
|
+
},
|
|
695
|
+
description: "Remove one trigger, selected by --index (from trigger-list) or --name, keeping the others.",
|
|
696
|
+
examples: [
|
|
697
|
+
"dss scenario trigger-remove my_scenario --index 0",
|
|
698
|
+
"dss scenario trigger-remove my_scenario --name nightly --dry-run",
|
|
699
|
+
],
|
|
700
|
+
},
|
|
701
|
+
"reporter-list": {
|
|
702
|
+
handler: async (c, a, f) => {
|
|
703
|
+
requireArgs(a, 1, commandUsage("scenario", "reporter-list"));
|
|
704
|
+
const reporters = await c.scenarios.listItems("reporter", a[0], {
|
|
705
|
+
projectKey: f["project-key"],
|
|
706
|
+
});
|
|
707
|
+
// Channel credentials live on the channel, but reporters of other types may carry their own.
|
|
708
|
+
return reporters.map((reporter, index) => f["full"] === true
|
|
709
|
+
? { index, ...asRecord(sanitizeMessagingChannelSecrets(reporter)), }
|
|
710
|
+
: compactReporter(reporter, index));
|
|
711
|
+
},
|
|
712
|
+
description: "List the reporters (notifications) of a scenario with their 0-based index: id, phase, active, channel, recipient, subject (compact by default; --full returns the DSS objects with credential-looking values redacted).",
|
|
713
|
+
examples: ["dss scenario reporter-list my_scenario",],
|
|
714
|
+
},
|
|
715
|
+
"reporter-add": {
|
|
716
|
+
handler: async (c, a, f) => {
|
|
717
|
+
requireArgs(a, 1, commandUsage("scenario", "reporter-add"));
|
|
718
|
+
const reporter = scenarioReporterFromFlags(f);
|
|
719
|
+
// DSS saves a reporter with any channel id; an unknown one never sends. Warn, don't fail:
|
|
720
|
+
// a channel may be hidden from this key yet usable by the scenario's run-as user.
|
|
721
|
+
const channel = f["channel"];
|
|
722
|
+
if (typeof channel === "string") {
|
|
723
|
+
const known = (await c.messagingChannels.list()).map((entry) => entry.id);
|
|
724
|
+
if (!known.includes(channel)) {
|
|
725
|
+
enqueueCliWarning({
|
|
726
|
+
code: "reporter_channel_unknown",
|
|
727
|
+
channel,
|
|
728
|
+
knownChannels: known,
|
|
729
|
+
hint: "No messaging channel with this id is visible: the reporter will not send. Check `dss messaging-channel list`.",
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
return addScenarioItem(c, "reporter", a[0], reporter, f);
|
|
734
|
+
},
|
|
735
|
+
description: "Append a reporter, keeping the others. Mail: --channel ID (messaging-channel list), --recipient, --subject, --message (required), --condition EXPR, --phase START|END (default END). Slack, Teams and other channel types: pass the reporter JSON with --data.",
|
|
736
|
+
examples: [
|
|
737
|
+
'dss scenario reporter-add my_scenario --channel smtp --recipient ops@example.com --subject "Scenario done" --message "See DSS"',
|
|
738
|
+
"dss scenario reporter-add my_scenario --channel smtp --recipient ops@example.com --subject Failed --message Failed --condition \"outcome == 'FAILED'\" --dry-run",
|
|
739
|
+
],
|
|
740
|
+
},
|
|
741
|
+
"reporter-remove": {
|
|
742
|
+
handler: (c, a, f) => {
|
|
743
|
+
requireArgs(a, 1, commandUsage("scenario", "reporter-remove"));
|
|
744
|
+
return removeScenarioItem(c, "reporter", a[0], f);
|
|
745
|
+
},
|
|
746
|
+
description: "Remove one reporter, selected by --index (from reporter-list) or --name, keeping the others.",
|
|
747
|
+
examples: ["dss scenario reporter-remove my_scenario --index 0",],
|
|
748
|
+
},
|
|
749
|
+
"run-as-set": {
|
|
750
|
+
handler: async (c, a, f) => {
|
|
751
|
+
requireArgs(a, 1, commandUsage("scenario", "run-as-set"));
|
|
752
|
+
const user = f["user"];
|
|
753
|
+
const clear = f["clear"] === true;
|
|
754
|
+
if ((typeof user === "string") === clear) {
|
|
755
|
+
throw new UsageError(`Pass exactly one of --user LOGIN or --clear. Usage: ${commandUsage("scenario", "run-as-set")}`);
|
|
756
|
+
}
|
|
757
|
+
const runAsUser = typeof user === "string" ? user : null;
|
|
758
|
+
const pk = f["project-key"];
|
|
759
|
+
if (executionMode(f).dryRun) {
|
|
760
|
+
const current = asRecord(await c.scenarios.get(a[0], { projectKey: pk, }));
|
|
761
|
+
return {
|
|
762
|
+
dryRun: true,
|
|
763
|
+
action: "run-as-set",
|
|
764
|
+
resource: "scenario",
|
|
765
|
+
id: a[0],
|
|
766
|
+
runAsUser,
|
|
767
|
+
currentRunAsUser: current?.["runAsUser"] ?? null,
|
|
768
|
+
endpoint: encodedProjectEndpoint(c, pk, `/scenarios/${encodeURIComponent(a[0])}/`),
|
|
769
|
+
method: "PUT",
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
const result = await c.scenarios.setRunAs(a[0], runAsUser, { projectKey: pk, });
|
|
773
|
+
return {
|
|
774
|
+
updated: a[0],
|
|
775
|
+
resource: "scenario",
|
|
776
|
+
part: "runAsUser",
|
|
777
|
+
runAsUser: result.runAsUser,
|
|
778
|
+
effectiveRunAs: result.effectiveRunAs,
|
|
779
|
+
...(result.previousRunAsUser === undefined
|
|
780
|
+
? {}
|
|
781
|
+
: { previousRunAsUser: result.previousRunAsUser, }),
|
|
782
|
+
};
|
|
783
|
+
},
|
|
784
|
+
description: "Set the user a scenario runs as (--user LOGIN, administrators only) or clear it (--clear: it then runs as its last editor). The result reports the effective run-as; scenario status shows runAsUser.",
|
|
785
|
+
examples: [
|
|
786
|
+
"dss scenario run-as-set my_scenario --user service_account",
|
|
787
|
+
"dss scenario run-as-set my_scenario --clear --dry-run",
|
|
788
|
+
],
|
|
789
|
+
},
|
|
312
790
|
});
|