@camunda/ccma-shared-types 2.0.0 → 4.0.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/_tests/approvals/cluster-bulk-operation.spec.d.ts +1 -0
- package/dist/_tests/dtos/FeatureFlagOverride.dto.spec.d.ts +1 -0
- package/dist/_tests/permissions/session-permissions.spec.d.ts +1 -0
- package/dist/annotations/policy.d.ts +98 -0
- package/dist/annotations/policy.js +61 -0
- package/dist/annotations/policy.js.map +7 -0
- package/dist/approvals/cluster-bulk-operation.d.ts +196 -0
- package/dist/approvals/cluster-bulk-operation.js +127 -0
- package/dist/approvals/cluster-bulk-operation.js.map +7 -0
- package/dist/approvals/policy.d.ts +138 -9
- package/dist/approvals/policy.js +100 -23
- package/dist/approvals/policy.js.map +2 -2
- package/dist/approvals/use-cases.const.d.ts +114 -0
- package/dist/approvals/use-cases.const.js +31 -0
- package/dist/approvals/use-cases.const.js.map +7 -0
- package/dist/config/camundi-permission-rules.js +24 -3
- package/dist/config/camundi-permission-rules.js.map +2 -2
- package/dist/constants/misc.const.d.ts +2 -0
- package/dist/constants/misc.const.js +4 -0
- package/dist/constants/misc.const.js.map +2 -2
- package/dist/dtos/AlertSubscription.dto.d.ts +6 -0
- package/dist/dtos/AlertSubscription.dto.js.map +2 -2
- package/dist/dtos/Annotation.dto.d.ts +240 -0
- package/dist/dtos/Annotation.dto.js +47 -0
- package/dist/dtos/Annotation.dto.js.map +7 -0
- package/dist/dtos/ApprovalJob.dto.d.ts +408 -4
- package/dist/dtos/ApprovalJob.dto.js +64 -1
- package/dist/dtos/ApprovalJob.dto.js.map +2 -2
- package/dist/dtos/ClusterOwnerMove.dto.d.ts +81 -0
- package/dist/dtos/ClusterOwnerMove.dto.js +1 -0
- package/dist/dtos/ClusterOwnerMove.dto.js.map +7 -0
- package/dist/dtos/CommonCluster.dto.d.ts +6 -0
- package/dist/dtos/ControlV2.dto.d.ts +55 -3
- package/dist/dtos/FeatureFlagOverride.dto.d.ts +226 -0
- package/dist/dtos/FeatureFlagOverride.dto.js +38 -0
- package/dist/dtos/FeatureFlagOverride.dto.js.map +7 -0
- package/dist/dtos/FeatureFlags.dto.d.ts +58 -45
- package/dist/dtos/FeatureFlags.dto.js +10 -0
- package/dist/dtos/FeatureFlags.dto.js.map +3 -3
- package/dist/dtos/Generations.dto.d.ts +17 -0
- package/dist/dtos/Generations.dto.js +7 -0
- package/dist/dtos/Generations.dto.js.map +2 -2
- package/dist/dtos/Generations.dto.spec.d.ts +1 -0
- package/dist/dtos/K8sContext.dto.d.ts +13 -4
- package/dist/dtos/ReleaseTrainSanity.dto.d.ts +6 -0
- package/dist/enums/SystemAuditEntity.enum.d.ts +1 -0
- package/dist/enums/SystemAuditEntity.enum.js +1 -0
- package/dist/enums/SystemAuditEntity.enum.js.map +2 -2
- package/dist/index.d.ts +21 -10
- package/dist/index.js +157 -9
- package/dist/index.js.map +2 -2
- package/dist/permissions/accounts.permissions.d.ts +35 -0
- package/dist/permissions/accounts.permissions.js +33 -1
- package/dist/permissions/accounts.permissions.js.map +2 -2
- package/dist/permissions/combo.d.ts +9 -0
- package/dist/permissions/combo.js +64 -3
- package/dist/permissions/combo.js.map +2 -2
- package/dist/permissions/console.permissions.d.ts +32 -0
- package/dist/permissions/console.permissions.js +24 -2
- package/dist/permissions/console.permissions.js.map +2 -2
- package/dist/types/Roles.d.ts +1 -1
- package/dist/types/Roles.js.map +2 -2
- package/dist/utils/versionCheck.utils.d.ts +9 -0
- package/dist/utils/versionCheck.utils.js +7 -2
- package/dist/utils/versionCheck.utils.js.map +2 -2
- package/package.json +1 -1
- package/dist/constants/failoverRegionPairs.const.d.ts +0 -36
- package/dist/constants/failoverRegionPairs.const.js +0 -34
- package/dist/constants/failoverRegionPairs.const.js.map +0 -7
- /package/dist/_tests/{constants/failoverRegionPairs.const.spec.d.ts → annotations/policy.spec.d.ts} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { AnnotationDto } from '../dtos/Annotation.dto.js';
|
|
2
|
+
/**
|
|
3
|
+
* The rules of the annotations, as pure functions.
|
|
4
|
+
*
|
|
5
|
+
* Two consumers run this file: control-backend decides whether to refuse a
|
|
6
|
+
* write, and control-frontend decides whether to show the button. One copy
|
|
7
|
+
* means the button and the route can never disagree.
|
|
8
|
+
*
|
|
9
|
+
* No function here does I/O or returns a promise, which is what keeps it safe
|
|
10
|
+
* for a browser bundle and for a list of 200 rows.
|
|
11
|
+
*/
|
|
12
|
+
/** Who is asking. The frontend has this from `useSession`. */
|
|
13
|
+
export interface AnnotationActorView {
|
|
14
|
+
userId: string;
|
|
15
|
+
/**
|
|
16
|
+
* The address of the actor. It is the SECOND identity of the author rule,
|
|
17
|
+
* for the same reason as in the approvals: one person can hold two Auth0
|
|
18
|
+
* identities with two different `sub` values, and the author of a note must
|
|
19
|
+
* still be able to edit it after signing in through the other one.
|
|
20
|
+
*/
|
|
21
|
+
email?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Why a write was refused.
|
|
25
|
+
*
|
|
26
|
+
* The backend maps each code to a status, and the frontend shows the same
|
|
27
|
+
* words. See `ANNOTATION_DENIAL_MESSAGES`.
|
|
28
|
+
*/
|
|
29
|
+
export type AnnotationDenialCode = 'not-author' | 'empty-body' | 'body-too-long' | 'empty-tag' | 'tag-too-long';
|
|
30
|
+
/**
|
|
31
|
+
* What a person reads for each denial.
|
|
32
|
+
*
|
|
33
|
+
* The MESSAGE is here, and not in the error catalogue of the backend, because
|
|
34
|
+
* the frontend shows it beside a disabled button before any request is sent.
|
|
35
|
+
* Two spellings of one rule is the drift that this file exists to stop.
|
|
36
|
+
*/
|
|
37
|
+
export declare const ANNOTATION_DENIAL_MESSAGES: Record<AnnotationDenialCode, string>;
|
|
38
|
+
/** A rule that refused, with the code and the words for it. */
|
|
39
|
+
export interface AnnotationDenial {
|
|
40
|
+
ok: false;
|
|
41
|
+
code: AnnotationDenialCode;
|
|
42
|
+
message: string;
|
|
43
|
+
}
|
|
44
|
+
/** A rule that let the write through. */
|
|
45
|
+
export interface AnnotationAllowance {
|
|
46
|
+
ok: true;
|
|
47
|
+
}
|
|
48
|
+
export type AnnotationDecision = AnnotationAllowance | AnnotationDenial;
|
|
49
|
+
/**
|
|
50
|
+
* Reports whether the actor wrote this annotation.
|
|
51
|
+
*
|
|
52
|
+
* The id is read first, and the address second. See `AnnotationActorView.email`
|
|
53
|
+
* for why one identity is not enough.
|
|
54
|
+
*/
|
|
55
|
+
export declare function isAuthor(annotation: AnnotationDto, actor: AnnotationActorView): boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Whether the actor can change the text of a note.
|
|
58
|
+
*
|
|
59
|
+
* Only the author. A note carries an opinion with a name on it, so a second
|
|
60
|
+
* person who edits it would put words in that person's mouth. Somebody who
|
|
61
|
+
* disagrees writes a note of their own, and both stay visible.
|
|
62
|
+
*/
|
|
63
|
+
export declare function canEditNote(annotation: AnnotationDto, actor: AnnotationActorView): AnnotationDecision;
|
|
64
|
+
/**
|
|
65
|
+
* Whether the actor can remove an annotation.
|
|
66
|
+
*
|
|
67
|
+
* A NOTE follows `canEditNote`: the author, and nobody else.
|
|
68
|
+
*
|
|
69
|
+
* A TAG is different, and anybody can remove one. A tag is a grouping and not
|
|
70
|
+
* a statement, a wrong tag makes a search answer wrongly for everybody, and
|
|
71
|
+
* the person who can see the mistake is rarely the person who made it. The
|
|
72
|
+
* removal keeps the row, so who added it and who took it off both stay
|
|
73
|
+
* readable.
|
|
74
|
+
*/
|
|
75
|
+
export declare function canRemove(annotation: AnnotationDto, actor: AnnotationActorView): AnnotationDecision;
|
|
76
|
+
/**
|
|
77
|
+
* Whether this text can be a note.
|
|
78
|
+
*
|
|
79
|
+
* The frontend runs it on every keystroke to hold the button, and the route
|
|
80
|
+
* runs it on the body that arrives. The Zod schema of the route holds the same
|
|
81
|
+
* two numbers, so a caller that skips the frontend still meets them.
|
|
82
|
+
*/
|
|
83
|
+
export declare function checkNoteBody(body: string): AnnotationDecision;
|
|
84
|
+
/**
|
|
85
|
+
* Whether this text can be a tag name.
|
|
86
|
+
*
|
|
87
|
+
* The dictionary stores TWO values for one tag, so both are measured. The
|
|
88
|
+
* identity is `normalizeTagName(name)`, which the unique index holds, and the
|
|
89
|
+
* displayed name is `name.trim()`, which is what a reader sees on every badge.
|
|
90
|
+
* They are not the same length: normalizing turns a run of inner spaces into
|
|
91
|
+
* one hyphen, so `a<many spaces>b` is shorter after it than before. Measuring
|
|
92
|
+
* only the normalized value therefore let a display name through that is
|
|
93
|
+
* longer than the ceiling this function exists to hold.
|
|
94
|
+
*
|
|
95
|
+
* Emptiness is asked of the normalized value alone: a name of nothing but
|
|
96
|
+
* whitespace has no identity, whatever it looks like.
|
|
97
|
+
*/
|
|
98
|
+
export declare function checkTagName(name: string): AnnotationDecision;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { isSameActorEmail } from "../approvals/policy.js";
|
|
2
|
+
import {
|
|
3
|
+
ANNOTATION_BODY_MAX,
|
|
4
|
+
ANNOTATION_TAG_NAME_MAX,
|
|
5
|
+
normalizeTagName
|
|
6
|
+
} from "../dtos/Annotation.dto.js";
|
|
7
|
+
const ANNOTATION_DENIAL_MESSAGES = {
|
|
8
|
+
"not-author": "Only the person who wrote a note can change or remove it.",
|
|
9
|
+
"empty-body": "A note needs some text.",
|
|
10
|
+
"body-too-long": `A note holds at most ${ANNOTATION_BODY_MAX} characters.`,
|
|
11
|
+
"empty-tag": "A tag needs a name.",
|
|
12
|
+
"tag-too-long": `A tag name holds at most ${ANNOTATION_TAG_NAME_MAX} characters.`
|
|
13
|
+
};
|
|
14
|
+
const ALLOW = { ok: true };
|
|
15
|
+
function deny(code) {
|
|
16
|
+
return { ok: false, code, message: ANNOTATION_DENIAL_MESSAGES[code] };
|
|
17
|
+
}
|
|
18
|
+
function isAuthor(annotation, actor) {
|
|
19
|
+
if (annotation.createdBy.userId === actor.userId) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
return isSameActorEmail(annotation.createdBy.email, actor.email);
|
|
23
|
+
}
|
|
24
|
+
function canEditNote(annotation, actor) {
|
|
25
|
+
return isAuthor(annotation, actor) ? ALLOW : deny("not-author");
|
|
26
|
+
}
|
|
27
|
+
function canRemove(annotation, actor) {
|
|
28
|
+
if (annotation.kind === "tag") {
|
|
29
|
+
return ALLOW;
|
|
30
|
+
}
|
|
31
|
+
return canEditNote(annotation, actor);
|
|
32
|
+
}
|
|
33
|
+
function checkNoteBody(body) {
|
|
34
|
+
const trimmed = body.trim();
|
|
35
|
+
if (trimmed.length === 0) {
|
|
36
|
+
return deny("empty-body");
|
|
37
|
+
}
|
|
38
|
+
if (trimmed.length > ANNOTATION_BODY_MAX) {
|
|
39
|
+
return deny("body-too-long");
|
|
40
|
+
}
|
|
41
|
+
return ALLOW;
|
|
42
|
+
}
|
|
43
|
+
function checkTagName(name) {
|
|
44
|
+
const normalized = normalizeTagName(name);
|
|
45
|
+
if (normalized.length === 0) {
|
|
46
|
+
return deny("empty-tag");
|
|
47
|
+
}
|
|
48
|
+
if (normalized.length > ANNOTATION_TAG_NAME_MAX || name.trim().length > ANNOTATION_TAG_NAME_MAX) {
|
|
49
|
+
return deny("tag-too-long");
|
|
50
|
+
}
|
|
51
|
+
return ALLOW;
|
|
52
|
+
}
|
|
53
|
+
export {
|
|
54
|
+
ANNOTATION_DENIAL_MESSAGES,
|
|
55
|
+
canEditNote,
|
|
56
|
+
canRemove,
|
|
57
|
+
checkNoteBody,
|
|
58
|
+
checkTagName,
|
|
59
|
+
isAuthor
|
|
60
|
+
};
|
|
61
|
+
//# sourceMappingURL=policy.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/annotations/policy.ts"],
|
|
4
|
+
"sourcesContent": ["/*\n * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH\n * under one or more contributor license agreements. Licensed under a commercial license.\n * You may not use this file except in compliance with the commercial license.\n */\n\nimport { isSameActorEmail } from '../approvals/policy.js'\nimport type { AnnotationDto } from '../dtos/Annotation.dto.js'\nimport {\n ANNOTATION_BODY_MAX,\n ANNOTATION_TAG_NAME_MAX,\n normalizeTagName,\n} from '../dtos/Annotation.dto.js'\n\n/**\n * The rules of the annotations, as pure functions.\n *\n * Two consumers run this file: control-backend decides whether to refuse a\n * write, and control-frontend decides whether to show the button. One copy\n * means the button and the route can never disagree.\n *\n * No function here does I/O or returns a promise, which is what keeps it safe\n * for a browser bundle and for a list of 200 rows.\n */\n\n/** Who is asking. The frontend has this from `useSession`. */\nexport interface AnnotationActorView {\n userId: string\n /**\n * The address of the actor. It is the SECOND identity of the author rule,\n * for the same reason as in the approvals: one person can hold two Auth0\n * identities with two different `sub` values, and the author of a note must\n * still be able to edit it after signing in through the other one.\n */\n email?: string\n}\n\n/**\n * Why a write was refused.\n *\n * The backend maps each code to a status, and the frontend shows the same\n * words. See `ANNOTATION_DENIAL_MESSAGES`.\n */\nexport type AnnotationDenialCode =\n | 'not-author'\n | 'empty-body'\n | 'body-too-long'\n | 'empty-tag'\n | 'tag-too-long'\n\n/**\n * What a person reads for each denial.\n *\n * The MESSAGE is here, and not in the error catalogue of the backend, because\n * the frontend shows it beside a disabled button before any request is sent.\n * Two spellings of one rule is the drift that this file exists to stop.\n */\nexport const ANNOTATION_DENIAL_MESSAGES: Record<AnnotationDenialCode, string> =\n {\n 'not-author': 'Only the person who wrote a note can change or remove it.',\n 'empty-body': 'A note needs some text.',\n 'body-too-long': `A note holds at most ${ANNOTATION_BODY_MAX} characters.`,\n 'empty-tag': 'A tag needs a name.',\n 'tag-too-long': `A tag name holds at most ${ANNOTATION_TAG_NAME_MAX} characters.`,\n }\n\n/** A rule that refused, with the code and the words for it. */\nexport interface AnnotationDenial {\n ok: false\n code: AnnotationDenialCode\n message: string\n}\n\n/** A rule that let the write through. */\nexport interface AnnotationAllowance {\n ok: true\n}\n\nexport type AnnotationDecision = AnnotationAllowance | AnnotationDenial\n\nconst ALLOW: AnnotationAllowance = { ok: true }\n\nfunction deny(code: AnnotationDenialCode): AnnotationDenial {\n return { ok: false, code, message: ANNOTATION_DENIAL_MESSAGES[code] }\n}\n\n/**\n * Reports whether the actor wrote this annotation.\n *\n * The id is read first, and the address second. See `AnnotationActorView.email`\n * for why one identity is not enough.\n */\nexport function isAuthor(\n annotation: AnnotationDto,\n actor: AnnotationActorView,\n): boolean {\n if (annotation.createdBy.userId === actor.userId) {\n return true\n }\n return isSameActorEmail(annotation.createdBy.email, actor.email)\n}\n\n/**\n * Whether the actor can change the text of a note.\n *\n * Only the author. A note carries an opinion with a name on it, so a second\n * person who edits it would put words in that person's mouth. Somebody who\n * disagrees writes a note of their own, and both stay visible.\n */\nexport function canEditNote(\n annotation: AnnotationDto,\n actor: AnnotationActorView,\n): AnnotationDecision {\n return isAuthor(annotation, actor) ? ALLOW : deny('not-author')\n}\n\n/**\n * Whether the actor can remove an annotation.\n *\n * A NOTE follows `canEditNote`: the author, and nobody else.\n *\n * A TAG is different, and anybody can remove one. A tag is a grouping and not\n * a statement, a wrong tag makes a search answer wrongly for everybody, and\n * the person who can see the mistake is rarely the person who made it. The\n * removal keeps the row, so who added it and who took it off both stay\n * readable.\n */\nexport function canRemove(\n annotation: AnnotationDto,\n actor: AnnotationActorView,\n): AnnotationDecision {\n if (annotation.kind === 'tag') {\n return ALLOW\n }\n return canEditNote(annotation, actor)\n}\n\n/**\n * Whether this text can be a note.\n *\n * The frontend runs it on every keystroke to hold the button, and the route\n * runs it on the body that arrives. The Zod schema of the route holds the same\n * two numbers, so a caller that skips the frontend still meets them.\n */\nexport function checkNoteBody(body: string): AnnotationDecision {\n const trimmed = body.trim()\n if (trimmed.length === 0) {\n return deny('empty-body')\n }\n if (trimmed.length > ANNOTATION_BODY_MAX) {\n return deny('body-too-long')\n }\n return ALLOW\n}\n\n/**\n * Whether this text can be a tag name.\n *\n * The dictionary stores TWO values for one tag, so both are measured. The\n * identity is `normalizeTagName(name)`, which the unique index holds, and the\n * displayed name is `name.trim()`, which is what a reader sees on every badge.\n * They are not the same length: normalizing turns a run of inner spaces into\n * one hyphen, so `a<many spaces>b` is shorter after it than before. Measuring\n * only the normalized value therefore let a display name through that is\n * longer than the ceiling this function exists to hold.\n *\n * Emptiness is asked of the normalized value alone: a name of nothing but\n * whitespace has no identity, whatever it looks like.\n */\nexport function checkTagName(name: string): AnnotationDecision {\n const normalized = normalizeTagName(name)\n if (normalized.length === 0) {\n return deny('empty-tag')\n }\n if (\n normalized.length > ANNOTATION_TAG_NAME_MAX ||\n name.trim().length > ANNOTATION_TAG_NAME_MAX\n ) {\n return deny('tag-too-long')\n }\n return ALLOW\n}\n"],
|
|
5
|
+
"mappings": "AAMA,SAAS,wBAAwB;AAEjC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AA6CA,MAAM,6BACX;AAAA,EACE,cAAc;AAAA,EACd,cAAc;AAAA,EACd,iBAAiB,wBAAwB,mBAAmB;AAAA,EAC5D,aAAa;AAAA,EACb,gBAAgB,4BAA4B,uBAAuB;AACrE;AAgBF,MAAM,QAA6B,EAAE,IAAI,KAAK;AAE9C,SAAS,KAAK,MAA8C;AAC1D,SAAO,EAAE,IAAI,OAAO,MAAM,SAAS,2BAA2B,IAAI,EAAE;AACtE;AAQO,SAAS,SACd,YACA,OACS;AACT,MAAI,WAAW,UAAU,WAAW,MAAM,QAAQ;AAChD,WAAO;AAAA,EACT;AACA,SAAO,iBAAiB,WAAW,UAAU,OAAO,MAAM,KAAK;AACjE;AASO,SAAS,YACd,YACA,OACoB;AACpB,SAAO,SAAS,YAAY,KAAK,IAAI,QAAQ,KAAK,YAAY;AAChE;AAaO,SAAS,UACd,YACA,OACoB;AACpB,MAAI,WAAW,SAAS,OAAO;AAC7B,WAAO;AAAA,EACT;AACA,SAAO,YAAY,YAAY,KAAK;AACtC;AASO,SAAS,cAAc,MAAkC;AAC9D,QAAM,UAAU,KAAK,KAAK;AAC1B,MAAI,QAAQ,WAAW,GAAG;AACxB,WAAO,KAAK,YAAY;AAAA,EAC1B;AACA,MAAI,QAAQ,SAAS,qBAAqB;AACxC,WAAO,KAAK,eAAe;AAAA,EAC7B;AACA,SAAO;AACT;AAgBO,SAAS,aAAa,MAAkC;AAC7D,QAAM,aAAa,iBAAiB,IAAI;AACxC,MAAI,WAAW,WAAW,GAAG;AAC3B,WAAO,KAAK,WAAW;AAAA,EACzB;AACA,MACE,WAAW,SAAS,2BACpB,KAAK,KAAK,EAAE,SAAS,yBACrB;AACA,WAAO,KAAK,cAAc;AAAA,EAC5B;AACA,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cluster bulk operation on the approval mechanism of control-backend.
|
|
3
|
+
*
|
|
4
|
+
* control-backend and control-frontend both read this file. The backend
|
|
5
|
+
* builds its six use cases from `CLUSTER_BULK_OPERATION_RULES`, and the form
|
|
6
|
+
* of the frontend reads the same table to run `canCreate` before the request
|
|
7
|
+
* goes out. A permission that only one side knew would be a button that the
|
|
8
|
+
* server then refuses.
|
|
9
|
+
*
|
|
10
|
+
* console-backend reads the step types at the end. control-backend calls one
|
|
11
|
+
* step for each cluster, and console-backend does the work of that step with
|
|
12
|
+
* the same code that its own bulk operation runs.
|
|
13
|
+
*/
|
|
14
|
+
import { ClusterBulkOperationStatus, ClusterBulkOperationType } from '../enums/ClusterBulkOperation.enum.js';
|
|
15
|
+
import type { ClusterBulkOperationParameter } from '../types/ClusterBulkOperation.type.js';
|
|
16
|
+
/**
|
|
17
|
+
* The most clusters that one request holds.
|
|
18
|
+
*
|
|
19
|
+
* The payload is a json column, and the create route reads it in one body of
|
|
20
|
+
* at most 1 MiB. 3000 clusters with the longest names stay far below that.
|
|
21
|
+
* A larger set is two requests, which is also two decisions for the people
|
|
22
|
+
* who approve.
|
|
23
|
+
*/
|
|
24
|
+
export declare const CLUSTER_BULK_OPERATION_MAX_CLUSTERS = 3000;
|
|
25
|
+
/** The longest name of an operation. The name is what the queue shows. */
|
|
26
|
+
export declare const CLUSTER_BULK_OPERATION_NAME_MAX_LENGTH = 200;
|
|
27
|
+
/**
|
|
28
|
+
* The longest cluster name that the payload keeps.
|
|
29
|
+
*
|
|
30
|
+
* The name is a copy for a reader. The id alone decides what the step
|
|
31
|
+
* changes.
|
|
32
|
+
*/
|
|
33
|
+
export declare const CLUSTER_BULK_OPERATION_CLUSTER_NAME_MAX_LENGTH = 100;
|
|
34
|
+
/**
|
|
35
|
+
* The wait between two sleeping clusters of one datacenter.
|
|
36
|
+
*
|
|
37
|
+
* The same number as the bulk operation of console-backend. A wake-up loads
|
|
38
|
+
* the datacenter, and many wake-ups together overload it. Datacenters do not
|
|
39
|
+
* wait for each other.
|
|
40
|
+
*/
|
|
41
|
+
export declare const CLUSTER_BULK_SLEEPING_CLUSTER_DELAY_MS = 60000;
|
|
42
|
+
/** What one operation type needs, and which use case carries it. */
|
|
43
|
+
export interface ClusterBulkOperationRule {
|
|
44
|
+
useCase: string;
|
|
45
|
+
label: string;
|
|
46
|
+
/** The permission that opens, approves and runs a request of this type. */
|
|
47
|
+
permission: string;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The floor of approvals of a cluster bulk operation on prod, for every type.
|
|
51
|
+
* Dev, int and local use `CLUSTER_BULK_OPERATION_TEST_STAGE_MIN_ACKS`.
|
|
52
|
+
*
|
|
53
|
+
* A bulk operation changes many clusters with one decision, so it needs two
|
|
54
|
+
* people other than the requester. Every other use case needs one. This
|
|
55
|
+
* floor replaces `MINIMUM_EYEPAIRS` for the approval flow only. The old flow
|
|
56
|
+
* of console-backend still reads `MINIMUM_EYEPAIRS`.
|
|
57
|
+
*/
|
|
58
|
+
export declare const CLUSTER_BULK_OPERATION_MIN_ACKS = 2;
|
|
59
|
+
/**
|
|
60
|
+
* The floor of approvals of a cluster bulk operation on dev, int and local.
|
|
61
|
+
*
|
|
62
|
+
* A test stage often has one other Camundi who can approve. One approval
|
|
63
|
+
* there still tests the whole flow.
|
|
64
|
+
*/
|
|
65
|
+
export declare const CLUSTER_BULK_OPERATION_TEST_STAGE_MIN_ACKS = 1;
|
|
66
|
+
/**
|
|
67
|
+
* The rules of each operation type.
|
|
68
|
+
*
|
|
69
|
+
* The permissions are the ones that the bulk operation of console-backend
|
|
70
|
+
* checks today (`getBulkOpRequiredPermission`), so a Camundi who can run one
|
|
71
|
+
* flow can run the other. `CLUSTERS_READALLCLUSTERINFO` is part of every
|
|
72
|
+
* type there, and `clusterBulkRequiredPermissions` adds it here.
|
|
73
|
+
*/
|
|
74
|
+
export declare const CLUSTER_BULK_OPERATION_RULES: Record<ClusterBulkOperationType, ClusterBulkOperationRule>;
|
|
75
|
+
/** The operation types in the order that a form offers them. */
|
|
76
|
+
export declare const CLUSTER_BULK_OPERATION_TYPES: ClusterBulkOperationType[];
|
|
77
|
+
/** The operation type of a use case, or `undefined` for any other use case. */
|
|
78
|
+
export declare function clusterBulkOperationTypeOf(useCase: string): ClusterBulkOperationType | undefined;
|
|
79
|
+
/** One cluster of a request. */
|
|
80
|
+
export interface ClusterBulkTargetDto {
|
|
81
|
+
id: string;
|
|
82
|
+
/** A copy for a reader. The step reads the id alone. */
|
|
83
|
+
name?: string;
|
|
84
|
+
orgId?: string;
|
|
85
|
+
}
|
|
86
|
+
export interface ClusterBulkGenerationUpdateSettings {
|
|
87
|
+
targetGenerationId: string;
|
|
88
|
+
/** A copy for a reader. */
|
|
89
|
+
targetGenerationName?: string;
|
|
90
|
+
/** See `ClusterBulkOperationGenerationUpdateParameter.yoloAllClusters`. */
|
|
91
|
+
yoloAllClusters?: boolean;
|
|
92
|
+
/** See `ClusterBulkOperationGenerationUpdateParameter.yoloForceUpdate`. */
|
|
93
|
+
yoloForceUpdate?: boolean;
|
|
94
|
+
}
|
|
95
|
+
export interface ClusterBulkPlanUpdateSettings {
|
|
96
|
+
targetPlanId: string;
|
|
97
|
+
targetPlanName?: string;
|
|
98
|
+
/** Keeps the active modification of a cluster through the change. */
|
|
99
|
+
keepModification?: boolean;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* No `keepModification`: the parameter of a plan type update in the old flow
|
|
103
|
+
* has none, and its dialog never sends it.
|
|
104
|
+
*/
|
|
105
|
+
export interface ClusterBulkPlanTypeUpdateSettings {
|
|
106
|
+
targetPlanTypeId: string;
|
|
107
|
+
targetPlanTypeName?: string;
|
|
108
|
+
}
|
|
109
|
+
export interface ClusterBulkSleepWakeupSettings {
|
|
110
|
+
action: 'sleep' | 'wakeup';
|
|
111
|
+
}
|
|
112
|
+
/** The settings of each type. Deletion and spec update have none. */
|
|
113
|
+
export interface ClusterBulkSettingsByType {
|
|
114
|
+
[ClusterBulkOperationType.GENERATION_UPDATE]: ClusterBulkGenerationUpdateSettings;
|
|
115
|
+
[ClusterBulkOperationType.PLAN_UPDATE]: ClusterBulkPlanUpdateSettings;
|
|
116
|
+
[ClusterBulkOperationType.PLAN_TYPE_UPDATE]: ClusterBulkPlanTypeUpdateSettings;
|
|
117
|
+
[ClusterBulkOperationType.SLEEP_WAKEUP]: ClusterBulkSleepWakeupSettings;
|
|
118
|
+
[ClusterBulkOperationType.DELETION]: Record<string, never>;
|
|
119
|
+
[ClusterBulkOperationType.SPEC_UPDATE]: Record<string, never>;
|
|
120
|
+
}
|
|
121
|
+
/** The payload of a request, for one operation type. */
|
|
122
|
+
export interface ClusterBulkOperationPayloadDto<T extends ClusterBulkOperationType = ClusterBulkOperationType> {
|
|
123
|
+
name: string;
|
|
124
|
+
clusters: ClusterBulkTargetDto[];
|
|
125
|
+
settings: ClusterBulkSettingsByType[T];
|
|
126
|
+
}
|
|
127
|
+
/** True when the settings of a generation update go past a safety check. */
|
|
128
|
+
export declare function isClusterBulkYolo(type: ClusterBulkOperationType, settings: unknown): boolean;
|
|
129
|
+
/**
|
|
130
|
+
* Every permission that a request of this type and these settings needs.
|
|
131
|
+
*
|
|
132
|
+
* control-backend stores the answer on the request as
|
|
133
|
+
* `requiredPermissions`, and the form passes the same answer to `canCreate`.
|
|
134
|
+
* A yolo flag adds `CLUSTERS_CAN_UPDATE_GENERATION_YOLO`, which is the rule
|
|
135
|
+
* of `needsGenerationYolo` in console-backend.
|
|
136
|
+
*/
|
|
137
|
+
export declare function clusterBulkRequiredPermissions(type: ClusterBulkOperationType, settings: unknown): string[];
|
|
138
|
+
/**
|
|
139
|
+
* Turns the settings of a request into the parameter of console-backend.
|
|
140
|
+
*
|
|
141
|
+
* The step route of console-backend reads the parameter shape of its own
|
|
142
|
+
* bulk operation, so one function in console-backend serves both flows.
|
|
143
|
+
* `nop` of the sleep action is not reachable from here on purpose: a request
|
|
144
|
+
* that changes nothing is not a request that people approve.
|
|
145
|
+
*/
|
|
146
|
+
export declare function clusterBulkParameterOf(type: ClusterBulkOperationType, settings: unknown): ClusterBulkOperationParameter;
|
|
147
|
+
/** The outcome of one cluster, once its step finished. */
|
|
148
|
+
export type ClusterBulkFinalStatus = ClusterBulkOperationStatus.DONE | ClusterBulkOperationStatus.ERRORED | ClusterBulkOperationStatus.SKIPPED;
|
|
149
|
+
/**
|
|
150
|
+
* What a request records for one cluster, in `executionResult`.
|
|
151
|
+
*
|
|
152
|
+
* `executionResult` of a bulk request is an object with ONE key for each
|
|
153
|
+
* cluster that finished, keyed by the id of the cluster. A cluster with no
|
|
154
|
+
* key has not run yet. That is what lets an interrupted run skip the
|
|
155
|
+
* clusters that are finished, and what a reader shows as "not reached".
|
|
156
|
+
*/
|
|
157
|
+
export interface ClusterBulkClusterResultDto {
|
|
158
|
+
result: ClusterBulkFinalStatus;
|
|
159
|
+
/** Why a cluster errored or was skipped, or a note on a done cluster. */
|
|
160
|
+
error?: string;
|
|
161
|
+
/** An ISO timestamp: when the step answered. */
|
|
162
|
+
at: string;
|
|
163
|
+
}
|
|
164
|
+
/** Reads the result of one cluster out of `executionResult`. */
|
|
165
|
+
export declare function clusterBulkResultOf(executionResult: Record<string, unknown> | undefined, clusterId: string): ClusterBulkClusterResultDto | undefined;
|
|
166
|
+
/**
|
|
167
|
+
* The body of the step route of console-backend.
|
|
168
|
+
*
|
|
169
|
+
* `api/admin/approvals/clusters/:clusterId/bulk-step`. One call does the work
|
|
170
|
+
* of one operation type on one cluster.
|
|
171
|
+
*/
|
|
172
|
+
export interface ClusterBulkStepRequestDto {
|
|
173
|
+
type: ClusterBulkOperationType;
|
|
174
|
+
parameter: ClusterBulkOperationParameter;
|
|
175
|
+
/**
|
|
176
|
+
* When true, a sleeping cluster is NOT changed. The answer is `deferred`,
|
|
177
|
+
* with the datacenter of the cluster.
|
|
178
|
+
*
|
|
179
|
+
* This is how control-backend keeps the stagger of the old run: the first
|
|
180
|
+
* pass changes the awake clusters and learns which ones sleep, and a second
|
|
181
|
+
* pass changes the sleeping ones: one lane for each datacenter, the lanes
|
|
182
|
+
* side by side, and the clusters of one lane
|
|
183
|
+
* `CLUSTER_BULK_SLEEPING_CLUSTER_DELAY_MS` apart.
|
|
184
|
+
*/
|
|
185
|
+
deferIfSleeping: boolean;
|
|
186
|
+
}
|
|
187
|
+
/** The answer of the step route. */
|
|
188
|
+
export type ClusterBulkStepResultDto = {
|
|
189
|
+
outcome: 'finished';
|
|
190
|
+
result: ClusterBulkFinalStatus;
|
|
191
|
+
error?: string;
|
|
192
|
+
} | {
|
|
193
|
+
outcome: 'deferred';
|
|
194
|
+
/** The k8s context of the cluster, or `unknown`. */
|
|
195
|
+
datacenter: string;
|
|
196
|
+
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ClusterBulkOperationStatus,
|
|
3
|
+
ClusterBulkOperationType
|
|
4
|
+
} from "../enums/ClusterBulkOperation.enum.js";
|
|
5
|
+
import { ConsolePermissions } from "../permissions/console.permissions.js";
|
|
6
|
+
import { APPROVAL_USE_CASE_TYPES } from "./use-cases.const.js";
|
|
7
|
+
const CLUSTER_BULK_OPERATION_MAX_CLUSTERS = 3e3;
|
|
8
|
+
const CLUSTER_BULK_OPERATION_NAME_MAX_LENGTH = 200;
|
|
9
|
+
const CLUSTER_BULK_OPERATION_CLUSTER_NAME_MAX_LENGTH = 100;
|
|
10
|
+
const CLUSTER_BULK_SLEEPING_CLUSTER_DELAY_MS = 6e4;
|
|
11
|
+
const CLUSTER_BULK_OPERATION_MIN_ACKS = 2;
|
|
12
|
+
const CLUSTER_BULK_OPERATION_TEST_STAGE_MIN_ACKS = 1;
|
|
13
|
+
const CLUSTER_BULK_OPERATION_RULES = {
|
|
14
|
+
[ClusterBulkOperationType.GENERATION_UPDATE]: {
|
|
15
|
+
useCase: APPROVAL_USE_CASE_TYPES.clusterBulkGenerationUpdate,
|
|
16
|
+
label: "Generation update",
|
|
17
|
+
permission: ConsolePermissions.CLUSTERS_CAN_UPDATE_GENERATION
|
|
18
|
+
},
|
|
19
|
+
[ClusterBulkOperationType.PLAN_UPDATE]: {
|
|
20
|
+
useCase: APPROVAL_USE_CASE_TYPES.clusterBulkPlanUpdate,
|
|
21
|
+
label: "Plan update",
|
|
22
|
+
permission: ConsolePermissions.CLUSTERS_CAN_UPDATE_CLUSTERPLANTYPE
|
|
23
|
+
},
|
|
24
|
+
[ClusterBulkOperationType.PLAN_TYPE_UPDATE]: {
|
|
25
|
+
useCase: APPROVAL_USE_CASE_TYPES.clusterBulkPlanTypeUpdate,
|
|
26
|
+
label: "Plan type update",
|
|
27
|
+
permission: ConsolePermissions.CLUSTERS_CAN_UPDATE_CLUSTERPLANTYPE
|
|
28
|
+
},
|
|
29
|
+
[ClusterBulkOperationType.SLEEP_WAKEUP]: {
|
|
30
|
+
useCase: APPROVAL_USE_CASE_TYPES.clusterBulkSleepWakeup,
|
|
31
|
+
label: "Sleep / wake up",
|
|
32
|
+
permission: ConsolePermissions.CLUSTERS_CAN_SEND_TO_SLEEP
|
|
33
|
+
},
|
|
34
|
+
[ClusterBulkOperationType.DELETION]: {
|
|
35
|
+
useCase: APPROVAL_USE_CASE_TYPES.clusterBulkDeletion,
|
|
36
|
+
label: "Deletion",
|
|
37
|
+
permission: ConsolePermissions.CLUSTERS_DELETE_CLUSTER
|
|
38
|
+
},
|
|
39
|
+
[ClusterBulkOperationType.SPEC_UPDATE]: {
|
|
40
|
+
useCase: APPROVAL_USE_CASE_TYPES.clusterBulkSpecUpdate,
|
|
41
|
+
label: "Spec update",
|
|
42
|
+
permission: ConsolePermissions.CLUSTERS_CAN_UPDATE_CLUSTERPLANTYPE
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
const CLUSTER_BULK_OPERATION_TYPES = [
|
|
46
|
+
ClusterBulkOperationType.GENERATION_UPDATE,
|
|
47
|
+
ClusterBulkOperationType.PLAN_UPDATE,
|
|
48
|
+
ClusterBulkOperationType.PLAN_TYPE_UPDATE,
|
|
49
|
+
ClusterBulkOperationType.SLEEP_WAKEUP,
|
|
50
|
+
ClusterBulkOperationType.SPEC_UPDATE,
|
|
51
|
+
ClusterBulkOperationType.DELETION
|
|
52
|
+
];
|
|
53
|
+
function clusterBulkOperationTypeOf(useCase) {
|
|
54
|
+
return CLUSTER_BULK_OPERATION_TYPES.find(
|
|
55
|
+
(type) => CLUSTER_BULK_OPERATION_RULES[type].useCase === useCase
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
function isClusterBulkYolo(type, settings) {
|
|
59
|
+
if (type !== ClusterBulkOperationType.GENERATION_UPDATE || typeof settings !== "object" || settings === null) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
const typed = settings;
|
|
63
|
+
return typed.yoloAllClusters === true || typed.yoloForceUpdate === true;
|
|
64
|
+
}
|
|
65
|
+
function clusterBulkRequiredPermissions(type, settings) {
|
|
66
|
+
const permissions = [
|
|
67
|
+
CLUSTER_BULK_OPERATION_RULES[type].permission,
|
|
68
|
+
ConsolePermissions.CLUSTERS_READALLCLUSTERINFO
|
|
69
|
+
];
|
|
70
|
+
if (isClusterBulkYolo(type, settings)) {
|
|
71
|
+
permissions.push(ConsolePermissions.CLUSTERS_CAN_UPDATE_GENERATION_YOLO);
|
|
72
|
+
}
|
|
73
|
+
return permissions;
|
|
74
|
+
}
|
|
75
|
+
function clusterBulkParameterOf(type, settings) {
|
|
76
|
+
const values = settings ?? {};
|
|
77
|
+
switch (type) {
|
|
78
|
+
case ClusterBulkOperationType.GENERATION_UPDATE:
|
|
79
|
+
return {
|
|
80
|
+
toGenerationUuid: String(values.targetGenerationId),
|
|
81
|
+
yoloAllClusters: values.yoloAllClusters === true,
|
|
82
|
+
yoloForceUpdate: values.yoloForceUpdate === true
|
|
83
|
+
};
|
|
84
|
+
case ClusterBulkOperationType.PLAN_UPDATE:
|
|
85
|
+
return {
|
|
86
|
+
toPlanUuid: String(values.targetPlanId),
|
|
87
|
+
yolo: false,
|
|
88
|
+
keepModification: values.keepModification === true
|
|
89
|
+
};
|
|
90
|
+
case ClusterBulkOperationType.PLAN_TYPE_UPDATE:
|
|
91
|
+
return {
|
|
92
|
+
toPlanTypeUuid: String(values.targetPlanTypeId),
|
|
93
|
+
yolo: false
|
|
94
|
+
};
|
|
95
|
+
case ClusterBulkOperationType.SLEEP_WAKEUP:
|
|
96
|
+
return { toSleep: values.action === "wakeup" ? "wakeup" : "sleep" };
|
|
97
|
+
default:
|
|
98
|
+
return void 0;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function clusterBulkResultOf(executionResult, clusterId) {
|
|
102
|
+
const entry = executionResult?.[clusterId];
|
|
103
|
+
if (typeof entry !== "object" || entry === null) {
|
|
104
|
+
return void 0;
|
|
105
|
+
}
|
|
106
|
+
const result = entry.result;
|
|
107
|
+
if (result !== ClusterBulkOperationStatus.DONE && result !== ClusterBulkOperationStatus.ERRORED && result !== ClusterBulkOperationStatus.SKIPPED) {
|
|
108
|
+
return void 0;
|
|
109
|
+
}
|
|
110
|
+
return entry;
|
|
111
|
+
}
|
|
112
|
+
export {
|
|
113
|
+
CLUSTER_BULK_OPERATION_CLUSTER_NAME_MAX_LENGTH,
|
|
114
|
+
CLUSTER_BULK_OPERATION_MAX_CLUSTERS,
|
|
115
|
+
CLUSTER_BULK_OPERATION_MIN_ACKS,
|
|
116
|
+
CLUSTER_BULK_OPERATION_NAME_MAX_LENGTH,
|
|
117
|
+
CLUSTER_BULK_OPERATION_RULES,
|
|
118
|
+
CLUSTER_BULK_OPERATION_TEST_STAGE_MIN_ACKS,
|
|
119
|
+
CLUSTER_BULK_OPERATION_TYPES,
|
|
120
|
+
CLUSTER_BULK_SLEEPING_CLUSTER_DELAY_MS,
|
|
121
|
+
clusterBulkOperationTypeOf,
|
|
122
|
+
clusterBulkParameterOf,
|
|
123
|
+
clusterBulkRequiredPermissions,
|
|
124
|
+
clusterBulkResultOf,
|
|
125
|
+
isClusterBulkYolo
|
|
126
|
+
};
|
|
127
|
+
//# sourceMappingURL=cluster-bulk-operation.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/approvals/cluster-bulk-operation.ts"],
|
|
4
|
+
"sourcesContent": ["/*\n * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH\n * under one or more contributor license agreements. Licensed under a commercial license.\n * You may not use this file except in compliance with the commercial license.\n */\n\n/**\n * The cluster bulk operation on the approval mechanism of control-backend.\n *\n * control-backend and control-frontend both read this file. The backend\n * builds its six use cases from `CLUSTER_BULK_OPERATION_RULES`, and the form\n * of the frontend reads the same table to run `canCreate` before the request\n * goes out. A permission that only one side knew would be a button that the\n * server then refuses.\n *\n * console-backend reads the step types at the end. control-backend calls one\n * step for each cluster, and console-backend does the work of that step with\n * the same code that its own bulk operation runs.\n */\n\nimport {\n ClusterBulkOperationStatus,\n ClusterBulkOperationType,\n} from '../enums/ClusterBulkOperation.enum.js'\nimport { ConsolePermissions } from '../permissions/console.permissions.js'\nimport type { ClusterBulkOperationParameter } from '../types/ClusterBulkOperation.type.js'\nimport { APPROVAL_USE_CASE_TYPES } from './use-cases.const.js'\n\n/**\n * The most clusters that one request holds.\n *\n * The payload is a json column, and the create route reads it in one body of\n * at most 1 MiB. 3000 clusters with the longest names stay far below that.\n * A larger set is two requests, which is also two decisions for the people\n * who approve.\n */\nexport const CLUSTER_BULK_OPERATION_MAX_CLUSTERS = 3000\n\n/** The longest name of an operation. The name is what the queue shows. */\nexport const CLUSTER_BULK_OPERATION_NAME_MAX_LENGTH = 200\n\n/**\n * The longest cluster name that the payload keeps.\n *\n * The name is a copy for a reader. The id alone decides what the step\n * changes.\n */\nexport const CLUSTER_BULK_OPERATION_CLUSTER_NAME_MAX_LENGTH = 100\n\n/**\n * The wait between two sleeping clusters of one datacenter.\n *\n * The same number as the bulk operation of console-backend. A wake-up loads\n * the datacenter, and many wake-ups together overload it. Datacenters do not\n * wait for each other.\n */\nexport const CLUSTER_BULK_SLEEPING_CLUSTER_DELAY_MS = 60_000\n\n/** What one operation type needs, and which use case carries it. */\nexport interface ClusterBulkOperationRule {\n useCase: string\n label: string\n /** The permission that opens, approves and runs a request of this type. */\n permission: string\n}\n\n/**\n * The floor of approvals of a cluster bulk operation on prod, for every type.\n * Dev, int and local use `CLUSTER_BULK_OPERATION_TEST_STAGE_MIN_ACKS`.\n *\n * A bulk operation changes many clusters with one decision, so it needs two\n * people other than the requester. Every other use case needs one. This\n * floor replaces `MINIMUM_EYEPAIRS` for the approval flow only. The old flow\n * of console-backend still reads `MINIMUM_EYEPAIRS`.\n */\nexport const CLUSTER_BULK_OPERATION_MIN_ACKS = 2\n\n/**\n * The floor of approvals of a cluster bulk operation on dev, int and local.\n *\n * A test stage often has one other Camundi who can approve. One approval\n * there still tests the whole flow.\n */\nexport const CLUSTER_BULK_OPERATION_TEST_STAGE_MIN_ACKS = 1\n\n/**\n * The rules of each operation type.\n *\n * The permissions are the ones that the bulk operation of console-backend\n * checks today (`getBulkOpRequiredPermission`), so a Camundi who can run one\n * flow can run the other. `CLUSTERS_READALLCLUSTERINFO` is part of every\n * type there, and `clusterBulkRequiredPermissions` adds it here.\n */\nexport const CLUSTER_BULK_OPERATION_RULES: Record<\n ClusterBulkOperationType,\n ClusterBulkOperationRule\n> = {\n [ClusterBulkOperationType.GENERATION_UPDATE]: {\n useCase: APPROVAL_USE_CASE_TYPES.clusterBulkGenerationUpdate,\n label: 'Generation update',\n permission: ConsolePermissions.CLUSTERS_CAN_UPDATE_GENERATION,\n },\n [ClusterBulkOperationType.PLAN_UPDATE]: {\n useCase: APPROVAL_USE_CASE_TYPES.clusterBulkPlanUpdate,\n label: 'Plan update',\n permission: ConsolePermissions.CLUSTERS_CAN_UPDATE_CLUSTERPLANTYPE,\n },\n [ClusterBulkOperationType.PLAN_TYPE_UPDATE]: {\n useCase: APPROVAL_USE_CASE_TYPES.clusterBulkPlanTypeUpdate,\n label: 'Plan type update',\n permission: ConsolePermissions.CLUSTERS_CAN_UPDATE_CLUSTERPLANTYPE,\n },\n [ClusterBulkOperationType.SLEEP_WAKEUP]: {\n useCase: APPROVAL_USE_CASE_TYPES.clusterBulkSleepWakeup,\n label: 'Sleep / wake up',\n permission: ConsolePermissions.CLUSTERS_CAN_SEND_TO_SLEEP,\n },\n [ClusterBulkOperationType.DELETION]: {\n useCase: APPROVAL_USE_CASE_TYPES.clusterBulkDeletion,\n label: 'Deletion',\n permission: ConsolePermissions.CLUSTERS_DELETE_CLUSTER,\n },\n [ClusterBulkOperationType.SPEC_UPDATE]: {\n useCase: APPROVAL_USE_CASE_TYPES.clusterBulkSpecUpdate,\n label: 'Spec update',\n permission: ConsolePermissions.CLUSTERS_CAN_UPDATE_CLUSTERPLANTYPE,\n },\n}\n\n/** The operation types in the order that a form offers them. */\nexport const CLUSTER_BULK_OPERATION_TYPES: ClusterBulkOperationType[] = [\n ClusterBulkOperationType.GENERATION_UPDATE,\n ClusterBulkOperationType.PLAN_UPDATE,\n ClusterBulkOperationType.PLAN_TYPE_UPDATE,\n ClusterBulkOperationType.SLEEP_WAKEUP,\n ClusterBulkOperationType.SPEC_UPDATE,\n ClusterBulkOperationType.DELETION,\n]\n\n/** The operation type of a use case, or `undefined` for any other use case. */\nexport function clusterBulkOperationTypeOf(\n useCase: string,\n): ClusterBulkOperationType | undefined {\n return CLUSTER_BULK_OPERATION_TYPES.find(\n (type) => CLUSTER_BULK_OPERATION_RULES[type].useCase === useCase,\n )\n}\n\n/** One cluster of a request. */\nexport interface ClusterBulkTargetDto {\n id: string\n /** A copy for a reader. The step reads the id alone. */\n name?: string\n orgId?: string\n}\n\nexport interface ClusterBulkGenerationUpdateSettings {\n targetGenerationId: string\n /** A copy for a reader. */\n targetGenerationName?: string\n /** See `ClusterBulkOperationGenerationUpdateParameter.yoloAllClusters`. */\n yoloAllClusters?: boolean\n /** See `ClusterBulkOperationGenerationUpdateParameter.yoloForceUpdate`. */\n yoloForceUpdate?: boolean\n}\n\nexport interface ClusterBulkPlanUpdateSettings {\n targetPlanId: string\n targetPlanName?: string\n /** Keeps the active modification of a cluster through the change. */\n keepModification?: boolean\n}\n\n/**\n * No `keepModification`: the parameter of a plan type update in the old flow\n * has none, and its dialog never sends it.\n */\nexport interface ClusterBulkPlanTypeUpdateSettings {\n targetPlanTypeId: string\n targetPlanTypeName?: string\n}\n\nexport interface ClusterBulkSleepWakeupSettings {\n action: 'sleep' | 'wakeup'\n}\n\n/** The settings of each type. Deletion and spec update have none. */\nexport interface ClusterBulkSettingsByType {\n [ClusterBulkOperationType.GENERATION_UPDATE]: ClusterBulkGenerationUpdateSettings\n [ClusterBulkOperationType.PLAN_UPDATE]: ClusterBulkPlanUpdateSettings\n [ClusterBulkOperationType.PLAN_TYPE_UPDATE]: ClusterBulkPlanTypeUpdateSettings\n [ClusterBulkOperationType.SLEEP_WAKEUP]: ClusterBulkSleepWakeupSettings\n [ClusterBulkOperationType.DELETION]: Record<string, never>\n [ClusterBulkOperationType.SPEC_UPDATE]: Record<string, never>\n}\n\n/** The payload of a request, for one operation type. */\nexport interface ClusterBulkOperationPayloadDto<\n T extends ClusterBulkOperationType = ClusterBulkOperationType,\n> {\n name: string\n clusters: ClusterBulkTargetDto[]\n settings: ClusterBulkSettingsByType[T]\n}\n\n/** True when the settings of a generation update go past a safety check. */\nexport function isClusterBulkYolo(\n type: ClusterBulkOperationType,\n settings: unknown,\n): boolean {\n if (\n type !== ClusterBulkOperationType.GENERATION_UPDATE ||\n typeof settings !== 'object' ||\n settings === null\n ) {\n return false\n }\n const typed = settings as ClusterBulkGenerationUpdateSettings\n return typed.yoloAllClusters === true || typed.yoloForceUpdate === true\n}\n\n/**\n * Every permission that a request of this type and these settings needs.\n *\n * control-backend stores the answer on the request as\n * `requiredPermissions`, and the form passes the same answer to `canCreate`.\n * A yolo flag adds `CLUSTERS_CAN_UPDATE_GENERATION_YOLO`, which is the rule\n * of `needsGenerationYolo` in console-backend.\n */\nexport function clusterBulkRequiredPermissions(\n type: ClusterBulkOperationType,\n settings: unknown,\n): string[] {\n const permissions = [\n CLUSTER_BULK_OPERATION_RULES[type].permission,\n ConsolePermissions.CLUSTERS_READALLCLUSTERINFO,\n ]\n if (isClusterBulkYolo(type, settings)) {\n permissions.push(ConsolePermissions.CLUSTERS_CAN_UPDATE_GENERATION_YOLO)\n }\n return permissions\n}\n\n/**\n * Turns the settings of a request into the parameter of console-backend.\n *\n * The step route of console-backend reads the parameter shape of its own\n * bulk operation, so one function in console-backend serves both flows.\n * `nop` of the sleep action is not reachable from here on purpose: a request\n * that changes nothing is not a request that people approve.\n */\nexport function clusterBulkParameterOf(\n type: ClusterBulkOperationType,\n settings: unknown,\n): ClusterBulkOperationParameter {\n const values = (settings ?? {}) as Record<string, unknown>\n switch (type) {\n case ClusterBulkOperationType.GENERATION_UPDATE:\n return {\n toGenerationUuid: String(values.targetGenerationId),\n yoloAllClusters: values.yoloAllClusters === true,\n yoloForceUpdate: values.yoloForceUpdate === true,\n }\n case ClusterBulkOperationType.PLAN_UPDATE:\n return {\n toPlanUuid: String(values.targetPlanId),\n yolo: false,\n keepModification: values.keepModification === true,\n }\n case ClusterBulkOperationType.PLAN_TYPE_UPDATE:\n return {\n toPlanTypeUuid: String(values.targetPlanTypeId),\n yolo: false,\n }\n case ClusterBulkOperationType.SLEEP_WAKEUP:\n return { toSleep: values.action === 'wakeup' ? 'wakeup' : 'sleep' }\n default:\n return undefined\n }\n}\n\n/** The outcome of one cluster, once its step finished. */\nexport type ClusterBulkFinalStatus =\n | ClusterBulkOperationStatus.DONE\n | ClusterBulkOperationStatus.ERRORED\n | ClusterBulkOperationStatus.SKIPPED\n\n/**\n * What a request records for one cluster, in `executionResult`.\n *\n * `executionResult` of a bulk request is an object with ONE key for each\n * cluster that finished, keyed by the id of the cluster. A cluster with no\n * key has not run yet. That is what lets an interrupted run skip the\n * clusters that are finished, and what a reader shows as \"not reached\".\n */\nexport interface ClusterBulkClusterResultDto {\n result: ClusterBulkFinalStatus\n /** Why a cluster errored or was skipped, or a note on a done cluster. */\n error?: string\n /** An ISO timestamp: when the step answered. */\n at: string\n}\n\n/** Reads the result of one cluster out of `executionResult`. */\nexport function clusterBulkResultOf(\n executionResult: Record<string, unknown> | undefined,\n clusterId: string,\n): ClusterBulkClusterResultDto | undefined {\n const entry = executionResult?.[clusterId]\n if (typeof entry !== 'object' || entry === null) {\n return undefined\n }\n const result = (entry as { result?: unknown }).result\n if (\n result !== ClusterBulkOperationStatus.DONE &&\n result !== ClusterBulkOperationStatus.ERRORED &&\n result !== ClusterBulkOperationStatus.SKIPPED\n ) {\n return undefined\n }\n return entry as ClusterBulkClusterResultDto\n}\n\n/**\n * The body of the step route of console-backend.\n *\n * `api/admin/approvals/clusters/:clusterId/bulk-step`. One call does the work\n * of one operation type on one cluster.\n */\nexport interface ClusterBulkStepRequestDto {\n type: ClusterBulkOperationType\n parameter: ClusterBulkOperationParameter\n /**\n * When true, a sleeping cluster is NOT changed. The answer is `deferred`,\n * with the datacenter of the cluster.\n *\n * This is how control-backend keeps the stagger of the old run: the first\n * pass changes the awake clusters and learns which ones sleep, and a second\n * pass changes the sleeping ones: one lane for each datacenter, the lanes\n * side by side, and the clusters of one lane\n * `CLUSTER_BULK_SLEEPING_CLUSTER_DELAY_MS` apart.\n */\n deferIfSleeping: boolean\n}\n\n/** The answer of the step route. */\nexport type ClusterBulkStepResultDto =\n | {\n outcome: 'finished'\n result: ClusterBulkFinalStatus\n error?: string\n }\n | {\n outcome: 'deferred'\n /** The k8s context of the cluster, or `unknown`. */\n datacenter: string\n }\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|