dataiku-sdk 4.1.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/dist/build-metadata.json +1 -1
- package/dist/packages/types/src/index.d.ts +2 -0
- package/dist/packages/types/src/index.js +2 -0
- package/dist/src/cli/command-syntax.json +2426 -1262
- package/dist/src/cli/commands/future.js +7 -2
- package/dist/src/cli/commands/index.js +4 -0
- 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/scenario.d.ts +6 -0
- package/dist/src/cli/commands/scenario.js +423 -13
- package/dist/src/cli/contract.js +17 -2
- 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/plans.js +94 -2
- package/dist/src/cli/read-shapes.js +2 -1
- package/dist/src/cli/result-shapes.js +1 -0
- package/dist/src/cli/usage.js +7 -0
- package/dist/src/cli/version.d.ts +2 -2
- package/dist/src/cli/version.js +2 -2
- package/dist/src/client.d.ts +11 -0
- package/dist/src/client.js +21 -2
- package/dist/src/generated/action-output-schemas.json +82 -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/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/utils/messaging-channel-secrets.d.ts +17 -0
- package/dist/src/utils/messaging-channel-secrets.js +114 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/skills/dataiku-dss/SKILL.md +4 -3
- package/skills/dataiku-dss/references/notifications.md +29 -0
- package/skills/dataiku-dss/references/troubleshooting.md +1 -1
|
@@ -4,16 +4,20 @@ import { deepMerge, } from "../utils/deep-merge.js";
|
|
|
4
4
|
import { computeNextPollDelayMs, isRequestDeadlineError, } from "../utils/polling.js";
|
|
5
5
|
import { isRecord, } from "../utils/records.js";
|
|
6
6
|
import { BaseResource, requireArrayResponse, requireNonEmpty, } from "./base.js";
|
|
7
|
+
import { selectScenarioListIndex, } from "./scenario-authoring.js";
|
|
8
|
+
/** DSS keeps triggers and reporters at the top level of the scenario (as dataikuapi does), not under params. */
|
|
7
9
|
export const SCENARIO_CANONICAL_EDITABLE_FIELDS = [
|
|
8
10
|
"params.steps",
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
+
"triggers",
|
|
12
|
+
"reporters",
|
|
11
13
|
"params.customScript",
|
|
12
14
|
"active",
|
|
13
15
|
"name",
|
|
14
16
|
];
|
|
15
17
|
/** Default byte cap applied when fetching a scenario run log. */
|
|
16
18
|
export const DEFAULT_SCENARIO_MAX_LOG_BYTES = 1_048_576;
|
|
19
|
+
/** Where each list lives on the scenario object (top level, as dataikuapi). */
|
|
20
|
+
const SCENARIO_LIST_FIELDS = { trigger: "triggers", reporter: "reporters", };
|
|
17
21
|
/**
|
|
18
22
|
* Wraps a primary code-run failure so the original error (and its taxonomy:
|
|
19
23
|
* category, exit-code semantics) stays intact while the concurrent throwaway
|
|
@@ -48,11 +52,55 @@ function triggerRunIdOf(trigger, scenarioId) {
|
|
|
48
52
|
}
|
|
49
53
|
return runId;
|
|
50
54
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const triggerId =
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
/** Trigger id (`manual` for API runs), trigger run id and cancellation flag of a POST /run response. */
|
|
56
|
+
function triggerFireOf(trigger, scenarioId) {
|
|
57
|
+
const triggerId = isRecord(trigger.trigger) && typeof trigger.trigger.id === "string"
|
|
58
|
+
? trigger.trigger.id
|
|
59
|
+
: "manual";
|
|
60
|
+
return {
|
|
61
|
+
triggerId,
|
|
62
|
+
triggerRunId: triggerRunIdOf(trigger, scenarioId),
|
|
63
|
+
cancelled: trigger.cancelled === true,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** The `triggers`/`reporters` array of a scenario object; absent means empty, any other shape is an error. */
|
|
67
|
+
function scenarioListField(scenario, kind) {
|
|
68
|
+
const items = scenario[SCENARIO_LIST_FIELDS[kind]];
|
|
69
|
+
return items === undefined ? [] : requireArrayResponse(items, `scenario ${kind}s`);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* DSS stamps `params.lastUpdate` on every temporal trigger each time the scenario
|
|
73
|
+
* is saved, so echoing the value it last stored can never be read back (the
|
|
74
|
+
* verification after a trigger edit would report every trigger as unpersisted).
|
|
75
|
+
* Omitting it lets DSS stamp afresh, as it does for the other saved triggers.
|
|
76
|
+
*/
|
|
77
|
+
function withoutServerStamp(item) {
|
|
78
|
+
if (!isRecord(item) || !isRecord(item.params) || !("lastUpdate" in item.params))
|
|
79
|
+
return item;
|
|
80
|
+
const { lastUpdate: _stamp, ...params } = item.params;
|
|
81
|
+
return { ...item, params, };
|
|
82
|
+
}
|
|
83
|
+
/** `YYYY-MM-DD` day of a date or ISO date-time (the day DSS filters runs by); anything else is a usage error. */
|
|
84
|
+
function scenarioDay(value, name) {
|
|
85
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})(?:[T ].*)?$/.exec(value);
|
|
86
|
+
if (match) {
|
|
87
|
+
const day = new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3])));
|
|
88
|
+
if (day.getUTCMonth() === Number(match[2]) - 1 && day.getUTCDate() === Number(match[3])) {
|
|
89
|
+
return `${match[1]}-${match[2]}-${match[3]}`;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
throw new ClientValidationError(`${name} must be a YYYY-MM-DD date or ISO date-time, got ${JSON.stringify(value)}.`, "invalid_flag_value");
|
|
93
|
+
}
|
|
94
|
+
/** Who a scenario really runs as: its `runAsUser`, else the last editor (dataikuapi `effective_run_as`). */
|
|
95
|
+
export function effectiveRunAs(scenario) {
|
|
96
|
+
if (typeof scenario.runAsUser === "string")
|
|
97
|
+
return scenario.runAsUser;
|
|
98
|
+
for (const tag of [scenario.versionTag, scenario.creationTag,]) {
|
|
99
|
+
if (isRecord(tag) && isRecord(tag.lastModifiedBy) && typeof tag.lastModifiedBy.login === "string") {
|
|
100
|
+
return tag.lastModifiedBy.login;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return undefined;
|
|
56
104
|
}
|
|
57
105
|
/** A finite number, or undefined for anything else (absent/malformed JSON fields). */
|
|
58
106
|
function optionalNumber(value) {
|
|
@@ -320,12 +368,37 @@ export function scenarioUpdatePreview(current, data) {
|
|
|
320
368
|
}
|
|
321
369
|
function scenarioUpdateVerificationError(mismatches, normalization) {
|
|
322
370
|
const mismatchPaths = mismatches.map((mismatch) => mismatch.path).join(", ");
|
|
323
|
-
|
|
371
|
+
const body = {
|
|
324
372
|
message: `Scenario update did not persist requested fields after refetch: ${mismatchPaths}`,
|
|
325
373
|
mismatches,
|
|
326
374
|
canonicalEditableFields: SCENARIO_CANONICAL_EDITABLE_FIELDS,
|
|
327
375
|
...(normalization.length > 0 ? { normalization, } : {}),
|
|
328
|
-
}
|
|
376
|
+
};
|
|
377
|
+
const error = new DataikuError(400, "Scenario Update Verification Failed", JSON.stringify(body));
|
|
378
|
+
// CLI envelopes show `details`, never the body. Only paths go there: reporter and step
|
|
379
|
+
// values can hold webhook URLs, tokens, or credentials.
|
|
380
|
+
error.details = {
|
|
381
|
+
mismatches: mismatches.map((mismatch) => ({
|
|
382
|
+
path: mismatch.path,
|
|
383
|
+
differingPaths: differingLeafPaths(mismatch.expected, mismatch.actual, mismatch.path),
|
|
384
|
+
})),
|
|
385
|
+
canonicalEditableFields: SCENARIO_CANONICAL_EDITABLE_FIELDS,
|
|
386
|
+
};
|
|
387
|
+
return error;
|
|
388
|
+
}
|
|
389
|
+
/** Paths under `path` where the sent value is missing or different in what DSS stored (e.g. `reporters.0.messaging.channelId`). */
|
|
390
|
+
function differingLeafPaths(expected, actual, path) {
|
|
391
|
+
if (Array.isArray(expected)) {
|
|
392
|
+
if (!Array.isArray(actual))
|
|
393
|
+
return [path,];
|
|
394
|
+
return expected.flatMap((item, index) => differingLeafPaths(item, actual[index], `${path}.${index}`));
|
|
395
|
+
}
|
|
396
|
+
if (isRecord(expected)) {
|
|
397
|
+
if (!isRecord(actual))
|
|
398
|
+
return [path,];
|
|
399
|
+
return Object.entries(expected).flatMap(([key, value,]) => differingLeafPaths(value, actual[key], `${path}.${key}`));
|
|
400
|
+
}
|
|
401
|
+
return jsonValueEqual(expected, actual) ? [] : [path,];
|
|
329
402
|
}
|
|
330
403
|
export class ScenariosResource extends BaseResource {
|
|
331
404
|
/** List all scenarios in a project. */
|
|
@@ -348,16 +421,24 @@ export class ScenariosResource extends BaseResource {
|
|
|
348
421
|
name,
|
|
349
422
|
projectKey: pk,
|
|
350
423
|
type: scenarioType,
|
|
351
|
-
|
|
424
|
+
...(scenarioType === "step_based"
|
|
425
|
+
? { params: { steps: [], }, triggers: [], reporters: [], }
|
|
426
|
+
: { params: {}, }),
|
|
352
427
|
...opts?.data,
|
|
353
428
|
};
|
|
354
429
|
await this.client.post(`/public/api/projects/${this.enc(opts?.projectKey)}/scenarios/`, body);
|
|
355
430
|
}
|
|
356
|
-
/**
|
|
357
|
-
|
|
431
|
+
/**
|
|
432
|
+
* Trigger a scenario run. `params` is sent as the POST body (dataikuapi
|
|
433
|
+
* `run(params)`) and reaches the run as its trigger parameters. The result
|
|
434
|
+
* identifies the TRIGGER fire, not a scenario run: resolve it with
|
|
435
|
+
* {@link getRunForTrigger}. `cancelled` is already true when DSS refused the
|
|
436
|
+
* fire because the scenario is running.
|
|
437
|
+
*/
|
|
438
|
+
async run(scenarioId, opts) {
|
|
358
439
|
const scEnc = encodeURIComponent(requireNonEmpty(scenarioId, "scenarioId"));
|
|
359
|
-
const result = await this.client.post(`/public/api/projects/${this.enc(projectKey)}/scenarios/${scEnc}/run/`, {});
|
|
360
|
-
return
|
|
440
|
+
const result = await this.client.post(`/public/api/projects/${this.enc(opts?.projectKey)}/scenarios/${scEnc}/run/`, opts?.params ?? {});
|
|
441
|
+
return triggerFireOf(result, scenarioId);
|
|
361
442
|
}
|
|
362
443
|
/** Get the light/status view of a scenario. */
|
|
363
444
|
async status(scenarioId, projectKey) {
|
|
@@ -367,12 +448,15 @@ export class ScenariosResource extends BaseResource {
|
|
|
367
448
|
}
|
|
368
449
|
/** Merge-update a scenario's definition, then refetch and verify requested fields persisted. */
|
|
369
450
|
async update(scenarioId, data, projectKey) {
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
451
|
+
const current = await this.client.get(`/public/api/projects/${this.enc(projectKey)}/scenarios/${encodeURIComponent(scenarioId)}/`);
|
|
452
|
+
return this.applyUpdate(scenarioId, current, data, projectKey);
|
|
453
|
+
}
|
|
454
|
+
/** PUT the merge of `data` into an already-read scenario, refetch, and verify it persisted. */
|
|
455
|
+
async applyUpdate(scenarioId, current, data, projectKey) {
|
|
456
|
+
const path = `/public/api/projects/${this.enc(projectKey)}/scenarios/${encodeURIComponent(scenarioId)}/`;
|
|
373
457
|
const preview = scenarioUpdatePreview(current, data);
|
|
374
|
-
await this.client.put(
|
|
375
|
-
const after = await this.client.get(
|
|
458
|
+
await this.client.put(path, preview.next);
|
|
459
|
+
const after = await this.client.get(path);
|
|
376
460
|
const mismatches = scenarioFieldMismatches(after, preview.next, preview.normalizedData);
|
|
377
461
|
if (mismatches.length > 0) {
|
|
378
462
|
throw scenarioUpdateVerificationError(mismatches, preview.normalization);
|
|
@@ -406,14 +490,15 @@ export class ScenariosResource extends BaseResource {
|
|
|
406
490
|
// POST /run/ returns a TRIGGER run id, which differs from the actual scenario
|
|
407
491
|
// run id; resolve the real run via get-run-for-trigger so a completed scenario
|
|
408
492
|
// is not misreported as a timeout (the trigger id never matches lastRun).
|
|
409
|
-
const trigger = await this.client.post(`${base}/run/`, {});
|
|
410
|
-
const
|
|
493
|
+
const trigger = await this.client.post(`${base}/run/`, opts?.params ?? {});
|
|
494
|
+
const fire = triggerFireOf(trigger, scenarioId);
|
|
495
|
+
const tracked = await this.trackTriggerRun(scenarioId, opts?.projectKey, trigger, {
|
|
411
496
|
startedAt,
|
|
412
497
|
baseIntervalMs,
|
|
413
498
|
adaptivePolling,
|
|
414
499
|
timeout,
|
|
415
500
|
});
|
|
416
|
-
const resolvedRunId = tracked.runId ||
|
|
501
|
+
const resolvedRunId = tracked.runId || fire.triggerRunId;
|
|
417
502
|
const steps = tracked.timedOut || !tracked.runId
|
|
418
503
|
? undefined
|
|
419
504
|
: await this.runStepSummaries(base, tracked.runId);
|
|
@@ -424,9 +509,10 @@ export class ScenariosResource extends BaseResource {
|
|
|
424
509
|
success: tracked.outcome === "SUCCESS",
|
|
425
510
|
elapsedMs: Date.now() - startedAt,
|
|
426
511
|
pollCount: tracked.pollCount,
|
|
427
|
-
...(
|
|
428
|
-
? { triggerRunId:
|
|
512
|
+
...(fire.triggerRunId !== resolvedRunId || tracked.cancelled
|
|
513
|
+
? { triggerRunId: fire.triggerRunId, }
|
|
429
514
|
: {}),
|
|
515
|
+
...(tracked.cancelled ? { cancelled: true, } : {}),
|
|
430
516
|
...(tracked.timedOut ? { timedOut: true, } : {}),
|
|
431
517
|
...(steps ? { steps, } : {}),
|
|
432
518
|
};
|
|
@@ -470,7 +556,7 @@ export class ScenariosResource extends BaseResource {
|
|
|
470
556
|
extension: "py",
|
|
471
557
|
});
|
|
472
558
|
const trigger = await this.client.post(`${base}/run/`, {});
|
|
473
|
-
const tracked = await this.trackTriggerRun(scenarioId,
|
|
559
|
+
const tracked = await this.trackTriggerRun(scenarioId, opts?.projectKey, trigger, {
|
|
474
560
|
startedAt,
|
|
475
561
|
baseIntervalMs,
|
|
476
562
|
adaptivePolling,
|
|
@@ -535,16 +621,81 @@ export class ScenariosResource extends BaseResource {
|
|
|
535
621
|
const scEnc = encodeURIComponent(scenarioId);
|
|
536
622
|
await this.client.post(`/public/api/projects/${this.enc(projectKey)}/scenarios/${scEnc}/abort`);
|
|
537
623
|
}
|
|
538
|
-
/**
|
|
624
|
+
/**
|
|
625
|
+
* Get the raw last-runs list (GET /scenarios/{id}/get-last-runs/?limit=N).
|
|
626
|
+
* `onlyFinished` drops runs still in progress (DSS `onlyFinishedRuns`).
|
|
627
|
+
*/
|
|
539
628
|
async getLastRuns(scenarioId, opts) {
|
|
540
629
|
if (opts?.limit !== undefined && (!Number.isInteger(opts.limit) || opts.limit < 1)) {
|
|
541
630
|
throw new ClientValidationError(`limit must be a positive integer, got ${opts.limit}.`);
|
|
542
631
|
}
|
|
543
632
|
const scEnc = encodeURIComponent(scenarioId);
|
|
544
|
-
const query =
|
|
545
|
-
|
|
633
|
+
const query = new URLSearchParams();
|
|
634
|
+
if (opts?.limit !== undefined)
|
|
635
|
+
query.set("limit", String(opts.limit));
|
|
636
|
+
if (opts?.onlyFinished === true)
|
|
637
|
+
query.set("onlyFinishedRuns", "true");
|
|
638
|
+
const suffix = query.size > 0 ? `?${query}` : "";
|
|
639
|
+
const raw = await this.client.get(`/public/api/projects/${this.enc(opts?.projectKey)}/scenarios/${scEnc}/get-last-runs/${suffix}`);
|
|
546
640
|
return requireArrayResponse(raw, "scenarios.getLastRuns").map(scenarioRunSummaryFromRaw);
|
|
547
641
|
}
|
|
642
|
+
/**
|
|
643
|
+
* Runs that started between two days (GET /scenarios/{id}/get-runs-by-date).
|
|
644
|
+
* Dates are `YYYY-MM-DD` or ISO date-times (the time part is dropped, as
|
|
645
|
+
* dataikuapi does). DSS treats `to` as exclusive (verified live: `from=to=today`
|
|
646
|
+
* is empty and today's runs need `to=tomorrow`), so it defaults to tomorrow
|
|
647
|
+
* rather than dataikuapi's today, which would leave today's runs out.
|
|
648
|
+
*/
|
|
649
|
+
async getRunsByDate(scenarioId, opts) {
|
|
650
|
+
const query = new URLSearchParams({
|
|
651
|
+
fromDate: scenarioDay(opts.from, "from"),
|
|
652
|
+
toDate: scenarioDay(opts.to ?? new Date(Date.now() + 86_400_000).toLocaleDateString("en-CA"), "to"),
|
|
653
|
+
});
|
|
654
|
+
const raw = await this.client.get(`/public/api/projects/${this.enc(opts.projectKey)}/scenarios/${encodeURIComponent(requireNonEmpty(scenarioId, "scenarioId"))}/get-runs-by-date?${query}`);
|
|
655
|
+
return requireArrayResponse(raw, "scenarios.getRunsByDate").map(scenarioRunSummaryFromRaw);
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* JUnit XML report of a test scenario run (GET /scenarios/{id}/{runId}/scenario-run-report),
|
|
659
|
+
* or of one step with `stepId` (…/step-run-report?stepId=). DSS only builds
|
|
660
|
+
* reports for scenarios marked as test; the text is byte-bounded like the log.
|
|
661
|
+
*/
|
|
662
|
+
async getRunReport(scenarioId, runId, opts) {
|
|
663
|
+
if (opts?.stepId !== undefined)
|
|
664
|
+
requireNonEmpty(opts.stepId, "stepId");
|
|
665
|
+
const maxBytes = Math.max(0, Math.floor(opts?.maxBytes ?? DEFAULT_SCENARIO_MAX_LOG_BYTES));
|
|
666
|
+
const runPath = `/public/api/projects/${this.enc(opts?.projectKey)}/scenarios/${encodeURIComponent(requireNonEmpty(scenarioId, "scenarioId"))}/${encodeURIComponent(requireNonEmpty(runId, "runId"))}`;
|
|
667
|
+
const { text, truncated, } = await this.client.getTextLimited(opts?.stepId === undefined
|
|
668
|
+
? `${runPath}/scenario-run-report`
|
|
669
|
+
: `${runPath}/step-run-report?stepId=${encodeURIComponent(opts.stepId)}`, maxBytes);
|
|
670
|
+
return { text, truncated, maxBytes, };
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Resolve a trigger run (what `run` returns) to its scenario run: the scenario
|
|
674
|
+
* run id once DSS started one, or `CANCELLED` when DSS cancelled the fire (a
|
|
675
|
+
* run already in progress suppresses it) and no run will ever exist.
|
|
676
|
+
*/
|
|
677
|
+
async getRunForTrigger(scenarioId, triggerRunId, opts) {
|
|
678
|
+
const pkEnc = this.enc(opts?.projectKey);
|
|
679
|
+
const triggerId = requireNonEmpty(opts?.triggerId ?? "manual", "triggerId");
|
|
680
|
+
const fire = { triggerId, triggerRunId: requireNonEmpty(triggerRunId, "triggerRunId"), };
|
|
681
|
+
const run = await this.client.get(`/public/api/projects/${pkEnc}/scenarios/${encodeURIComponent(requireNonEmpty(scenarioId, "scenarioId"))}/get-run-for-trigger?triggerId=${encodeURIComponent(triggerId)}&triggerRunId=${encodeURIComponent(fire.triggerRunId)}`);
|
|
682
|
+
if (isRecord(run.scenarioRun)) {
|
|
683
|
+
const outcome = isRecord(run.scenarioRun.result) && typeof run.scenarioRun.result.outcome === "string"
|
|
684
|
+
? run.scenarioRun.result.outcome
|
|
685
|
+
: undefined;
|
|
686
|
+
const start = optionalNumber(run.scenarioRun.start);
|
|
687
|
+
return {
|
|
688
|
+
...fire,
|
|
689
|
+
cancelled: false,
|
|
690
|
+
state: outcome === undefined ? "RUNNING" : "FINISHED",
|
|
691
|
+
...(typeof run.scenarioRun.runId === "string" ? { runId: run.scenarioRun.runId, } : {}),
|
|
692
|
+
...(outcome === undefined ? {} : { outcome, }),
|
|
693
|
+
...(start === undefined ? {} : { start, }),
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
const cancelled = await this.isTriggerFireCancelled(pkEnc, scenarioId, fire);
|
|
697
|
+
return { ...fire, cancelled, state: cancelled ? "CANCELLED" : "PENDING", };
|
|
698
|
+
}
|
|
548
699
|
/** Get the details of a specific run (GET /scenarios/{id}/{runId}). */
|
|
549
700
|
async getRunDetails(scenarioId, runId, opts) {
|
|
550
701
|
return this.client.get(`/public/api/projects/${this.enc(opts?.projectKey)}/scenarios/${encodeURIComponent(scenarioId)}/${encodeURIComponent(runId)}/`);
|
|
@@ -604,34 +755,108 @@ export class ScenariosResource extends BaseResource {
|
|
|
604
755
|
status: scenarioLightStatusFromRaw(after),
|
|
605
756
|
};
|
|
606
757
|
}
|
|
758
|
+
/** The scenario's triggers or reporters, in DSS order (GET /scenarios/{id}/). */
|
|
759
|
+
async listItems(kind, scenarioId, opts) {
|
|
760
|
+
return scenarioListField(await this.getSettings(scenarioId, opts?.projectKey), kind);
|
|
761
|
+
}
|
|
762
|
+
async getSettings(scenarioId, projectKey) {
|
|
763
|
+
return this.client.get(`/public/api/projects/${this.enc(projectKey)}/scenarios/${encodeURIComponent(requireNonEmpty(scenarioId, "scenarioId"))}/`);
|
|
764
|
+
}
|
|
607
765
|
/**
|
|
608
|
-
*
|
|
609
|
-
*
|
|
610
|
-
*
|
|
611
|
-
* run is resolved via get-run-for-trigger (the trigger id never matches
|
|
612
|
-
* lastRun and would misreport a completed scenario as a timeout).
|
|
766
|
+
* Append a trigger or reporter. `triggers`/`reporters` are arrays, which a merge
|
|
767
|
+
* update replaces wholesale, so this reads the current list and writes it back
|
|
768
|
+
* with the new element last (then verifies the stored result).
|
|
613
769
|
*/
|
|
614
|
-
async
|
|
615
|
-
|
|
616
|
-
const
|
|
770
|
+
async addItem(kind, scenarioId, item, opts) {
|
|
771
|
+
const current = await this.getSettings(scenarioId, opts?.projectKey);
|
|
772
|
+
const items = scenarioListField(current, kind);
|
|
773
|
+
const result = await this.applyUpdate(scenarioId, current, {
|
|
774
|
+
[SCENARIO_LIST_FIELDS[kind]]: [...items.map(withoutServerStamp), item,],
|
|
775
|
+
}, opts?.projectKey);
|
|
776
|
+
const stored = scenarioListField(result.after, kind);
|
|
777
|
+
return { kind, index: items.length, item: stored[items.length], count: stored.length, };
|
|
778
|
+
}
|
|
779
|
+
/** Remove the trigger or reporter selected by `--index`/`--name`, keeping every other element. */
|
|
780
|
+
async removeItem(kind, scenarioId, selector, opts) {
|
|
781
|
+
const current = await this.getSettings(scenarioId, opts?.projectKey);
|
|
782
|
+
const items = scenarioListField(current, kind);
|
|
783
|
+
const index = selectScenarioListIndex(items, selector, kind);
|
|
784
|
+
const result = await this.applyUpdate(scenarioId, current, {
|
|
785
|
+
[SCENARIO_LIST_FIELDS[kind]]: items.flatMap((existing, position) => position === index ? [] : [withoutServerStamp(existing),]),
|
|
786
|
+
}, opts?.projectKey);
|
|
787
|
+
return {
|
|
788
|
+
kind,
|
|
789
|
+
index,
|
|
790
|
+
item: items[index],
|
|
791
|
+
count: scenarioListField(result.after, kind).length,
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Set (`user`) or clear (`null`) the scenario's `runAsUser` (admin only for a
|
|
796
|
+
* non-null value). Clearing deletes the key, which a merge update cannot do.
|
|
797
|
+
*/
|
|
798
|
+
async setRunAs(scenarioId, user, opts) {
|
|
799
|
+
const path = `/public/api/projects/${this.enc(opts?.projectKey)}/scenarios/${encodeURIComponent(requireNonEmpty(scenarioId, "scenarioId"))}/`;
|
|
800
|
+
const current = await this.client.get(path);
|
|
801
|
+
const { runAsUser: previous, ...withoutRunAs } = current;
|
|
802
|
+
await this.client.put(path, user === null ? withoutRunAs : { ...withoutRunAs, runAsUser: user, });
|
|
803
|
+
const after = await this.client.get(path);
|
|
804
|
+
const stored = typeof after.runAsUser === "string" ? after.runAsUser : null;
|
|
805
|
+
if (stored !== user) {
|
|
806
|
+
const error = new DataikuError(400, "Scenario Update Verification Failed", `Scenario run-as did not persist after refetch: expected ${user === null ? "no runAsUser" : "the requested user"}.`);
|
|
807
|
+
error.details = { mismatches: [{ path: "runAsUser", },], };
|
|
808
|
+
throw error;
|
|
809
|
+
}
|
|
810
|
+
return {
|
|
811
|
+
scenarioId,
|
|
812
|
+
runAsUser: stored,
|
|
813
|
+
effectiveRunAs: effectiveRunAs(after),
|
|
814
|
+
...(typeof previous === "string" ? { previousRunAsUser: previous, } : {}),
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* Poll get-run-for-trigger until the triggered run reports an outcome, DSS
|
|
819
|
+
* cancels the trigger fire, or the deadline elapses. Shared by runAndWait and
|
|
820
|
+
* runScript: POST /run/ returns a TRIGGER run id, which differs from the actual
|
|
821
|
+
* scenario run id, so the real run is resolved via get-run-for-trigger (the
|
|
822
|
+
* trigger id never matches lastRun and would misreport a completed scenario as
|
|
823
|
+
* a timeout). While no scenario run exists yet, the trigger fire itself is read:
|
|
824
|
+
* DSS cancels a fire when the scenario is already running (or a newer fire
|
|
825
|
+
* replaces it), and then no run will ever appear.
|
|
826
|
+
*/
|
|
827
|
+
async trackTriggerRun(scenarioId, projectKey, trigger, timing) {
|
|
828
|
+
const fire = triggerFireOf(trigger, scenarioId);
|
|
829
|
+
if (fire.cancelled) {
|
|
830
|
+
return { runId: "", outcome: "CANCELLED", timedOut: false, cancelled: true, pollCount: 0, };
|
|
831
|
+
}
|
|
832
|
+
const pkEnc = this.enc(projectKey);
|
|
833
|
+
const base = `/public/api/projects/${pkEnc}/scenarios/${encodeURIComponent(scenarioId)}`;
|
|
834
|
+
const trigQuery = `triggerId=${encodeURIComponent(fire.triggerId)}&triggerRunId=${encodeURIComponent(fire.triggerRunId)}`;
|
|
835
|
+
const remainingMs = () => Math.max(1, timing.timeout - (Date.now() - timing.startedAt));
|
|
617
836
|
let runId = "";
|
|
618
837
|
let pollCount = 0;
|
|
619
838
|
while (true) {
|
|
620
839
|
if (Date.now() - timing.startedAt >= timing.timeout) {
|
|
621
|
-
return { runId, outcome: "TIMEOUT", timedOut: true, pollCount, };
|
|
840
|
+
return { runId, outcome: "TIMEOUT", timedOut: true, cancelled: false, pollCount, };
|
|
622
841
|
}
|
|
623
842
|
pollCount += 1;
|
|
624
|
-
// Budget the per-poll
|
|
843
|
+
// Budget the per-poll GETs by the remaining wait so a stalling
|
|
625
844
|
// trigger-status endpoint cannot defeat the overall run deadline.
|
|
626
|
-
const remainingMs = Math.max(1, timing.timeout - (Date.now() - timing.startedAt));
|
|
627
845
|
let run;
|
|
846
|
+
let cancelled = false;
|
|
628
847
|
try {
|
|
629
|
-
run = await this.client.get(`${base}/get-run-for-trigger?${trigQuery}`, { timeoutMs: remainingMs, });
|
|
848
|
+
run = await this.client.get(`${base}/get-run-for-trigger?${trigQuery}`, { timeoutMs: remainingMs(), });
|
|
849
|
+
if (!isRecord(run.scenarioRun)) {
|
|
850
|
+
cancelled = await this.isTriggerFireCancelled(pkEnc, scenarioId, fire, remainingMs());
|
|
851
|
+
}
|
|
630
852
|
}
|
|
631
853
|
catch (error) {
|
|
632
854
|
if (!isRequestDeadlineError(error, timing.startedAt + timing.timeout))
|
|
633
855
|
throw error;
|
|
634
|
-
return { runId, outcome: "TIMEOUT", timedOut: true, pollCount, };
|
|
856
|
+
return { runId, outcome: "TIMEOUT", timedOut: true, cancelled: false, pollCount, };
|
|
857
|
+
}
|
|
858
|
+
if (cancelled) {
|
|
859
|
+
return { runId, outcome: "CANCELLED", timedOut: false, cancelled: true, pollCount, };
|
|
635
860
|
}
|
|
636
861
|
const scenarioRun = run.scenarioRun;
|
|
637
862
|
if (scenarioRun) {
|
|
@@ -639,7 +864,7 @@ export class ScenariosResource extends BaseResource {
|
|
|
639
864
|
const result = scenarioRun.result;
|
|
640
865
|
const finished = result?.outcome;
|
|
641
866
|
if (finished) {
|
|
642
|
-
return { runId, outcome: finished, timedOut: false, pollCount, };
|
|
867
|
+
return { runId, outcome: finished, timedOut: false, cancelled: false, pollCount, };
|
|
643
868
|
}
|
|
644
869
|
}
|
|
645
870
|
const nextDelayMs = computeNextPollDelayMs({
|
|
@@ -648,10 +873,15 @@ export class ScenariosResource extends BaseResource {
|
|
|
648
873
|
adaptiveEnabled: timing.adaptivePolling,
|
|
649
874
|
});
|
|
650
875
|
const { promise, resolve, } = Promise.withResolvers();
|
|
651
|
-
setTimeout(resolve, Math.min(nextDelayMs,
|
|
876
|
+
setTimeout(resolve, Math.min(nextDelayMs, remainingMs()));
|
|
652
877
|
await promise;
|
|
653
878
|
}
|
|
654
879
|
}
|
|
880
|
+
/** Whether DSS cancelled this trigger fire (GET /scenarios/trigger/{id}/{triggerId}?triggerRunId=). */
|
|
881
|
+
async isTriggerFireCancelled(pkEnc, scenarioId, fire, timeoutMs) {
|
|
882
|
+
const raw = await this.client.get(`/public/api/projects/${pkEnc}/scenarios/trigger/${encodeURIComponent(scenarioId)}/${encodeURIComponent(fire.triggerId)}?triggerRunId=${encodeURIComponent(fire.triggerRunId)}`, timeoutMs === undefined ? undefined : { timeoutMs, });
|
|
883
|
+
return raw.cancelled === true;
|
|
884
|
+
}
|
|
655
885
|
/**
|
|
656
886
|
* Best-effort step outcome summaries from the run report
|
|
657
887
|
* (GET /scenarios/{id}/{runId}). Never fails the wait: an unavailable
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replace every credential in a channel list item, definition, settings object,
|
|
3
|
+
* creation body, or permission payload with `[redacted]`. `secrets` adds exact
|
|
4
|
+
* values to scrub from free text (error messages echoing the request).
|
|
5
|
+
*/
|
|
6
|
+
export declare function sanitizeMessagingChannelSecrets<T>(value: T, secrets?: string[]): T;
|
|
7
|
+
/** Exact secret values of a request body, for scrubbing a DSS error that echoes it. */
|
|
8
|
+
export declare function messagingChannelSecretsFromBody(body: unknown): string[];
|
|
9
|
+
/** Scrub an error that may echo a submitted channel configuration before it reaches output. */
|
|
10
|
+
export declare function sanitizeMessagingChannelError(error: unknown, secrets: string[]): unknown;
|
|
11
|
+
/**
|
|
12
|
+
* Optimistic-concurrency token for channel settings. It hashes the redacted
|
|
13
|
+
* view, so it can be published next to redacted output without handing out a
|
|
14
|
+
* checksum of the credentials (a checksum would let low-entropy passwords be
|
|
15
|
+
* guessed offline). Credential-only edits therefore do not change it.
|
|
16
|
+
*/
|
|
17
|
+
export declare function messagingChannelSettingsHash(settings: unknown): string;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { isRecord, } from "./records.js";
|
|
2
|
+
import { credentialValues, normalizeSecretKey, sanitizeErrorSecrets, sanitizeSecrets, SECRET_REDACTED, } from "./secret-sanitize.js";
|
|
3
|
+
import { stableHash, } from "./stable-hash.js";
|
|
4
|
+
/**
|
|
5
|
+
* Keys of a messaging-channel configuration that carry credentials or bearer
|
|
6
|
+
* material: SMTP/Graph/SES secrets, Slack/Teams/Google Chat webhooks (the URL
|
|
7
|
+
* itself is the credential), Twilio tokens. Matched after stripping `-`/`_` and
|
|
8
|
+
* lowercasing. The SDK resource layer forwards values verbatim to DSS (over
|
|
9
|
+
* TLS) and returns server responses unmodified; CLI output, plans, previews and
|
|
10
|
+
* errors replace these with `[redacted]`. Login names and sender addresses are
|
|
11
|
+
* identifiers, not secrets, and stay visible.
|
|
12
|
+
*/
|
|
13
|
+
const SENSITIVE_MESSAGING_CHANNEL_KEYS = {
|
|
14
|
+
accesskey: true,
|
|
15
|
+
apikey: true,
|
|
16
|
+
apitoken: true,
|
|
17
|
+
authorization: true,
|
|
18
|
+
authorizationtoken: true,
|
|
19
|
+
authtoken: true,
|
|
20
|
+
clientsecret: true,
|
|
21
|
+
credential: true,
|
|
22
|
+
credentials: true,
|
|
23
|
+
password: true,
|
|
24
|
+
privatekey: true,
|
|
25
|
+
secretkey: true,
|
|
26
|
+
token: true,
|
|
27
|
+
webhookkey: true,
|
|
28
|
+
webhooktoken: true,
|
|
29
|
+
webhookurl: true,
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* SMTP session properties are `{key, value, secret}` entries (`mail.smtp.password`, ...).
|
|
33
|
+
* The value is a secret when DSS flags the entry `secret` or the property name says so.
|
|
34
|
+
*/
|
|
35
|
+
const SECRET_PROPERTY_NAME = /pass|secret|token|auth|credential|key/i;
|
|
36
|
+
function isSecretSessionProperty(property) {
|
|
37
|
+
return isRecord(property) && typeof property["key"] === "string"
|
|
38
|
+
&& (property["secret"] === true || SECRET_PROPERTY_NAME.test(property["key"]));
|
|
39
|
+
}
|
|
40
|
+
/** Blank the value of secret session properties, at any depth. */
|
|
41
|
+
function redactSessionProperties(value) {
|
|
42
|
+
if (Array.isArray(value))
|
|
43
|
+
return value.map(redactSessionProperties);
|
|
44
|
+
if (!isRecord(value))
|
|
45
|
+
return value;
|
|
46
|
+
return Object.fromEntries(Object.entries(value).map(([name, item,]) => {
|
|
47
|
+
if (normalizeSecretKey(name) === "sessionproperties" && Array.isArray(item)) {
|
|
48
|
+
return [
|
|
49
|
+
name,
|
|
50
|
+
item.map((property) => isSecretSessionProperty(property) ? { ...property, value: SECRET_REDACTED, } : property),
|
|
51
|
+
];
|
|
52
|
+
}
|
|
53
|
+
return [name, redactSessionProperties(item),];
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Replace every credential in a channel list item, definition, settings object,
|
|
58
|
+
* creation body, or permission payload with `[redacted]`. `secrets` adds exact
|
|
59
|
+
* values to scrub from free text (error messages echoing the request).
|
|
60
|
+
*/
|
|
61
|
+
export function sanitizeMessagingChannelSecrets(value, secrets = []) {
|
|
62
|
+
return sanitizeSecrets(redactSessionProperties(value), {
|
|
63
|
+
sensitiveKeys: SENSITIVE_MESSAGING_CHANNEL_KEYS,
|
|
64
|
+
secrets,
|
|
65
|
+
redactUrlUserinfo: true,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/** Exact secret values of a request body, for scrubbing a DSS error that echoes it. */
|
|
69
|
+
export function messagingChannelSecretsFromBody(body) {
|
|
70
|
+
const found = [];
|
|
71
|
+
const visit = (value) => {
|
|
72
|
+
if (Array.isArray(value)) {
|
|
73
|
+
value.forEach(visit);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (!isRecord(value))
|
|
77
|
+
return;
|
|
78
|
+
for (const [key, item,] of Object.entries(value)) {
|
|
79
|
+
if (SENSITIVE_MESSAGING_CHANNEL_KEYS[normalizeSecretKey(key)] === true) {
|
|
80
|
+
if (typeof item === "string")
|
|
81
|
+
found.push(item);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (normalizeSecretKey(key) === "sessionproperties" && Array.isArray(item)) {
|
|
85
|
+
for (const property of item) {
|
|
86
|
+
if (isSecretSessionProperty(property) && typeof property["value"] === "string") {
|
|
87
|
+
found.push(property["value"]);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
visit(item);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
visit(body);
|
|
96
|
+
return [...found, ...credentialValues(body),];
|
|
97
|
+
}
|
|
98
|
+
/** Scrub an error that may echo a submitted channel configuration before it reaches output. */
|
|
99
|
+
export function sanitizeMessagingChannelError(error, secrets) {
|
|
100
|
+
return sanitizeErrorSecrets(error, {
|
|
101
|
+
sensitiveKeys: SENSITIVE_MESSAGING_CHANNEL_KEYS,
|
|
102
|
+
secrets,
|
|
103
|
+
redactUrlUserinfo: true,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Optimistic-concurrency token for channel settings. It hashes the redacted
|
|
108
|
+
* view, so it can be published next to redacted output without handing out a
|
|
109
|
+
* checksum of the credentials (a checksum would let low-entropy passwords be
|
|
110
|
+
* guessed offline). Credential-only edits therefore do not change it.
|
|
111
|
+
*/
|
|
112
|
+
export function messagingChannelSettingsHash(settings) {
|
|
113
|
+
return stableHash(sanitizeMessagingChannelSecrets(settings));
|
|
114
|
+
}
|
package/package.json
CHANGED
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "dataiku-sdk",
|
|
4
|
-
"version": "4.
|
|
4
|
+
"version": "4.2.0",
|
|
5
5
|
"description": "Dataiku DSS SDK and agent-oriented CLI for programmatic access to DSS REST APIs.",
|
|
6
6
|
"homepage": "https://github.com/clssck/dataiku-sdk",
|
|
7
7
|
"repository": "https://github.com/clssck/dataiku-sdk",
|
|
@@ -22,9 +22,9 @@ Reads (`list`, `get`, `preview`, `schema`, `cat`, `log`, `last-runs`): run them
|
|
|
22
22
|
name or flag fails before any DSS call, and the error carries the usage line or a "Did you mean".
|
|
23
23
|
Before the first write of an action, fetch its entry once (syntax, payload schema, side effects,
|
|
24
24
|
dry-run, cleanup), several in one request: `dss commands run --fields recipe.set-payload,recipe.update`.
|
|
25
|
-
Unknown names: `dss commands run` lists
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
Unknown names: `dss commands run` lists actions; by concept: `dss commands run --contains "build dataset"`.
|
|
26
|
+
Contract version: `dss agent contract --fields agentContractVersion`. Never load the full registry;
|
|
27
|
+
export it if needed: `dss commands run --output commands.json`.
|
|
28
28
|
|
|
29
29
|
## Safety and results
|
|
30
30
|
|
|
@@ -58,3 +58,4 @@ Paths are relative to this skill, not CWD. Do not preload all references.
|
|
|
58
58
|
| Flow maps and zones | [Flow](references/flow-maps.md) |
|
|
59
59
|
| Recipes, libraries, notebooks, Git, code envs | [Coding](references/coding.md) |
|
|
60
60
|
| Failures, build logs, downloads, SQL, schema changes | [Troubleshooting](references/troubleshooting.md) |
|
|
61
|
+
| Scenario alerts, messaging channels, test reports | [Notifications](references/notifications.md) |
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Scenario notifications, messaging channels, test reports
|
|
2
|
+
|
|
3
|
+
## Email notification for a scenario, end to end
|
|
4
|
+
|
|
5
|
+
1. Find a mail channel: `dss messaging-channel list --family mail` (compact: `id`, `type`, `family`). Empty: there is none; an admin creates one (step 5).
|
|
6
|
+
2. `dss messaging-channel get CHANNEL_ID` shows its sender. Credentials (password, secret keys, webhook URLs, tokens) are always `[redacted]` in output, plans, and errors.
|
|
7
|
+
3. Attach it: `dss scenario reporter-add SCENARIO --channel CHANNEL_ID --recipient a@x.com,b@x.com --subject 'Scenario ${scenarioName}: ${outcome}' --message 'Finished with ${outcome}' --dry-run`, then apply without `--dry-run`. `--phase START|END` (default END), `--condition EXPR` limits when it fires. Other channel types (Slack, Teams, ...): pass the reporter JSON with `--data-file`.
|
|
8
|
+
4. Check: `dss scenario reporter-list SCENARIO`. `dss messaging-channel permissions-get CHANNEL_ID` shows which users and groups may use the channel.
|
|
9
|
+
5. Admin only, creating a channel: `dss messaging-channel create --type smtp --id mail-main --data-file smtp.json --dry-run` (`--type` smtp, aws-ses-mail, microsoft-graph-mail, slack, msteams, google-chat, twilio, shell; `--data` is its configuration; `--permissions '[{"group":"analysts","canUse":true}]'`, every user when omitted). Record it for cleanup with `--record-cleanup`.
|
|
10
|
+
|
|
11
|
+
Changing a channel: `dss messaging-channel settings-set ID --data '{"sender":"ops@x.com"}' --dry-run` merges into the stored settings; omit credentials you do not change. A `[redacted]` placeholder pasted back is refused. For concurrent edits pass `--expect-hash` with the `settingsHash` from `settings-get` (it covers non-credential fields only).
|
|
12
|
+
|
|
13
|
+
Send mail directly (external side effect, cannot be recalled; always `--dry-run` first, project needs Write content): `dss messaging-channel send CHANNEL_ID --to a@x.com,b@x.com --subject S --body-file body.html --attachment report.csv --project-key PROJ`. HTML unless `--plain-text`; one mail per `--to` address unless `--single-message`; `--attachment` takes comma-separated or repeated paths.
|
|
14
|
+
|
|
15
|
+
## Test scenarios
|
|
16
|
+
|
|
17
|
+
- `dss project testing-status [--bundle-id ID]`: worst outcome across the last runs of the project's test scenarios (scenarios with `markedAsTest`).
|
|
18
|
+
- `dss project test-report [--bundle-id ID]`: JUnit XML of the last test runs, in `report`; `--output PATH` writes the file instead; `--format html --output PATH` writes the HTML page.
|
|
19
|
+
- Project Deployer: `dss project-deployer testing-status DEPLOYMENT [--bundle-id ID] [--automation-node-id ID]`; `dss project-deployer run-test-scenarios DEPLOYMENT --dry-run` previews running the tests on the Automation node (blocks until they finish: raise `--request-timeout`). `--automation-node-id` is only for multi-node deployments.
|
|
20
|
+
- `dss scenario run-report SCENARIO RUNID [--step-id ID] [--output PATH]`: JUnit XML of one run of a test scenario (non-test scenarios: 400).
|
|
21
|
+
|
|
22
|
+
## Scenario setup and history
|
|
23
|
+
|
|
24
|
+
- Triggers: `dss scenario trigger-add SCENARIO --frequency minutely|hourly|daily|weekly|monthly [--every N --hour H --minute M --days Monday,Friday --day-of-month D --run-on ON_THE_DAY|LAST_DAY_OF_THE_MONTH|FIRST_WEEK..LAST_WEEK --start-date YYYY-MM-DD --timezone TZ --name N]`, or any trigger type with `--data`. `trigger-list` gives the 0-based `index` for `trigger-remove --index N` (or `--name X`). Each add/remove rewrites the whole list, keeping the others, and verifies it; `--dry-run` previews. Triggers fire only while the scenario is active (`scenario active-set SCENARIO true`).
|
|
25
|
+
- Reporters: `reporter-list`, `reporter-remove --index N|--name X` as above; `--full` shows DSS objects with credentials redacted.
|
|
26
|
+
- Run as: `dss scenario run-as-set SCENARIO --user LOGIN` (administrators) or `--clear` (runs as the last editor); the result's `effectiveRunAs` is the user DSS will use; `scenario status` shows `runAsUser`.
|
|
27
|
+
- Create with settings: `dss scenario create ID NAME --data '{"triggers":[...],"reporters":[...],"markedAsTest":true}'`.
|
|
28
|
+
- Run: `scenario run ID --params '{"region":"EU"}'` (or `--params-file`). Without `--wait` it returns a trigger run id (`triggerRunId`, not a run id); `scenario get-run-for-trigger ID TRIGGERRUNID` returns `state` PENDING, RUNNING, FINISHED (`runId`, `outcome`) or CANCELLED (DSS refused the fire because a run was in progress: wait or `scenario abort`, then run again). `run-and-wait` / `run --wait` end CANCELLED, exit 4, in that case.
|
|
29
|
+
- History: `scenario last-runs ID --only-finished --limit 1` is the last finished run (`outcome`); `scenario runs-by-date ID --from 2026-10-01 --to 2026-11-01` (`--to` exclusive; default tomorrow). `dss future list --scenarios-only` lists running scenario futures.
|