@camunda/ccma-shared-types 7.2.0 → 7.4.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/types/ClusterTtl.type.spec.d.ts +1 -0
- package/dist/approvals/use-cases.const.d.ts +2 -2
- package/dist/approvals/use-cases.const.js +3 -3
- package/dist/approvals/use-cases.const.js.map +2 -2
- package/dist/constants/{customer-hardware-sizing.const.d.ts → custom-hardware-sizing.const.d.ts} +18 -18
- package/dist/constants/{customer-hardware-sizing.const.js → custom-hardware-sizing.const.js} +35 -35
- package/dist/constants/custom-hardware-sizing.const.js.map +7 -0
- package/dist/dtos/ApprovalJob.dto.d.ts +2 -2
- package/dist/dtos/ApprovalJob.dto.js.map +1 -1
- package/dist/dtos/CommonCluster.dto.d.ts +12 -0
- package/dist/dtos/ControlV2.dto.d.ts +20 -0
- package/dist/enums/AuditEntity.enum.d.ts +1 -0
- package/dist/enums/AuditEntity.enum.js +1 -0
- package/dist/enums/AuditEntity.enum.js.map +2 -2
- package/dist/index.d.ts +5 -3
- package/dist/index.js +63 -25
- package/dist/index.js.map +2 -2
- package/dist/search/cluster-search-fields.d.ts +11 -0
- package/dist/search/cluster-search-fields.js +29 -1
- package/dist/search/cluster-search-fields.js.map +2 -2
- package/dist/search/index.d.ts +1 -1
- package/dist/search/index.js +5 -1
- package/dist/search/index.js.map +2 -2
- package/dist/types/ClusterTtl.type.d.ts +96 -0
- package/dist/types/ClusterTtl.type.js +89 -0
- package/dist/types/ClusterTtl.type.js.map +7 -0
- package/package.json +1 -1
- package/dist/constants/customer-hardware-sizing.const.js.map +0 -7
- /package/dist/_tests/constants/{customer-hardware-sizing.const.spec.d.ts → custom-hardware-sizing.const.spec.d.ts} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -35,8 +35,8 @@ export declare const APPROVAL_USE_CASE_TYPES: {
|
|
|
35
35
|
readonly updateUserMetadata: "update-user-metadata";
|
|
36
36
|
readonly changeFeatureFlag: "change-feature-flag";
|
|
37
37
|
readonly createPersonalOrganisation: "create-personal-organisation";
|
|
38
|
-
readonly
|
|
39
|
-
readonly
|
|
38
|
+
readonly createCustomHardwareSizing: "create-custom-hardware-sizing";
|
|
39
|
+
readonly removeCustomHardwareSizing: "remove-custom-hardware-sizing";
|
|
40
40
|
readonly onboardToEnterprise: "onboard-to-enterprise";
|
|
41
41
|
readonly changeClusterPlanType: "change-cluster-plan-type";
|
|
42
42
|
};
|
|
@@ -30,9 +30,9 @@ const APPROVAL_USE_CASE_TYPES = {
|
|
|
30
30
|
// Open to every Camundi, and it needs no approval on any stage.
|
|
31
31
|
createPersonalOrganisation: "create-personal-organisation",
|
|
32
32
|
// Open to every Camundi, runs on dev only, and needs no approval. The two
|
|
33
|
-
// use cases of
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
// use cases of custom hardware sizing.
|
|
34
|
+
createCustomHardwareSizing: "create-custom-hardware-sizing",
|
|
35
|
+
removeCustomHardwareSizing: "remove-custom-hardware-sizing",
|
|
36
36
|
// The enterprise onboarding of one organization, behind
|
|
37
37
|
// `control.onboardToEnterpriseApproval`: the contract, the plan type of
|
|
38
38
|
// each cluster, and the reservations, in one request.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/approvals/use-cases.const.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 { AccountsPermissions } from '../permissions/accounts.permissions.js'\n\n/**\n * The names and the value ranges of the registered use cases.\n *\n * control-backend owns the registry, and a frontend reads it at run time\n * (`GET /api/approvals/use-cases`). Two things cannot wait for that answer:\n *\n * - **The identifier**, because a caller must NAME a use case to find it in the\n * registry, and to open a request of it.\n * - **The range of a payload field**, because a form holds the value before any\n * call goes out.\n *\n * Both live here, so the schema of the service and the form of the browser can\n * never hold different numbers.\n */\n\n/** The identifier of each use case, on the wire and in the table. */\nexport const APPROVAL_USE_CASE_TYPES = {\n sendToExpiredTrial: 'send-to-expired-trial',\n extendTrial: 'extend-trial',\n moveClusterOrg: 'move-cluster-org',\n // One use case for each type of a cluster bulk operation. The types need\n // different permissions and different approval counts, and a use case holds\n // one of each. See `cluster-bulk-operation.ts` beside this file.\n clusterBulkGenerationUpdate: 'cluster-bulk-generation-update',\n clusterBulkPlanUpdate: 'cluster-bulk-plan-update',\n clusterBulkPlanTypeUpdate: 'cluster-bulk-plan-type-update',\n clusterBulkSleepWakeup: 'cluster-bulk-sleep-wakeup',\n clusterBulkDeletion: 'cluster-bulk-deletion',\n clusterBulkSpecUpdate: 'cluster-bulk-spec-update',\n // The second group, behind `control.approvalAdminActions`.\n changePlatformRoles: 'change-platform-roles',\n deleteInternalBackup: 'delete-internal-backup',\n changeOrgLimits: 'change-org-limits',\n deleteSsoConnection: 'delete-sso-connection',\n changeSalesPlan: 'change-sales-plan',\n // The third group, behind `control.approvalAccountActions`.\n deleteOrganization: 'delete-organization',\n deleteUser: 'delete-user',\n changeUserEmail: 'change-user-email',\n purgeUsers: 'purge-users',\n updateUserMetadata: 'update-user-metadata',\n // A change of the feature flag table, on a stage where\n // `isFeatureFlagApprovalRequired` is true.\n changeFeatureFlag: 'change-feature-flag',\n // Open to every Camundi, and it needs no approval on any stage.\n createPersonalOrganisation: 'create-personal-organisation',\n // Open to every Camundi, runs on dev only, and needs no approval. The two\n // use cases of
|
|
5
|
-
"mappings": "AAMA,SAAS,2BAA2B;AAkB7B,MAAM,0BAA0B;AAAA,EACrC,oBAAoB;AAAA,EACpB,aAAa;AAAA,EACb,gBAAgB;AAAA;AAAA;AAAA;AAAA,EAIhB,6BAA6B;AAAA,EAC7B,uBAAuB;AAAA,EACvB,2BAA2B;AAAA,EAC3B,wBAAwB;AAAA,EACxB,qBAAqB;AAAA,EACrB,uBAAuB;AAAA;AAAA,EAEvB,qBAAqB;AAAA,EACrB,sBAAsB;AAAA,EACtB,iBAAiB;AAAA,EACjB,qBAAqB;AAAA,EACrB,iBAAiB;AAAA;AAAA,EAEjB,oBAAoB;AAAA,EACpB,YAAY;AAAA,EACZ,iBAAiB;AAAA,EACjB,YAAY;AAAA,EACZ,oBAAoB;AAAA;AAAA;AAAA,EAGpB,mBAAmB;AAAA;AAAA,EAEnB,4BAA4B;AAAA;AAAA;AAAA,EAG5B,
|
|
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 { AccountsPermissions } from '../permissions/accounts.permissions.js'\n\n/**\n * The names and the value ranges of the registered use cases.\n *\n * control-backend owns the registry, and a frontend reads it at run time\n * (`GET /api/approvals/use-cases`). Two things cannot wait for that answer:\n *\n * - **The identifier**, because a caller must NAME a use case to find it in the\n * registry, and to open a request of it.\n * - **The range of a payload field**, because a form holds the value before any\n * call goes out.\n *\n * Both live here, so the schema of the service and the form of the browser can\n * never hold different numbers.\n */\n\n/** The identifier of each use case, on the wire and in the table. */\nexport const APPROVAL_USE_CASE_TYPES = {\n sendToExpiredTrial: 'send-to-expired-trial',\n extendTrial: 'extend-trial',\n moveClusterOrg: 'move-cluster-org',\n // One use case for each type of a cluster bulk operation. The types need\n // different permissions and different approval counts, and a use case holds\n // one of each. See `cluster-bulk-operation.ts` beside this file.\n clusterBulkGenerationUpdate: 'cluster-bulk-generation-update',\n clusterBulkPlanUpdate: 'cluster-bulk-plan-update',\n clusterBulkPlanTypeUpdate: 'cluster-bulk-plan-type-update',\n clusterBulkSleepWakeup: 'cluster-bulk-sleep-wakeup',\n clusterBulkDeletion: 'cluster-bulk-deletion',\n clusterBulkSpecUpdate: 'cluster-bulk-spec-update',\n // The second group, behind `control.approvalAdminActions`.\n changePlatformRoles: 'change-platform-roles',\n deleteInternalBackup: 'delete-internal-backup',\n changeOrgLimits: 'change-org-limits',\n deleteSsoConnection: 'delete-sso-connection',\n changeSalesPlan: 'change-sales-plan',\n // The third group, behind `control.approvalAccountActions`.\n deleteOrganization: 'delete-organization',\n deleteUser: 'delete-user',\n changeUserEmail: 'change-user-email',\n purgeUsers: 'purge-users',\n updateUserMetadata: 'update-user-metadata',\n // A change of the feature flag table, on a stage where\n // `isFeatureFlagApprovalRequired` is true.\n changeFeatureFlag: 'change-feature-flag',\n // Open to every Camundi, and it needs no approval on any stage.\n createPersonalOrganisation: 'create-personal-organisation',\n // Open to every Camundi, runs on dev only, and needs no approval. The two\n // use cases of custom hardware sizing.\n createCustomHardwareSizing: 'create-custom-hardware-sizing',\n removeCustomHardwareSizing: 'remove-custom-hardware-sizing',\n // The enterprise onboarding of one organization, behind\n // `control.onboardToEnterpriseApproval`: the contract, the plan type of\n // each cluster, and the reservations, in one request.\n onboardToEnterprise: 'onboard-to-enterprise',\n // The plan type and the hardware packages of ONE cluster, behind\n // `control.changeClusterPlanType`. The bulk plan type update beside it\n // checks no reservation and moves no cluster plan, so this one is not a\n // duplicate of it. See `change-cluster-plan-type.ts` in control-backend.\n changeClusterPlanType: 'change-cluster-plan-type',\n} as const\n\n/**\n * The approvals that a change of the feature flag table needs on prod.\n *\n * A flag there turns a feature on or off for every customer, and a person who\n * holds `aafw` could do it alone. Two other Camundis stand in every change,\n * the same floor as a cluster bulk operation on prod\n * (`CLUSTER_BULK_OPERATION_MIN_ACKS`). Int needs one.\n */\nexport const FEATURE_FLAG_MIN_ACKS_PROD = 2\n\n/**\n * The most personal organizations that one Camundi can request.\n *\n * The request runs with no approval, so this cap is the only guard against a\n * loop that creates organizations without end. It counts the requests that a\n * Camundi opened, and a person who needs more asks a Camundi who holds\n * `AccountsPermissions.CREATE_ORG`.\n */\nexport const PERSONAL_ORGANIZATION_MAX_PER_REQUESTER = 10\n\n/** The shortest and longest name of a personal organization. */\nexport const PERSONAL_ORGANIZATION_NAME_MIN_LENGTH = 3\nexport const PERSONAL_ORGANIZATION_NAME_MAX_LENGTH = 100\n\nexport type ApprovalUseCaseType =\n (typeof APPROVAL_USE_CASE_TYPES)[keyof typeof APPROVAL_USE_CASE_TYPES]\n\n/**\n * The shortest extension that accounts-backend accepts.\n *\n * `POST /:orgId/extendtrial` refuses a date that is less than 14 days away\n * when the body carries `onBehalfOf`, and it measures those 14 days when the\n * CALL arrives. The floor here is 15 and not 14, for two reasons:\n *\n * - The two clocks are not the same instant. Accounts builds its own \"14 days\n * from now\" a few milliseconds after control-backend builds the date, so a\n * date of exactly 14 days is already in the past for the comparison.\n * - Accounts counts with `setDate`, which is calendar arithmetic. A change of\n * the daylight saving time moves its limit by an hour.\n */\nexport const EXTEND_TRIAL_MIN_DAYS = 15\n\n/**\n * The longest extension that accounts-backend accepts.\n *\n * The same route refuses a date more than 62 days away. 61 keeps the same\n * margin as the floor above.\n */\nexport const EXTEND_TRIAL_MAX_DAYS = 61\n\n/**\n * The most roles that one change-platform-roles request adds, or removes.\n *\n * The form of control-frontend offers every platform role, and a person who\n * gives a Camundi a new position rarely changes more than a few. A request with\n * more roles than this is a request that nobody reads before they approve it.\n */\nexport const PLATFORM_ROLE_CHANGE_MAX_ROLES = 10\n\n/**\n * The longest text value that one change-org-limits request writes.\n *\n * A text limit holds a short value, such as a list of regions. The bound keeps\n * a request readable for the person who approves it.\n */\nexport const ORG_LIMIT_TEXT_MAX_LENGTH = 2000\n\n/**\n * The most users that one purge-users request deletes.\n *\n * The God Mode purge deletes one user after the other, and Auth0 limits how\n * fast that goes. A list longer than this is also a list that no approver\n * reads before they agree.\n */\nexport const PURGE_USERS_MAX_USERS = 1000\n\n/**\n * What a person must hold to open or to approve a purge-users request.\n *\n * The purge of God Mode is behind `GOD_PURGE_USERS`, and each deletion calls a\n * route behind `DELETE_USER`. The use case of control-backend returns this\n * list from `permissionsFor`, and the form passes it to `canCreate`, so the\n * button and the server never disagree.\n */\nexport const PURGE_USERS_REQUIRED_PERMISSIONS: readonly string[] = [\n AccountsPermissions.GOD_PURGE_USERS,\n AccountsPermissions.DELETE_USER,\n]\n\n/**\n * The largest metadata object that one update-user-metadata request writes,\n * in characters of its JSON text.\n *\n * This is a bound of the product, and not the limit of Auth0, which is far\n * larger. The approvers read the whole object before they agree, and an\n * object that is longer than this is an object that nobody reads.\n */\nexport const USER_METADATA_MAX_LENGTH = 16_000\n\n/**\n * The largest metadata object that a request carries as the object that the\n * person who asked READ.\n *\n * It is larger than `USER_METADATA_MAX_LENGTH`, so a user whose metadata grew\n * past that bound can still get an edit that makes it smaller. It still has a\n * bound, because the request keeps the copy in its row.\n */\nexport const USER_METADATA_EXPECTED_MAX_LENGTH = 500_000\n\n/** The longest email address that a change-user-email request carries. */\nexport const APPROVAL_EMAIL_MAX_LENGTH = 320\n\n/**\n * The email addresses that a change-user-email request accepts.\n *\n * It is the email pattern of zod 4.6. The pattern is here, so the schema of\n * control-backend and the form of control-frontend accept the same addresses,\n * and a form never sends an address that the service refuses.\n */\nexport const APPROVAL_EMAIL_PATTERN =\n // biome-ignore lint/complexity/noUselessEscapeInRegex: the same source as zod, character for character\n /^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$/\n"],
|
|
5
|
+
"mappings": "AAMA,SAAS,2BAA2B;AAkB7B,MAAM,0BAA0B;AAAA,EACrC,oBAAoB;AAAA,EACpB,aAAa;AAAA,EACb,gBAAgB;AAAA;AAAA;AAAA;AAAA,EAIhB,6BAA6B;AAAA,EAC7B,uBAAuB;AAAA,EACvB,2BAA2B;AAAA,EAC3B,wBAAwB;AAAA,EACxB,qBAAqB;AAAA,EACrB,uBAAuB;AAAA;AAAA,EAEvB,qBAAqB;AAAA,EACrB,sBAAsB;AAAA,EACtB,iBAAiB;AAAA,EACjB,qBAAqB;AAAA,EACrB,iBAAiB;AAAA;AAAA,EAEjB,oBAAoB;AAAA,EACpB,YAAY;AAAA,EACZ,iBAAiB;AAAA,EACjB,YAAY;AAAA,EACZ,oBAAoB;AAAA;AAAA;AAAA,EAGpB,mBAAmB;AAAA;AAAA,EAEnB,4BAA4B;AAAA;AAAA;AAAA,EAG5B,4BAA4B;AAAA,EAC5B,4BAA4B;AAAA;AAAA;AAAA;AAAA,EAI5B,qBAAqB;AAAA;AAAA;AAAA;AAAA;AAAA,EAKrB,uBAAuB;AACzB;AAUO,MAAM,6BAA6B;AAUnC,MAAM,0CAA0C;AAGhD,MAAM,wCAAwC;AAC9C,MAAM,wCAAwC;AAkB9C,MAAM,wBAAwB;AAQ9B,MAAM,wBAAwB;AAS9B,MAAM,iCAAiC;AAQvC,MAAM,4BAA4B;AASlC,MAAM,wBAAwB;AAU9B,MAAM,mCAAsD;AAAA,EACjE,oBAAoB;AAAA,EACpB,oBAAoB;AACtB;AAUO,MAAM,2BAA2B;AAUjC,MAAM,oCAAoC;AAG1C,MAAM,4BAA4B;AASlC,MAAM;AAAA;AAAA,EAEX;AAAA;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/constants/{customer-hardware-sizing.const.d.ts → custom-hardware-sizing.const.d.ts}
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Custom hardware sizing: a cluster plan that a Camundi creates for an
|
|
3
3
|
* organization of their own, with no admin permission. The approval use cases
|
|
4
|
-
* `create-
|
|
4
|
+
* `create-custom-hardware-sizing` and `remove-custom-hardware-sizing` run
|
|
5
5
|
* on dev only, and the limits in this file keep one plan from asking for a
|
|
6
6
|
* machine park.
|
|
7
7
|
*
|
|
@@ -11,23 +11,23 @@
|
|
|
11
11
|
* `G3-M (adapted for Cisco load test)` ask for 10 TB on purpose, and a cap
|
|
12
12
|
* from them would be 100 TB.
|
|
13
13
|
*/
|
|
14
|
-
/** The stage where
|
|
15
|
-
export declare const
|
|
14
|
+
/** The stage where custom hardware sizing runs. */
|
|
15
|
+
export declare const isCustomHardwareSizingStage: (stage: string) => boolean;
|
|
16
16
|
/**
|
|
17
|
-
* The most plans that one Camundi can own through
|
|
17
|
+
* The most plans that one Camundi can own through custom hardware sizing.
|
|
18
18
|
*
|
|
19
19
|
* The count is the number of plans the Camundi created. A plan that the
|
|
20
20
|
* Camundi deleted does not count.
|
|
21
21
|
*/
|
|
22
|
-
export declare const
|
|
23
|
-
/** The shortest and longest name of a
|
|
24
|
-
export declare const
|
|
25
|
-
export declare const
|
|
22
|
+
export declare const CUSTOM_HARDWARE_SIZING_MAX_PLANS_PER_REQUESTER = 10;
|
|
23
|
+
/** The shortest and longest name of a custom hardware sizing plan. */
|
|
24
|
+
export declare const CUSTOM_HARDWARE_SIZING_NAME_MIN_LENGTH = 3;
|
|
25
|
+
export declare const CUSTOM_HARDWARE_SIZING_NAME_MAX_LENGTH = 100;
|
|
26
26
|
/**
|
|
27
27
|
* The longest plan, as JSON text. The largest plan of prod is about 6000
|
|
28
28
|
* characters. The cap stops a request that carries a huge object.
|
|
29
29
|
*/
|
|
30
|
-
export declare const
|
|
30
|
+
export declare const CUSTOM_HARDWARE_SIZING_MAX_PLAN_JSON_LENGTH = 50000;
|
|
31
31
|
/**
|
|
32
32
|
* Caps for the requests and the limits of one component.
|
|
33
33
|
*
|
|
@@ -40,7 +40,7 @@ export declare const CUSTOMER_HARDWARE_SIZING_MAX_PLAN_JSON_LENGTH = 50000;
|
|
|
40
40
|
* - Elasticsearch: 30 CPU, 100 GiB, 3000 GiB of storage (no load test)
|
|
41
41
|
* - other: 6 CPU, 30 GiB, no storage
|
|
42
42
|
*/
|
|
43
|
-
export declare const
|
|
43
|
+
export declare const CUSTOM_HARDWARE_SIZING_RESOURCE_CAPS: {
|
|
44
44
|
readonly broker: {
|
|
45
45
|
readonly cpu: 160;
|
|
46
46
|
readonly memoryGi: 500;
|
|
@@ -58,7 +58,7 @@ export declare const CUSTOMER_HARDWARE_SIZING_RESOURCE_CAPS: {
|
|
|
58
58
|
};
|
|
59
59
|
};
|
|
60
60
|
/** Caps for the count fields of a plan. Prod maximum is in the comments. */
|
|
61
|
-
export declare const
|
|
61
|
+
export declare const CUSTOM_HARDWARE_SIZING_COUNT_CAPS: {
|
|
62
62
|
readonly clusterSize: 70;
|
|
63
63
|
readonly partitionsCount: 500;
|
|
64
64
|
readonly replicationFactor: 30;
|
|
@@ -66,7 +66,7 @@ export declare const CUSTOMER_HARDWARE_SIZING_COUNT_CAPS: {
|
|
|
66
66
|
readonly replicas: 30;
|
|
67
67
|
};
|
|
68
68
|
/** `zeebe.broker.storage.autoResizing.increase`. Prod maximum is 18 GiB. */
|
|
69
|
-
export declare const
|
|
69
|
+
export declare const CUSTOM_HARDWARE_SIZING_MAX_AUTO_RESIZE_INCREASE_GI = 180;
|
|
70
70
|
/**
|
|
71
71
|
* The caps over the whole broker set, which the caps for one broker do not
|
|
72
72
|
* give. A plan with 70 brokers of 160 CPU passes every cap above, and it asks
|
|
@@ -74,7 +74,7 @@ export declare const CUSTOMER_HARDWARE_SIZING_MAX_AUTO_RESIZE_INCREASE_GI = 180;
|
|
|
74
74
|
* 250 GiB of memory (5 brokers, `GLS HA`) and 2500 GiB of storage (5 brokers,
|
|
75
75
|
* `GLS HA`, no load test).
|
|
76
76
|
*/
|
|
77
|
-
export declare const
|
|
77
|
+
export declare const CUSTOM_HARDWARE_SIZING_BROKER_TOTAL_CAPS: {
|
|
78
78
|
readonly cpu: 840;
|
|
79
79
|
readonly memoryGi: 2500;
|
|
80
80
|
readonly storageGi: 25000;
|
|
@@ -85,7 +85,7 @@ export declare const CUSTOMER_HARDWARE_SIZING_BROKER_TOTAL_CAPS: {
|
|
|
85
85
|
* (5 nodes, `CCL G3L HA`). The storage of one node is a claim of one volume,
|
|
86
86
|
* so the total is the claim times the number of nodes.
|
|
87
87
|
*/
|
|
88
|
-
export declare const
|
|
88
|
+
export declare const CUSTOM_HARDWARE_SIZING_ELASTICSEARCH_TOTAL_CAPS: {
|
|
89
89
|
readonly cpu: 900;
|
|
90
90
|
readonly memoryGi: 3000;
|
|
91
91
|
readonly storageGi: 100000;
|
|
@@ -98,7 +98,7 @@ export declare const CUSTOMER_HARDWARE_SIZING_ELASTICSEARCH_TOTAL_CAPS: {
|
|
|
98
98
|
* the cap for one pod, 10 times over. The check counts a storage request of a
|
|
99
99
|
* pod in `resources` against it.
|
|
100
100
|
*/
|
|
101
|
-
export declare const
|
|
101
|
+
export declare const CUSTOM_HARDWARE_SIZING_REPLICA_SET_TOTAL_CAPS: {
|
|
102
102
|
readonly cpu: 120;
|
|
103
103
|
readonly memoryGi: 600;
|
|
104
104
|
readonly storageGi: 50000;
|
|
@@ -112,7 +112,7 @@ export declare const CUSTOMER_HARDWARE_SIZING_REPLICA_SET_TOTAL_CAPS: {
|
|
|
112
112
|
*/
|
|
113
113
|
export declare function parseKubernetesQuantity(value: unknown): number | undefined;
|
|
114
114
|
/**
|
|
115
|
-
* Checks a cluster plan against the caps of
|
|
115
|
+
* Checks a cluster plan against the caps of custom hardware sizing.
|
|
116
116
|
*
|
|
117
117
|
* Returns the list of violations, and an empty list when the plan is within
|
|
118
118
|
* the caps. It checks the size only. The shape of the plan is the job of the
|
|
@@ -121,4 +121,4 @@ export declare function parseKubernetesQuantity(value: unknown): number | undefi
|
|
|
121
121
|
* A value that this function cannot read is a violation too: a cap that the
|
|
122
122
|
* plan can skip with a strange spelling is no cap.
|
|
123
123
|
*/
|
|
124
|
-
export declare function
|
|
124
|
+
export declare function checkCustomHardwareSizing(plan: unknown): string[];
|
package/dist/constants/{customer-hardware-sizing.const.js → custom-hardware-sizing.const.js}
RENAMED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { canonicalPlan } from "../utils/elasticsearchPlanPath.utils.js";
|
|
2
|
-
const
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const
|
|
2
|
+
const isCustomHardwareSizingStage = (stage) => stage === "dev";
|
|
3
|
+
const CUSTOM_HARDWARE_SIZING_MAX_PLANS_PER_REQUESTER = 10;
|
|
4
|
+
const CUSTOM_HARDWARE_SIZING_NAME_MIN_LENGTH = 3;
|
|
5
|
+
const CUSTOM_HARDWARE_SIZING_NAME_MAX_LENGTH = 100;
|
|
6
|
+
const CUSTOM_HARDWARE_SIZING_MAX_PLAN_JSON_LENGTH = 5e4;
|
|
7
|
+
const CUSTOM_HARDWARE_SIZING_RESOURCE_CAPS = {
|
|
8
8
|
broker: { cpu: 160, memoryGi: 500, storageGi: 5e3 },
|
|
9
9
|
elasticsearch: { cpu: 300, memoryGi: 1e3, storageGi: 3e4 },
|
|
10
10
|
other: { cpu: 60, memoryGi: 300, storageGi: 5e3 }
|
|
11
11
|
};
|
|
12
|
-
const
|
|
12
|
+
const CUSTOM_HARDWARE_SIZING_COUNT_CAPS = {
|
|
13
13
|
// 7
|
|
14
14
|
clusterSize: 70,
|
|
15
15
|
// 50
|
|
@@ -21,18 +21,18 @@ const CUSTOMER_HARDWARE_SIZING_COUNT_CAPS = {
|
|
|
21
21
|
// `replicas` of any component. 3.
|
|
22
22
|
replicas: 30
|
|
23
23
|
};
|
|
24
|
-
const
|
|
25
|
-
const
|
|
24
|
+
const CUSTOM_HARDWARE_SIZING_MAX_AUTO_RESIZE_INCREASE_GI = 180;
|
|
25
|
+
const CUSTOM_HARDWARE_SIZING_BROKER_TOTAL_CAPS = {
|
|
26
26
|
cpu: 840,
|
|
27
27
|
memoryGi: 2500,
|
|
28
28
|
storageGi: 25e3
|
|
29
29
|
};
|
|
30
|
-
const
|
|
30
|
+
const CUSTOM_HARDWARE_SIZING_ELASTICSEARCH_TOTAL_CAPS = {
|
|
31
31
|
cpu: 900,
|
|
32
32
|
memoryGi: 3e3,
|
|
33
33
|
storageGi: 1e5
|
|
34
34
|
};
|
|
35
|
-
const
|
|
35
|
+
const CUSTOM_HARDWARE_SIZING_REPLICA_SET_TOTAL_CAPS = {
|
|
36
36
|
cpu: 120,
|
|
37
37
|
memoryGi: 600,
|
|
38
38
|
storageGi: 5e4
|
|
@@ -87,7 +87,7 @@ function checkNumberCap(violations, path, value, cap) {
|
|
|
87
87
|
}
|
|
88
88
|
function checkResource(violations, path, value, kind) {
|
|
89
89
|
const name = path.join(".");
|
|
90
|
-
const caps =
|
|
90
|
+
const caps = CUSTOM_HARDWARE_SIZING_RESOURCE_CAPS[componentOf(path)];
|
|
91
91
|
const parsed = parseKubernetesQuantity(value);
|
|
92
92
|
if (parsed === void 0) {
|
|
93
93
|
violations.push(`${name} is not a quantity that can be checked`);
|
|
@@ -115,12 +115,12 @@ function walk(violations, node, path) {
|
|
|
115
115
|
const next = [...path, key];
|
|
116
116
|
const parent = path.at(-1);
|
|
117
117
|
const grandParent = path.at(-2);
|
|
118
|
-
if (Object.hasOwn(
|
|
118
|
+
if (Object.hasOwn(CUSTOM_HARDWARE_SIZING_COUNT_CAPS, key) && !isRecord(value)) {
|
|
119
119
|
checkNumberCap(
|
|
120
120
|
violations,
|
|
121
121
|
next,
|
|
122
122
|
value,
|
|
123
|
-
|
|
123
|
+
CUSTOM_HARDWARE_SIZING_COUNT_CAPS[key]
|
|
124
124
|
);
|
|
125
125
|
} else if ((parent === "limits" || parent === "requests") && grandParent === "resources" && (key === "cpu" || key === "memory" || key === "storage")) {
|
|
126
126
|
checkResource(violations, next, value, key);
|
|
@@ -130,9 +130,9 @@ function walk(violations, node, path) {
|
|
|
130
130
|
checkResource(violations, next, value, "storage");
|
|
131
131
|
} else if (parent === "autoResizing" && key === "increase") {
|
|
132
132
|
const parsed = parseKubernetesQuantity(value);
|
|
133
|
-
if (parsed === void 0 || parsed / GIBIBYTE >
|
|
133
|
+
if (parsed === void 0 || parsed / GIBIBYTE > CUSTOM_HARDWARE_SIZING_MAX_AUTO_RESIZE_INCREASE_GI) {
|
|
134
134
|
violations.push(
|
|
135
|
-
`${next.join(".")} is not a quantity of at most ${
|
|
135
|
+
`${next.join(".")} is not a quantity of at most ${CUSTOM_HARDWARE_SIZING_MAX_AUTO_RESIZE_INCREASE_GI} GiB`
|
|
136
136
|
);
|
|
137
137
|
}
|
|
138
138
|
} else {
|
|
@@ -191,7 +191,7 @@ function checkReplicaSets(violations, node, path) {
|
|
|
191
191
|
storageResources: podResources,
|
|
192
192
|
legacyStorage: void 0
|
|
193
193
|
},
|
|
194
|
-
|
|
194
|
+
CUSTOM_HARDWARE_SIZING_REPLICA_SET_TOTAL_CAPS
|
|
195
195
|
);
|
|
196
196
|
}
|
|
197
197
|
for (const [key, value] of Object.entries(node)) {
|
|
@@ -212,7 +212,7 @@ function checkNodeSetTotals(violations, plan) {
|
|
|
212
212
|
storageResources: storage?.resources,
|
|
213
213
|
legacyStorage: broker.persistentStorageCapacity
|
|
214
214
|
},
|
|
215
|
-
|
|
215
|
+
CUSTOM_HARDWARE_SIZING_BROKER_TOTAL_CAPS
|
|
216
216
|
);
|
|
217
217
|
}
|
|
218
218
|
checkReplicaSets(violations, plan, []);
|
|
@@ -229,17 +229,17 @@ function checkNodeSetTotals(violations, plan) {
|
|
|
229
229
|
storageResources: storage?.resources,
|
|
230
230
|
legacyStorage: elasticsearch.persistentStorageCapacity
|
|
231
231
|
},
|
|
232
|
-
|
|
232
|
+
CUSTOM_HARDWARE_SIZING_ELASTICSEARCH_TOTAL_CAPS
|
|
233
233
|
);
|
|
234
234
|
}
|
|
235
235
|
}
|
|
236
|
-
function
|
|
236
|
+
function checkCustomHardwareSizing(plan) {
|
|
237
237
|
if (!isRecord(plan)) {
|
|
238
238
|
return ["the plan is not an object"];
|
|
239
239
|
}
|
|
240
|
-
if (JSON.stringify(plan).length >
|
|
240
|
+
if (JSON.stringify(plan).length > CUSTOM_HARDWARE_SIZING_MAX_PLAN_JSON_LENGTH) {
|
|
241
241
|
return [
|
|
242
|
-
`the plan is longer than ${
|
|
242
|
+
`the plan is longer than ${CUSTOM_HARDWARE_SIZING_MAX_PLAN_JSON_LENGTH} characters`
|
|
243
243
|
];
|
|
244
244
|
}
|
|
245
245
|
const canonical = canonicalPlan(plan);
|
|
@@ -249,18 +249,18 @@ function checkCustomerHardwareSizing(plan) {
|
|
|
249
249
|
return violations;
|
|
250
250
|
}
|
|
251
251
|
export {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
252
|
+
CUSTOM_HARDWARE_SIZING_BROKER_TOTAL_CAPS,
|
|
253
|
+
CUSTOM_HARDWARE_SIZING_COUNT_CAPS,
|
|
254
|
+
CUSTOM_HARDWARE_SIZING_ELASTICSEARCH_TOTAL_CAPS,
|
|
255
|
+
CUSTOM_HARDWARE_SIZING_MAX_AUTO_RESIZE_INCREASE_GI,
|
|
256
|
+
CUSTOM_HARDWARE_SIZING_MAX_PLANS_PER_REQUESTER,
|
|
257
|
+
CUSTOM_HARDWARE_SIZING_MAX_PLAN_JSON_LENGTH,
|
|
258
|
+
CUSTOM_HARDWARE_SIZING_NAME_MAX_LENGTH,
|
|
259
|
+
CUSTOM_HARDWARE_SIZING_NAME_MIN_LENGTH,
|
|
260
|
+
CUSTOM_HARDWARE_SIZING_REPLICA_SET_TOTAL_CAPS,
|
|
261
|
+
CUSTOM_HARDWARE_SIZING_RESOURCE_CAPS,
|
|
262
|
+
checkCustomHardwareSizing,
|
|
263
|
+
isCustomHardwareSizingStage,
|
|
264
264
|
parseKubernetesQuantity
|
|
265
265
|
};
|
|
266
|
-
//# sourceMappingURL=
|
|
266
|
+
//# sourceMappingURL=custom-hardware-sizing.const.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/constants/custom-hardware-sizing.const.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 { canonicalPlan } from '../utils/elasticsearchPlanPath.utils.js'\n\n/**\n * Custom hardware sizing: a cluster plan that a Camundi creates for an\n * organization of their own, with no admin permission. The approval use cases\n * `create-custom-hardware-sizing` and `remove-custom-hardware-sizing` run\n * on dev only, and the limits in this file keep one plan from asking for a\n * machine park.\n *\n * Every cap below is 10 times the largest value in the cluster plans of prod\n * (dump of 2026-10-01, 27 plans). The cap for storage is 10 times the largest\n * value of a plan that is NOT a load test: `Cisco XXL Year 1` and\n * `G3-M (adapted for Cisco load test)` ask for 10 TB on purpose, and a cap\n * from them would be 100 TB.\n */\n\n/** The stage where custom hardware sizing runs. */\nexport const isCustomHardwareSizingStage = (stage: string): boolean =>\n stage === 'dev'\n\n/**\n * The most plans that one Camundi can own through custom hardware sizing.\n *\n * The count is the number of plans the Camundi created. A plan that the\n * Camundi deleted does not count.\n */\nexport const CUSTOM_HARDWARE_SIZING_MAX_PLANS_PER_REQUESTER = 10\n\n/** The shortest and longest name of a custom hardware sizing plan. */\nexport const CUSTOM_HARDWARE_SIZING_NAME_MIN_LENGTH = 3\nexport const CUSTOM_HARDWARE_SIZING_NAME_MAX_LENGTH = 100\n\n/**\n * The longest plan, as JSON text. The largest plan of prod is about 6000\n * characters. The cap stops a request that carries a huge object.\n */\nexport const CUSTOM_HARDWARE_SIZING_MAX_PLAN_JSON_LENGTH = 50_000\n\n/** One cap for a resource of one kind of component. `gi` is gibibytes. */\ninterface ResourceCaps {\n cpu: number\n memoryGi: number\n storageGi: number\n}\n\n/**\n * Caps for the requests and the limits of one component.\n *\n * `other` is every component that is not the Zeebe broker or Elasticsearch:\n * Operate, Tasklist, Optimize, the gateways, Identity and the others. The\n * largest of them in prod is Operate, with 6 CPU and 30 GiB.\n *\n * Prod maximum, before the factor of 10:\n * - broker: 16 CPU, 50 GiB, 500 GiB of storage (no load test)\n * - Elasticsearch: 30 CPU, 100 GiB, 3000 GiB of storage (no load test)\n * - other: 6 CPU, 30 GiB, no storage\n */\nexport const CUSTOM_HARDWARE_SIZING_RESOURCE_CAPS = {\n broker: { cpu: 160, memoryGi: 500, storageGi: 5000 },\n elasticsearch: { cpu: 300, memoryGi: 1000, storageGi: 30_000 },\n other: { cpu: 60, memoryGi: 300, storageGi: 5000 },\n} as const satisfies Record<string, ResourceCaps>\n\n/** Caps for the count fields of a plan. Prod maximum is in the comments. */\nexport const CUSTOM_HARDWARE_SIZING_COUNT_CAPS = {\n // 7\n clusterSize: 70,\n // 50\n partitionsCount: 500,\n // 3\n replicationFactor: 30,\n // 5\n nodesCount: 50,\n // `replicas` of any component. 3.\n replicas: 30,\n} as const\n\n/** `zeebe.broker.storage.autoResizing.increase`. Prod maximum is 18 GiB. */\nexport const CUSTOM_HARDWARE_SIZING_MAX_AUTO_RESIZE_INCREASE_GI = 180\n\n/**\n * The caps over the whole broker set, which the caps for one broker do not\n * give. A plan with 70 brokers of 160 CPU passes every cap above, and it asks\n * for 11200 CPU. The prod maximum is 84 CPU (7 brokers, `Cisco XXL Year 1`),\n * 250 GiB of memory (5 brokers, `GLS HA`) and 2500 GiB of storage (5 brokers,\n * `GLS HA`, no load test).\n */\nexport const CUSTOM_HARDWARE_SIZING_BROKER_TOTAL_CAPS = {\n cpu: 840,\n memoryGi: 2500,\n storageGi: 25_000,\n} as const\n\n/**\n * The same, for the whole Elasticsearch node set. Ten times the prod maximum:\n * 90 CPU and 300 GiB of memory (3 nodes, `GLS HA`), and 10000 GiB of storage\n * (5 nodes, `CCL G3L HA`). The storage of one node is a claim of one volume,\n * so the total is the claim times the number of nodes.\n */\nexport const CUSTOM_HARDWARE_SIZING_ELASTICSEARCH_TOTAL_CAPS = {\n cpu: 900,\n memoryGi: 3000,\n storageGi: 100_000,\n} as const\n\n/**\n * The same, for the pods of one component that has `replicas`, such as\n * Operate or the gateway. Ten times the prod maximum: 12 CPU (the gateway, 3\n * replicas of 4 CPU) and 60 GiB of memory (Operate, 2 replicas of 30 GiB). No\n * component of prod has storage in its pod resources, so the storage total is\n * the cap for one pod, 10 times over. The check counts a storage request of a\n * pod in `resources` against it.\n */\nexport const CUSTOM_HARDWARE_SIZING_REPLICA_SET_TOTAL_CAPS = {\n cpu: 120,\n memoryGi: 600,\n storageGi: 50_000,\n} as const\n\nconst GIBIBYTE = 1024 ** 3\n\nconst QUANTITY_MULTIPLIERS: Record<string, number> = {\n '': 1,\n m: 1e-3,\n k: 1e3,\n M: 1e6,\n G: 1e9,\n T: 1e12,\n P: 1e15,\n Ki: 1024,\n Mi: 1024 ** 2,\n Gi: 1024 ** 3,\n Ti: 1024 ** 4,\n Pi: 1024 ** 5,\n}\n\nconst QUANTITY_PATTERN = /^([0-9]+(?:\\.[0-9]+)?)(m|k|M|G|T|P|Ki|Mi|Gi|Ti|Pi)?$/u\n\n/**\n * Reads a Kubernetes quantity such as `500m`, `2`, `4Gi` or `1G`, and returns\n * its value in base units: cores for a CPU, bytes for memory and storage.\n *\n * Returns `undefined` for a text that this function cannot read, so the\n * caller can refuse it. The exponent form (`1e3`) is not read on purpose.\n */\nexport function parseKubernetesQuantity(value: unknown): number | undefined {\n if (typeof value === 'number') {\n return Number.isFinite(value) && value >= 0 ? value : undefined\n }\n if (typeof value !== 'string') {\n return undefined\n }\n const match = QUANTITY_PATTERN.exec(value)\n if (!match) {\n return undefined\n }\n return Number(match[1]) * (QUANTITY_MULTIPLIERS[match[2] ?? ''] ?? 1)\n}\n\ntype ComponentKind = keyof typeof CUSTOM_HARDWARE_SIZING_RESOURCE_CAPS\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\nfunction componentOf(path: string[]): ComponentKind {\n if (path[0] === 'zeebe' && path[1] === 'broker') {\n return 'broker'\n }\n if (path[0] === 'elasticsearch') {\n return 'elasticsearch'\n }\n return 'other'\n}\n\nfunction checkNumberCap(\n violations: string[],\n path: string[],\n value: unknown,\n cap: number,\n): void {\n const name = path.join('.')\n const parsed = parseKubernetesQuantity(value)\n if (parsed === undefined) {\n violations.push(`${name} is not a number that can be checked`)\n } else if (parsed > cap) {\n violations.push(`${name} is ${parsed}, and the most is ${cap}`)\n }\n}\n\nfunction checkResource(\n violations: string[],\n path: string[],\n value: unknown,\n kind: 'cpu' | 'memory' | 'storage',\n): void {\n const name = path.join('.')\n const caps = CUSTOM_HARDWARE_SIZING_RESOURCE_CAPS[componentOf(path)]\n const parsed = parseKubernetesQuantity(value)\n if (parsed === undefined) {\n violations.push(`${name} is not a quantity that can be checked`)\n return\n }\n if (kind === 'cpu') {\n if (parsed > caps.cpu) {\n violations.push(`${name} is ${parsed} CPU, and the most is ${caps.cpu}`)\n }\n return\n }\n const cap = kind === 'memory' ? caps.memoryGi : caps.storageGi\n if (parsed / GIBIBYTE > cap) {\n violations.push(`${name} is over ${cap} GiB`)\n }\n}\n\nfunction walk(violations: string[], node: unknown, path: string[]): void {\n if (!isRecord(node)) {\n return\n }\n for (const [key, value] of Object.entries(node)) {\n // The shape check of console treats `null` like an absent key, so this\n // check does too. A `null` has nothing to cap.\n if (value === null || value === undefined) {\n continue\n }\n const next = [...path, key]\n const parent = path.at(-1)\n const grandParent = path.at(-2)\n if (\n Object.hasOwn(CUSTOM_HARDWARE_SIZING_COUNT_CAPS, key) &&\n !isRecord(value)\n ) {\n checkNumberCap(\n violations,\n next,\n value,\n CUSTOM_HARDWARE_SIZING_COUNT_CAPS[\n key as keyof typeof CUSTOM_HARDWARE_SIZING_COUNT_CAPS\n ],\n )\n } else if (\n (parent === 'limits' || parent === 'requests') &&\n grandParent === 'resources' &&\n (key === 'cpu' || key === 'memory' || key === 'storage')\n ) {\n checkResource(violations, next, value, key)\n } else if (\n (parent === 'limits' || parent === 'requests') &&\n grandParent === 'resources'\n ) {\n // Any other resource name (`ephemeral-storage`, a GPU) has no cap.\n violations.push(`${next.join('.')} is not a resource that has a cap`)\n } else if (key === 'persistentStorageCapacity') {\n // The legacy size of the volume. The operator can still read it.\n checkResource(violations, next, value, 'storage')\n } else if (parent === 'autoResizing' && key === 'increase') {\n // Only the step is capped. The disk itself stops at\n // `storage.resources.limits.storage`: `Plan.type.ts` calls it \"the upper\n // limit the pvc autoscaler can scale up to\", and the operator hands it\n // to the autoscaler as the storage limit of the PVC. The caps for that\n // field, for one node and for all nodes together, bound the size that a\n // disk can reach. `threshold` has no effect on that size.\n const parsed = parseKubernetesQuantity(value)\n if (\n parsed === undefined ||\n parsed / GIBIBYTE > CUSTOM_HARDWARE_SIZING_MAX_AUTO_RESIZE_INCREASE_GI\n ) {\n violations.push(\n `${next.join('.')} is not a quantity of at most ${CUSTOM_HARDWARE_SIZING_MAX_AUTO_RESIZE_INCREASE_GI} GiB`,\n )\n }\n } else {\n walk(violations, value, next)\n }\n }\n}\n\nconst larger = (\n block: Record<string, unknown> | undefined,\n key: string,\n): number | undefined => {\n const values = [block?.limits, block?.requests].map((entry) =>\n isRecord(entry) ? parseKubernetesQuantity(entry[key]) : undefined,\n )\n const known = values.filter((value) => value !== undefined)\n return known.length > 0 ? Math.max(...known) : undefined\n}\n\n/**\n * Checks the whole node set of one component against its totals. The caps for\n * one node do not bound `count` nodes together.\n *\n * Kubernetes schedules on `requests`, and a plan can set only `requests`. The\n * total reads the larger of the two, so that neither one skips it. The legacy\n * `persistentStorageCapacity` counts as storage too.\n */\nfunction checkSetTotals(\n violations: string[],\n who: string,\n count: unknown,\n parts: {\n resources: unknown\n storageResources: unknown\n legacyStorage: unknown\n },\n caps: { cpu: number; memoryGi: number; storageGi: number },\n): void {\n const nodes = parseKubernetesQuantity(count)\n if (nodes === undefined) {\n return\n }\n const resources = isRecord(parts.resources) ? parts.resources : undefined\n const storageResources = isRecord(parts.storageResources)\n ? parts.storageResources\n : undefined\n const cpu = larger(resources, 'cpu')\n if (cpu !== undefined && cpu * nodes > caps.cpu) {\n violations.push(`${who} together ask for more than ${caps.cpu} CPU`)\n }\n const memory = larger(resources, 'memory')\n if (memory !== undefined && (memory * nodes) / GIBIBYTE > caps.memoryGi) {\n violations.push(\n `${who} together ask for more than ${caps.memoryGi} GiB of memory`,\n )\n }\n const disks = [\n larger(storageResources, 'storage'),\n parseKubernetesQuantity(parts.legacyStorage),\n ].filter((value) => value !== undefined)\n if (\n disks.length > 0 &&\n (Math.max(...disks) * nodes) / GIBIBYTE > caps.storageGi\n ) {\n violations.push(\n `${who} together ask for more than ${caps.storageGi} GiB of storage`,\n )\n }\n}\n\n/**\n * Walks the plan for every component that has `replicas`, and checks its pods\n * together. The pod resources are in `resources`, or in `backend.resources`.\n * The brokers (`clusterSize`) and Elasticsearch (`nodesCount`) have their own\n * count and their own totals.\n */\nfunction checkReplicaSets(\n violations: string[],\n node: unknown,\n path: string[],\n): void {\n if (!isRecord(node)) {\n return\n }\n if (\n node.replicas !== undefined &&\n node.replicas !== null &&\n !isRecord(node.replicas)\n ) {\n const backend = isRecord(node.backend) ? node.backend : undefined\n const podResources = node.resources ?? backend?.resources\n checkSetTotals(\n violations,\n `the replicas of ${path.join('.') || 'the plan'}`,\n node.replicas,\n {\n resources: podResources,\n // The same block: a pod can ask for storage in its resources too.\n storageResources: podResources,\n legacyStorage: undefined,\n },\n CUSTOM_HARDWARE_SIZING_REPLICA_SET_TOTAL_CAPS,\n )\n }\n for (const [key, value] of Object.entries(node)) {\n checkReplicaSets(violations, value, [...path, key])\n }\n}\n\nfunction checkNodeSetTotals(\n violations: string[],\n plan: Record<string, unknown>,\n): void {\n const zeebe = isRecord(plan.zeebe) ? plan.zeebe : undefined\n const broker = isRecord(zeebe?.broker) ? zeebe.broker : undefined\n if (broker) {\n const storage = isRecord(broker.storage) ? broker.storage : undefined\n checkSetTotals(\n violations,\n 'the brokers',\n broker.clusterSize,\n {\n resources: broker.resources,\n storageResources: storage?.resources,\n legacyStorage: broker.persistentStorageCapacity,\n },\n CUSTOM_HARDWARE_SIZING_BROKER_TOTAL_CAPS,\n )\n }\n checkReplicaSets(violations, plan, [])\n const elasticsearch = isRecord(plan.elasticsearch)\n ? plan.elasticsearch\n : undefined\n if (elasticsearch) {\n const config = isRecord(elasticsearch.config)\n ? elasticsearch.config\n : undefined\n const storage = isRecord(config?.storage) ? config.storage : undefined\n checkSetTotals(\n violations,\n 'the Elasticsearch nodes',\n config?.nodesCount,\n {\n resources: elasticsearch.resources,\n storageResources: storage?.resources,\n legacyStorage: elasticsearch.persistentStorageCapacity,\n },\n CUSTOM_HARDWARE_SIZING_ELASTICSEARCH_TOTAL_CAPS,\n )\n }\n}\n\n/**\n * Checks a cluster plan against the caps of custom hardware sizing.\n *\n * Returns the list of violations, and an empty list when the plan is within\n * the caps. It checks the size only. The shape of the plan is the job of the\n * plan validator of console-backend.\n *\n * A value that this function cannot read is a violation too: a cap that the\n * plan can skip with a strange spelling is no cap.\n */\nexport function checkCustomHardwareSizing(plan: unknown): string[] {\n if (!isRecord(plan)) {\n return ['the plan is not an object']\n }\n if (\n JSON.stringify(plan).length > CUSTOM_HARDWARE_SIZING_MAX_PLAN_JSON_LENGTH\n ) {\n return [\n `the plan is longer than ${CUSTOM_HARDWARE_SIZING_MAX_PLAN_JSON_LENGTH} characters`,\n ]\n }\n // `operate.elasticsearch` is the old place of the Elasticsearch block. The\n // caps for Elasticsearch must read it as one place, so the plan is moved\n // to the canonical form first.\n const canonical = canonicalPlan(plan)\n const violations: string[] = []\n walk(violations, canonical, [])\n checkNodeSetTotals(violations, canonical)\n return violations\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -516,8 +516,8 @@ export interface ApprovalUseCaseDto {
|
|
|
516
516
|
* source, so that nobody can change what is not theirs. The action can
|
|
517
517
|
* touch an organization only when that check lets the person change it.
|
|
518
518
|
*
|
|
519
|
-
* `create-personal-organisation` and the two use cases of
|
|
520
|
-
* sizing set it. The two use cases of
|
|
519
|
+
* `create-personal-organisation` and the two use cases of custom hardware
|
|
520
|
+
* sizing set it. The two use cases of custom hardware sizing do the second
|
|
521
521
|
* check in two ways:
|
|
522
522
|
*
|
|
523
523
|
* - Creating a plan, and unlinking a plan from one organization, read the
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/dtos/ApprovalJob.dto.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 wire types of the generic approval mechanism.\n *\n * This file holds no logic and no dependency, because three consumers read it:\n * control-backend, control-frontend and cloudbot. The rules that read these\n * types are in `../approvals/policy.js`, and they are pure for the same reason.\n */\n\n/**\n * Every state that an approval request can hold.\n *\n * A `const` tuple, not an enum, so it is both the union type and the list that\n * validates a status filter at run time.\n *\n * The path of a request: `pending` until it reaches the threshold. Then\n * `executing` (automatic mode) or `approved` (explicit mode, which waits for a\n * person to press the button). `executed` or `failed` after the action ran. A\n * request that never reaches the threshold ends `rejected`, `withdrawn` or\n * `expired`.\n */\nexport const APPROVAL_JOB_STATUSES = [\n 'pending',\n 'approved',\n 'executing',\n 'executed',\n 'failed',\n 'rejected',\n 'withdrawn',\n 'expired',\n] as const\n\nexport type ApprovalJobStatus = (typeof APPROVAL_JOB_STATUSES)[number]\n\n/**\n * The states in which the action of a request is still COMING.\n *\n * `pending` waits for approvals, `approved` waits for a person to press the\n * button, and `executing` is running now. Each of them can still change the\n * world, so two requests for one target must not be in these states together.\n * `dedupeKey` reads this set for that reason.\n *\n * `failed` is NOT here, although a person can run a failed request again. A\n * failed request runs only when a person presses the button, and that person\n * sees the row. Blocking a new request on a failed one would also trap the\n * key: `canWithdraw` accepts `pending` alone, so nobody could close it.\n *\n * `executed`, `rejected`, `withdrawn` and `expired` are the end of a request.\n */\nexport const APPROVAL_LIVE_STATUSES = [\n 'pending',\n 'approved',\n 'executing',\n] as const satisfies readonly ApprovalJobStatus[]\n\n/**\n * How many requests one page of the list holds when the caller asks for no\n * number.\n *\n * The list is a work queue that a Camundi reads, so the page is large: a\n * person wants the whole queue and not ten rows at a time. What the number is\n * for is the ceiling below.\n */\nexport const APPROVAL_PAGE_SIZE_DEFAULT = 500\n\n/**\n * The largest page that the service builds, and the largest that a caller may\n * ask for. The API answers 400 above it.\n *\n * The list had no limit at all. One authenticated caller could then make the\n * service read the whole table into memory, serialize it, and do it again, so\n * the cost of a request grew with the age of the service and with nothing that\n * the caller had to pay for. A ceiling turns that into a fixed cost.\n *\n * It is HERE and not only in the service, because the view must know it: a\n * page that asked for more would get a 400 instead of rows.\n */\nexport const APPROVAL_PAGE_SIZE_MAX = 2000\n\n/**\n * The columns that the queue can be ordered by.\n *\n * A NAME and not an SQL expression: the repository maps each one, so a caller\n * can never name a column that this list does not hold. `status` orders by the\n * EFFECTIVE status, because a `pending` row whose time ran out reads as\n * `expired` everywhere else.\n */\nexport const APPROVAL_SORT_FIELDS = [\n 'createdAt',\n 'expiresAt',\n 'status',\n 'type',\n 'createdBy',\n] as const\n\nexport type ApprovalSortField = (typeof APPROVAL_SORT_FIELDS)[number]\n\nexport type ApprovalSortOrder = 'asc' | 'desc'\n\n/** The default order of the queue: the newest request first. */\nexport const APPROVAL_SORT_DEFAULT: {\n sort: ApprovalSortField\n order: ApprovalSortOrder\n} = { sort: 'createdAt', order: 'desc' }\n\n/**\n * The longest reason that a request, a rejection or a withdrawal accepts.\n *\n * ONE number, because three routes of control-backend hold it and three fields\n * of control-frontend must stop there. A field that let a person write more\n * than the schema accepts turns their sentence into a 400 after they wrote it.\n */\nexport const APPROVAL_REASON_MAX_LENGTH = 2000\n\n/**\n * The largest number of approvals that a caller can ask for.\n *\n * A request that asks for 400 approvals can never reach them, so it is a\n * request that only fills the table and expires. The ceiling turns that into a\n * 400 at the moment a person types it. It is HERE because the form of the\n * bulk operation must stop at the same number as the route.\n */\nexport const APPROVAL_ACKS_MAX = 10\n\n/**\n * The longest search text that the list accepts.\n *\n * A search runs `ILIKE` over the payload of every visible row, so the cost of\n * one call follows the length of the text. Nothing a person types to find a\n * request is near this.\n */\nexport const APPROVAL_SEARCH_MAX_LENGTH = 200\n\n/**\n * The payload field that names the organization a request acts on.\n *\n * It is a CONVENTION, and this constant is what makes the convention readable\n * from both sides. A use case whose action hits one organization puts the id of\n * that organization in this field, and `GET /api/approvals?orgId=\u2026` then finds\n * the request with no knowledge of the use case. The filter reads this one\n * field of the payload, so it matches the whole id and nothing else. The free\n * text search reads the payload as TEXT, which also matches an id that only\n * appears inside another field, and it cannot answer \"the requests of this\n * organization\".\n *\n * The organization page of control-frontend does NOT read this field. It asks\n * the entity-link filter (`entityType=org`), which reads a row that\n * `relatedEntities` of a use case writes \u2014 see `ApprovalEntityRef`. The summary\n * is separate again: the `org` field of a `summarize` result is what a READER\n * follows.\n */\nexport const APPROVAL_PAYLOAD_ORG_FIELD = 'orgId'\n\n/**\n * How many requests each status holds, for the filters of the queue.\n *\n * Every number answers the SAME question as the list beside it: the counts are\n * built with the visibility rule, the action filter and the search text of the\n * caller, and with the lazy expiry. A count that ignored one of those would\n * send a person to a filter that then shows nothing.\n */\nexport interface ApprovalStatusCountsDto {\n /** Every request that matches, whatever its status. */\n all: number\n /** One number for each status. A status with no request is 0, never absent. */\n byStatus: Record<ApprovalJobStatus, number>\n}\n\n/**\n * What happens when a request reaches its threshold.\n *\n * `auto` runs the action immediately. `explicit` holds the request at\n * `approved` and waits for a person to run it.\n */\nexport type ApprovalExecutionMode = 'auto' | 'explicit'\n\n/**\n * WHERE the action runs. It is a separate question from who starts it.\n *\n * - `inline` runs the action inside the call that started it, and the answer\n * already carries the outcome. Right for one call to another service.\n * - `background` answers as soon as the request is claimed, and the action\n * runs on. It reports into `ApprovalJobDto.progress`, and the row is the\n * only place that holds the progress, so a reload and a second person read\n * the same numbers. A pod that stops loses the run, and the reconciler of\n * control-backend recovers the row.\n *\n * An action over thousands of targets MUST be `background`. `inline` would\n * hold one HTTP request for the whole run, and the ingress, the load balancer\n * and the browser each have their own deadline before it finishes.\n *\n * An `inline` action must not read `context.signal`. The runner trusts the\n * result of an `inline` action that returns after a drain, and it would count\n * a partial run as `executed`. An action that stops between targets is\n * `background`.\n */\nexport type ApprovalActionMode = 'inline' | 'background'\n\n/**\n * A person, as an approval request records them.\n *\n * Both keys are kept. The machine then never has to choose one identity key,\n * which is what split the earlier acknowledgement flows: some held an email and\n * some held a user id, and the two could not be compared.\n */\nexport interface ApprovalActor {\n userId: string\n email: string\n}\n\nexport interface ApprovalAck extends ApprovalActor {\n /** An ISO timestamp. It is in a json column, so it is a string, not a Date. */\n date: string\n}\n\n/**\n * Who asked for an action, and who agreed to it.\n *\n * Every route of `api/admin/approvals` takes this envelope, and control-backend\n * builds it from the request that it runs. The envelope exists because the\n * token cannot carry it: the call is machine to machine, so `sub` holds the id\n * of the Auth0 application and not a person. A service that reads the token\n * alone therefore writes \"control-backend\" into its audit record, which hides\n * the two Camundis that the whole mechanism exists to record.\n *\n * `approvedBy` is a list, because a use case can need more than one approval,\n * and the record must name all of them. It can also be EMPTY: how many\n * approvals an action needs is a number of the use case, and `move-cluster-org`\n * holds zero on purpose. The person in `requestedBy` is never in the list: the\n * four-eyes rule is that the person who asks is not the person who agrees, and\n * control-backend holds that rule.\n *\n * Each person carries both keys of `ApprovalActor`. The email is what a route\n * uses to ask Auth0 about the person, and the user id is what an audit record\n * holds, because every other record of the fleet holds an Auth0 `sub`.\n *\n * CAUTION: The envelope is a CLAIM of the caller, and not proof. A route that\n * acts on it must ask Auth0 again whether `requestedBy` may do the action.\n * That means the permission of the action. For a use case with\n * `openToEveryone`, it means that the person is a Camundi, because no\n * permission runs that action. `api/admin/approvals` in accounts-backend does\n * that. See the note on `M2M_CONTROL_APPROVALS`.\n */\nexport interface ApprovalM2mActor {\n /** The id of the approval request, to join the records of both services. */\n requestId: string\n /** The Camundi who opened the request. */\n requestedBy: ApprovalActor\n /** The Camundis who approved it. Empty when the use case needs none. */\n approvedBy: ApprovalActor[]\n}\n\n/**\n * What one value of a summary field is.\n *\n * `text` is what a person reads. `id` is what a link points at, and it is\n * absent when the kind names no entity. The two are separate because the text\n * is a COPY that the request keeps: an organization that gets a new name after\n * the request was opened must still read as the organization that the person\n * chose.\n */\nexport interface ApprovalSummaryValue {\n text: string\n id?: string\n}\n\n/**\n * How a reader must render one field of a summary.\n *\n * `org`, `cluster` and `user` name an entity, so a reader can link to it and\n * can resolve a fresh name. The others are plain: `date` is an ISO timestamp,\n * and `code` is a value that must keep its characters, such as a json fragment.\n */\nexport const APPROVAL_SUMMARY_FIELD_KINDS = [\n 'text',\n 'number',\n 'boolean',\n 'date',\n 'org',\n 'cluster',\n 'user',\n 'code',\n] as const\n\nexport type ApprovalSummaryFieldKind =\n (typeof APPROVAL_SUMMARY_FIELD_KINDS)[number]\n\n/**\n * One labelled field of the payload of a request.\n *\n * This is what makes ONE view able to show EVERY use case. The payload of a\n * request is an object of anything, so a generic view can only print it as\n * json. The use case turns it into labelled fields here, and the view renders\n * a field by its kind: it links an `org`, it formats a `date`, and it lists\n * 300 clusters as 300 links.\n *\n * `values` is always an array. A field that holds one value holds an array of\n * one, so a reader needs no branch for \"one or many\".\n */\nexport interface ApprovalSummaryField {\n label: string\n kind: ApprovalSummaryFieldKind\n values: ApprovalSummaryValue[]\n /**\n * The values that this request replaces, for a field that changes. A view\n * shows them as removed beside `values` as added. A field with no\n * `previous` is not a change.\n */\n previous?: ApprovalSummaryValue[]\n /** The count of `previous`, when the list cut it. Like `total`, for `previous`. */\n previousTotal?: number\n /**\n * How many values the field holds, when `values` holds only the first of\n * them. The list route cuts every field to `APPROVAL_LIST_SUMMARY_VALUES_MAX`\n * values. The detail route sends every value and no `total`.\n */\n total?: number\n}\n\n/**\n * How many values of one summary field a row of the list carries.\n *\n * A bulk operation names up to 3000 clusters in one field. A page of 100 such\n * rows would carry 300,000 values that no row shows. The view shows at most\n * 12 values of a field, so the list stops there, and `total` holds the count.\n */\nexport const APPROVAL_LIST_SUMMARY_VALUES_MAX = 12\n\n/**\n * The kinds of entity that an approval request can concern.\n *\n * These are the entities that a request is findable by: the org and cluster\n * detail pages ask \"which requests touch me\", and the answer reads a row for\n * each entity. It is a subset of `APPROVAL_SUMMARY_FIELD_KINDS` \u2014 the kinds that\n * name an entity \u2014 because only those carry an id that a page can match.\n */\nexport const APPROVAL_ENTITY_TYPES = ['cluster', 'org', 'user'] as const\n\nexport type ApprovalEntityType = (typeof APPROVAL_ENTITY_TYPES)[number]\n\n/**\n * One entity that an approval request concerns.\n *\n * A use case reports these from its payload, and the service writes one row for\n * each into `approval_job_entity`. That row is what makes a request findable by\n * an entity it touches, without a text search over the payload: a cluster-move\n * request names its cluster, its target organization AND its source\n * organization, so it shows on all three detail pages. The `summary` still\n * carries the words a reader sees; this carries only the ids a page matches.\n */\nexport interface ApprovalEntityRef {\n type: ApprovalEntityType\n id: string\n}\n\n/**\n * How long a run may go quiet before a reader calls it stalled.\n *\n * The pod that runs an action writes its progress and renews its lease on one\n * timer, so a healthy run touches the row well inside this window. A row that\n * is `executing` and older than this held a pod that stopped, and the\n * reconciler of control-backend is on its way to it.\n *\n * It lives here, and not in the config of the service, because a READER needs\n * it: the browser must decide what to show without a second call, and a number\n * that only the backend knew would make the two disagree. `ACTION_LEASE_MS` in\n * `config/index.ts` is held below this on purpose.\n */\nexport const APPROVAL_RUN_STALE_MS = 120_000\n\n/**\n * What an action reports while it runs.\n *\n * This is what makes a long action watchable. Every field is in the DATABASE\n * and not in the answer to the call that started the run, so three readers see\n * the same thing: the person who pressed the button, the same person after a\n * reload, and a second person who opens the request.\n *\n * `total` can be 0 while an action counts its targets, and it stays 0 for an\n * action that cannot count them. A reader must then show that it runs, and no\n * percentage.\n */\nexport interface ApprovalProgress {\n /** Targets in total. 0 means the action did not say. */\n total: number\n /** Targets finished, whatever the outcome of each one. */\n done: number\n /** Targets that finished with an error. Part of `done`. */\n failed: number\n /** One short line for a person, such as the step that runs now. */\n note?: string\n /** An ISO timestamp: when the run started. */\n startedAt: string\n /** An ISO timestamp: when the run last wrote anything. */\n updatedAt: string\n}\n\n/** The share of the work that is finished, or `undefined` with no total. */\nexport function progressFraction(\n progress: ApprovalProgress | undefined,\n): number | undefined {\n if (!progress || progress.total <= 0) {\n return undefined\n }\n return Math.min(1, progress.done / progress.total)\n}\n\n/**\n * Reports whether a running request went quiet.\n *\n * True means that the pod which held the run stopped, or lost the row. It does\n * NOT mean that the action failed: the reconciler either runs it again, or it\n * writes `failed` so that a person can. See `APPROVAL_RUN_STALE_MS`.\n */\nexport function isRunStalled(job: ApprovalJobDto, now: Date): boolean {\n if (job.status !== 'executing') {\n return false\n }\n const lastSign = job.progress?.updatedAt ?? job.executingSince\n if (!lastSign) {\n return false\n }\n return now.getTime() - new Date(lastSign).getTime() > APPROVAL_RUN_STALE_MS\n}\n\n/** One approval request, as the API returns it. */\nexport interface ApprovalJobDto {\n id: string\n type: string\n dedupeKey?: string\n payload: Record<string, unknown>\n /**\n * The payload as labelled fields. See `ApprovalSummaryField`.\n *\n * The use case builds it when the request is opened AND every time the\n * request is read, so a field that a later version of the use case adds\n * appears on a request that is already open.\n */\n summary: ApprovalSummaryField[]\n /** Why the person opened the request. Some use cases demand one. */\n reason?: string\n createdBy: ApprovalActor\n acks: ApprovalAck[]\n /**\n * How many approvals THIS request needs.\n *\n * It is on the request and not only on the use case, because the person who\n * opens one can ask for more than the floor of the use case. A bulk\n * operation over 3000 clusters is the same action as one over 3, and it can\n * still deserve a third pair of eyes. `ApprovalUseCaseDto.minAcks` is the\n * floor that a caller cannot go below.\n */\n requiredAcks: number\n /**\n * What a person must hold to approve, reject or run THIS request.\n *\n * It is on the request and not only on the use case, because a payload can\n * raise the guard: a generation update with `yoloForceUpdate` needs the yolo\n * permission on top of the base one, and the same use case without the flag\n * does not. The set is computed ONE time, when the request is opened, so it\n * is also the record of what the request was guarded by.\n *\n * A person needs EVERY permission in the set, not one of them.\n */\n requiredPermissions: string[]\n executionMode: ApprovalExecutionMode\n actionMode: ApprovalActionMode\n status: ApprovalJobStatus\n closedBy?: ApprovalActor\n closedReason?: string\n /** An ISO timestamp. */\n expiresAt: string\n /**\n * An ISO timestamp: when the current run took the request.\n *\n * It is set every time a run starts, so a run that the reconciler starts\n * again moves it. `attempts` says how many runs there were.\n */\n executingSince?: string\n /** How many times a run started. A recovery raises it. */\n attempts: number\n /**\n * What the action reports while it runs. See `ApprovalProgress`.\n *\n * It stays on the row after the run, so the record of a finished run says\n * how many targets it touched and how many refused.\n */\n progress?: ApprovalProgress\n executedAt?: string\n executionError?: string\n /**\n * What the action reported when it ran.\n *\n * The shape belongs to the use case. An action over many targets puts the\n * result of each target here, which is the difference between \"it failed\"\n * and \"it did 2998 of 3000, and these two refused\".\n */\n executionResult?: Record<string, unknown>\n createdAt: string\n updatedAt: string\n}\n\n/**\n * What a use case is, as the API reports it.\n *\n * The frontend needs `requiredPermission` to run the same rule as the backend.\n * PR #9301 kept this registry a second time, by hand, in the frontend. A\n * registry that the server owns means a new use case needs no frontend deploy.\n */\nexport interface ApprovalUseCaseDto {\n type: string\n /** A short name for a person to read. */\n label: string\n /** One sentence: what the action does, and what it cannot undo. */\n description: string\n /** The approvals a new request asks for when the caller names no number. */\n defaultAcks: number\n /**\n * The floor. A caller can ask for more approvals, never for fewer.\n *\n * It is a separate number from `defaultAcks` because the two answer\n * different questions: the default is what the form offers, and the floor is\n * what the service refuses to go below.\n */\n minAcks: number\n ttlDays: number\n executionMode: ApprovalExecutionMode\n /** See `ApprovalActionMode`. An action over many targets is `background`. */\n actionMode: ApprovalActionMode\n /**\n * True when a run that was interrupted can start again by itself.\n *\n * It is a promise about the ACTION, and the reconciler reads it. Set it only\n * when the action can run a second time over the same payload and do no harm\n * twice: it must read what the first run already finished, out of\n * `ApprovalJobDto.executionResult`, and skip it.\n *\n * False, the default, is the safe answer. An interrupted run then lands in\n * `failed`, which a person can run again from the detail page, and a person\n * decides whether running it twice is safe.\n */\n resumable: boolean\n /**\n * The permission that every request of this use case needs to approve,\n * reject or run. A payload can add more. See\n * `ApprovalJobDto.requiredPermissions`, which is the effective set.\n *\n * It is also the permission to SEE a request of this use case, because a\n * person who cannot act on one has no reason to read it. The person who\n * opened a request always sees that request.\n *\n * The one exception to \"or run\" is a use case with `openToEveryone`. It has\n * no approval, so this permission only decides who sees the requests of\n * other people. Any signed-in person can open and run such a request.\n */\n requiredPermission: string\n /**\n * The permission to open a request. Absent means any signed-in Camundi.\n *\n * Set it when the pool that can ASK must be as small as the pool that can\n * approve. Leave it absent when the approvals alone are the guard, and the\n * proposal is not.\n */\n createPermission?: string\n /** True when a request cannot be opened without a reason. */\n reasonRequired: boolean\n /**\n * True when the ACTION needs the email address of the person who asked.\n *\n * A token can carry no address, and the row then stores\n * `UNKNOWN_ACTOR_EMAIL`. An action that sends that placeholder to another\n * service asks it to find a user that does not exist, so the request can\n * only fail \u2014 and it fails at the END, after other people spent their\n * approvals on it. A use case that carries the creator into its call sets\n * this, and `canCreate` then refuses such a caller at the start.\n *\n * `extend-trial` and `send-to-expired-trial` are two: both send the creator\n * to accounts in an `ApprovalM2mActor`, and accounts reads the permissions\n * of that person again before it acts.\n *\n * `move-cluster-org` does NOT set this. Its envelope reaches console, which\n * reads the user id alone and needs no address.\n */\n creatorEmailRequired?: boolean\n /**\n * A use case with no approval that any signed-in person can open AND run.\n *\n * A request with a floor of zero runs when it is opened, so `canCreate`\n * asks the person to hold `requiredPermission`. This flag lifts that one\n * rule, for an action whose run needs no permission of the person. Set it\n * only when two things hold:\n *\n * - Something else bounds how often a person can ask.\n * - The service that runs the action authorizes the person again at its\n * source, so that nobody can change what is not theirs. The action can\n * touch an organization only when that check lets the person change it.\n *\n * `create-personal-organisation` and the two use cases of customer hardware\n * sizing set it. The two use cases of customer hardware sizing do the second\n * check in two ways:\n *\n * - Creating a plan, and unlinking a plan from one organization, read the\n * role of the person in that organization. Only an OWNER passes.\n * - Deleting a plan reads no role. Console checks that the person created\n * the plan. Accounts reads the organizations that the person is a member\n * of, in any role, and removes the plan from their lists.\n */\n openToEveryone?: boolean\n}\n"],
|
|
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 wire types of the generic approval mechanism.\n *\n * This file holds no logic and no dependency, because three consumers read it:\n * control-backend, control-frontend and cloudbot. The rules that read these\n * types are in `../approvals/policy.js`, and they are pure for the same reason.\n */\n\n/**\n * Every state that an approval request can hold.\n *\n * A `const` tuple, not an enum, so it is both the union type and the list that\n * validates a status filter at run time.\n *\n * The path of a request: `pending` until it reaches the threshold. Then\n * `executing` (automatic mode) or `approved` (explicit mode, which waits for a\n * person to press the button). `executed` or `failed` after the action ran. A\n * request that never reaches the threshold ends `rejected`, `withdrawn` or\n * `expired`.\n */\nexport const APPROVAL_JOB_STATUSES = [\n 'pending',\n 'approved',\n 'executing',\n 'executed',\n 'failed',\n 'rejected',\n 'withdrawn',\n 'expired',\n] as const\n\nexport type ApprovalJobStatus = (typeof APPROVAL_JOB_STATUSES)[number]\n\n/**\n * The states in which the action of a request is still COMING.\n *\n * `pending` waits for approvals, `approved` waits for a person to press the\n * button, and `executing` is running now. Each of them can still change the\n * world, so two requests for one target must not be in these states together.\n * `dedupeKey` reads this set for that reason.\n *\n * `failed` is NOT here, although a person can run a failed request again. A\n * failed request runs only when a person presses the button, and that person\n * sees the row. Blocking a new request on a failed one would also trap the\n * key: `canWithdraw` accepts `pending` alone, so nobody could close it.\n *\n * `executed`, `rejected`, `withdrawn` and `expired` are the end of a request.\n */\nexport const APPROVAL_LIVE_STATUSES = [\n 'pending',\n 'approved',\n 'executing',\n] as const satisfies readonly ApprovalJobStatus[]\n\n/**\n * How many requests one page of the list holds when the caller asks for no\n * number.\n *\n * The list is a work queue that a Camundi reads, so the page is large: a\n * person wants the whole queue and not ten rows at a time. What the number is\n * for is the ceiling below.\n */\nexport const APPROVAL_PAGE_SIZE_DEFAULT = 500\n\n/**\n * The largest page that the service builds, and the largest that a caller may\n * ask for. The API answers 400 above it.\n *\n * The list had no limit at all. One authenticated caller could then make the\n * service read the whole table into memory, serialize it, and do it again, so\n * the cost of a request grew with the age of the service and with nothing that\n * the caller had to pay for. A ceiling turns that into a fixed cost.\n *\n * It is HERE and not only in the service, because the view must know it: a\n * page that asked for more would get a 400 instead of rows.\n */\nexport const APPROVAL_PAGE_SIZE_MAX = 2000\n\n/**\n * The columns that the queue can be ordered by.\n *\n * A NAME and not an SQL expression: the repository maps each one, so a caller\n * can never name a column that this list does not hold. `status` orders by the\n * EFFECTIVE status, because a `pending` row whose time ran out reads as\n * `expired` everywhere else.\n */\nexport const APPROVAL_SORT_FIELDS = [\n 'createdAt',\n 'expiresAt',\n 'status',\n 'type',\n 'createdBy',\n] as const\n\nexport type ApprovalSortField = (typeof APPROVAL_SORT_FIELDS)[number]\n\nexport type ApprovalSortOrder = 'asc' | 'desc'\n\n/** The default order of the queue: the newest request first. */\nexport const APPROVAL_SORT_DEFAULT: {\n sort: ApprovalSortField\n order: ApprovalSortOrder\n} = { sort: 'createdAt', order: 'desc' }\n\n/**\n * The longest reason that a request, a rejection or a withdrawal accepts.\n *\n * ONE number, because three routes of control-backend hold it and three fields\n * of control-frontend must stop there. A field that let a person write more\n * than the schema accepts turns their sentence into a 400 after they wrote it.\n */\nexport const APPROVAL_REASON_MAX_LENGTH = 2000\n\n/**\n * The largest number of approvals that a caller can ask for.\n *\n * A request that asks for 400 approvals can never reach them, so it is a\n * request that only fills the table and expires. The ceiling turns that into a\n * 400 at the moment a person types it. It is HERE because the form of the\n * bulk operation must stop at the same number as the route.\n */\nexport const APPROVAL_ACKS_MAX = 10\n\n/**\n * The longest search text that the list accepts.\n *\n * A search runs `ILIKE` over the payload of every visible row, so the cost of\n * one call follows the length of the text. Nothing a person types to find a\n * request is near this.\n */\nexport const APPROVAL_SEARCH_MAX_LENGTH = 200\n\n/**\n * The payload field that names the organization a request acts on.\n *\n * It is a CONVENTION, and this constant is what makes the convention readable\n * from both sides. A use case whose action hits one organization puts the id of\n * that organization in this field, and `GET /api/approvals?orgId=\u2026` then finds\n * the request with no knowledge of the use case. The filter reads this one\n * field of the payload, so it matches the whole id and nothing else. The free\n * text search reads the payload as TEXT, which also matches an id that only\n * appears inside another field, and it cannot answer \"the requests of this\n * organization\".\n *\n * The organization page of control-frontend does NOT read this field. It asks\n * the entity-link filter (`entityType=org`), which reads a row that\n * `relatedEntities` of a use case writes \u2014 see `ApprovalEntityRef`. The summary\n * is separate again: the `org` field of a `summarize` result is what a READER\n * follows.\n */\nexport const APPROVAL_PAYLOAD_ORG_FIELD = 'orgId'\n\n/**\n * How many requests each status holds, for the filters of the queue.\n *\n * Every number answers the SAME question as the list beside it: the counts are\n * built with the visibility rule, the action filter and the search text of the\n * caller, and with the lazy expiry. A count that ignored one of those would\n * send a person to a filter that then shows nothing.\n */\nexport interface ApprovalStatusCountsDto {\n /** Every request that matches, whatever its status. */\n all: number\n /** One number for each status. A status with no request is 0, never absent. */\n byStatus: Record<ApprovalJobStatus, number>\n}\n\n/**\n * What happens when a request reaches its threshold.\n *\n * `auto` runs the action immediately. `explicit` holds the request at\n * `approved` and waits for a person to run it.\n */\nexport type ApprovalExecutionMode = 'auto' | 'explicit'\n\n/**\n * WHERE the action runs. It is a separate question from who starts it.\n *\n * - `inline` runs the action inside the call that started it, and the answer\n * already carries the outcome. Right for one call to another service.\n * - `background` answers as soon as the request is claimed, and the action\n * runs on. It reports into `ApprovalJobDto.progress`, and the row is the\n * only place that holds the progress, so a reload and a second person read\n * the same numbers. A pod that stops loses the run, and the reconciler of\n * control-backend recovers the row.\n *\n * An action over thousands of targets MUST be `background`. `inline` would\n * hold one HTTP request for the whole run, and the ingress, the load balancer\n * and the browser each have their own deadline before it finishes.\n *\n * An `inline` action must not read `context.signal`. The runner trusts the\n * result of an `inline` action that returns after a drain, and it would count\n * a partial run as `executed`. An action that stops between targets is\n * `background`.\n */\nexport type ApprovalActionMode = 'inline' | 'background'\n\n/**\n * A person, as an approval request records them.\n *\n * Both keys are kept. The machine then never has to choose one identity key,\n * which is what split the earlier acknowledgement flows: some held an email and\n * some held a user id, and the two could not be compared.\n */\nexport interface ApprovalActor {\n userId: string\n email: string\n}\n\nexport interface ApprovalAck extends ApprovalActor {\n /** An ISO timestamp. It is in a json column, so it is a string, not a Date. */\n date: string\n}\n\n/**\n * Who asked for an action, and who agreed to it.\n *\n * Every route of `api/admin/approvals` takes this envelope, and control-backend\n * builds it from the request that it runs. The envelope exists because the\n * token cannot carry it: the call is machine to machine, so `sub` holds the id\n * of the Auth0 application and not a person. A service that reads the token\n * alone therefore writes \"control-backend\" into its audit record, which hides\n * the two Camundis that the whole mechanism exists to record.\n *\n * `approvedBy` is a list, because a use case can need more than one approval,\n * and the record must name all of them. It can also be EMPTY: how many\n * approvals an action needs is a number of the use case, and `move-cluster-org`\n * holds zero on purpose. The person in `requestedBy` is never in the list: the\n * four-eyes rule is that the person who asks is not the person who agrees, and\n * control-backend holds that rule.\n *\n * Each person carries both keys of `ApprovalActor`. The email is what a route\n * uses to ask Auth0 about the person, and the user id is what an audit record\n * holds, because every other record of the fleet holds an Auth0 `sub`.\n *\n * CAUTION: The envelope is a CLAIM of the caller, and not proof. A route that\n * acts on it must ask Auth0 again whether `requestedBy` may do the action.\n * That means the permission of the action. For a use case with\n * `openToEveryone`, it means that the person is a Camundi, because no\n * permission runs that action. `api/admin/approvals` in accounts-backend does\n * that. See the note on `M2M_CONTROL_APPROVALS`.\n */\nexport interface ApprovalM2mActor {\n /** The id of the approval request, to join the records of both services. */\n requestId: string\n /** The Camundi who opened the request. */\n requestedBy: ApprovalActor\n /** The Camundis who approved it. Empty when the use case needs none. */\n approvedBy: ApprovalActor[]\n}\n\n/**\n * What one value of a summary field is.\n *\n * `text` is what a person reads. `id` is what a link points at, and it is\n * absent when the kind names no entity. The two are separate because the text\n * is a COPY that the request keeps: an organization that gets a new name after\n * the request was opened must still read as the organization that the person\n * chose.\n */\nexport interface ApprovalSummaryValue {\n text: string\n id?: string\n}\n\n/**\n * How a reader must render one field of a summary.\n *\n * `org`, `cluster` and `user` name an entity, so a reader can link to it and\n * can resolve a fresh name. The others are plain: `date` is an ISO timestamp,\n * and `code` is a value that must keep its characters, such as a json fragment.\n */\nexport const APPROVAL_SUMMARY_FIELD_KINDS = [\n 'text',\n 'number',\n 'boolean',\n 'date',\n 'org',\n 'cluster',\n 'user',\n 'code',\n] as const\n\nexport type ApprovalSummaryFieldKind =\n (typeof APPROVAL_SUMMARY_FIELD_KINDS)[number]\n\n/**\n * One labelled field of the payload of a request.\n *\n * This is what makes ONE view able to show EVERY use case. The payload of a\n * request is an object of anything, so a generic view can only print it as\n * json. The use case turns it into labelled fields here, and the view renders\n * a field by its kind: it links an `org`, it formats a `date`, and it lists\n * 300 clusters as 300 links.\n *\n * `values` is always an array. A field that holds one value holds an array of\n * one, so a reader needs no branch for \"one or many\".\n */\nexport interface ApprovalSummaryField {\n label: string\n kind: ApprovalSummaryFieldKind\n values: ApprovalSummaryValue[]\n /**\n * The values that this request replaces, for a field that changes. A view\n * shows them as removed beside `values` as added. A field with no\n * `previous` is not a change.\n */\n previous?: ApprovalSummaryValue[]\n /** The count of `previous`, when the list cut it. Like `total`, for `previous`. */\n previousTotal?: number\n /**\n * How many values the field holds, when `values` holds only the first of\n * them. The list route cuts every field to `APPROVAL_LIST_SUMMARY_VALUES_MAX`\n * values. The detail route sends every value and no `total`.\n */\n total?: number\n}\n\n/**\n * How many values of one summary field a row of the list carries.\n *\n * A bulk operation names up to 3000 clusters in one field. A page of 100 such\n * rows would carry 300,000 values that no row shows. The view shows at most\n * 12 values of a field, so the list stops there, and `total` holds the count.\n */\nexport const APPROVAL_LIST_SUMMARY_VALUES_MAX = 12\n\n/**\n * The kinds of entity that an approval request can concern.\n *\n * These are the entities that a request is findable by: the org and cluster\n * detail pages ask \"which requests touch me\", and the answer reads a row for\n * each entity. It is a subset of `APPROVAL_SUMMARY_FIELD_KINDS` \u2014 the kinds that\n * name an entity \u2014 because only those carry an id that a page can match.\n */\nexport const APPROVAL_ENTITY_TYPES = ['cluster', 'org', 'user'] as const\n\nexport type ApprovalEntityType = (typeof APPROVAL_ENTITY_TYPES)[number]\n\n/**\n * One entity that an approval request concerns.\n *\n * A use case reports these from its payload, and the service writes one row for\n * each into `approval_job_entity`. That row is what makes a request findable by\n * an entity it touches, without a text search over the payload: a cluster-move\n * request names its cluster, its target organization AND its source\n * organization, so it shows on all three detail pages. The `summary` still\n * carries the words a reader sees; this carries only the ids a page matches.\n */\nexport interface ApprovalEntityRef {\n type: ApprovalEntityType\n id: string\n}\n\n/**\n * How long a run may go quiet before a reader calls it stalled.\n *\n * The pod that runs an action writes its progress and renews its lease on one\n * timer, so a healthy run touches the row well inside this window. A row that\n * is `executing` and older than this held a pod that stopped, and the\n * reconciler of control-backend is on its way to it.\n *\n * It lives here, and not in the config of the service, because a READER needs\n * it: the browser must decide what to show without a second call, and a number\n * that only the backend knew would make the two disagree. `ACTION_LEASE_MS` in\n * `config/index.ts` is held below this on purpose.\n */\nexport const APPROVAL_RUN_STALE_MS = 120_000\n\n/**\n * What an action reports while it runs.\n *\n * This is what makes a long action watchable. Every field is in the DATABASE\n * and not in the answer to the call that started the run, so three readers see\n * the same thing: the person who pressed the button, the same person after a\n * reload, and a second person who opens the request.\n *\n * `total` can be 0 while an action counts its targets, and it stays 0 for an\n * action that cannot count them. A reader must then show that it runs, and no\n * percentage.\n */\nexport interface ApprovalProgress {\n /** Targets in total. 0 means the action did not say. */\n total: number\n /** Targets finished, whatever the outcome of each one. */\n done: number\n /** Targets that finished with an error. Part of `done`. */\n failed: number\n /** One short line for a person, such as the step that runs now. */\n note?: string\n /** An ISO timestamp: when the run started. */\n startedAt: string\n /** An ISO timestamp: when the run last wrote anything. */\n updatedAt: string\n}\n\n/** The share of the work that is finished, or `undefined` with no total. */\nexport function progressFraction(\n progress: ApprovalProgress | undefined,\n): number | undefined {\n if (!progress || progress.total <= 0) {\n return undefined\n }\n return Math.min(1, progress.done / progress.total)\n}\n\n/**\n * Reports whether a running request went quiet.\n *\n * True means that the pod which held the run stopped, or lost the row. It does\n * NOT mean that the action failed: the reconciler either runs it again, or it\n * writes `failed` so that a person can. See `APPROVAL_RUN_STALE_MS`.\n */\nexport function isRunStalled(job: ApprovalJobDto, now: Date): boolean {\n if (job.status !== 'executing') {\n return false\n }\n const lastSign = job.progress?.updatedAt ?? job.executingSince\n if (!lastSign) {\n return false\n }\n return now.getTime() - new Date(lastSign).getTime() > APPROVAL_RUN_STALE_MS\n}\n\n/** One approval request, as the API returns it. */\nexport interface ApprovalJobDto {\n id: string\n type: string\n dedupeKey?: string\n payload: Record<string, unknown>\n /**\n * The payload as labelled fields. See `ApprovalSummaryField`.\n *\n * The use case builds it when the request is opened AND every time the\n * request is read, so a field that a later version of the use case adds\n * appears on a request that is already open.\n */\n summary: ApprovalSummaryField[]\n /** Why the person opened the request. Some use cases demand one. */\n reason?: string\n createdBy: ApprovalActor\n acks: ApprovalAck[]\n /**\n * How many approvals THIS request needs.\n *\n * It is on the request and not only on the use case, because the person who\n * opens one can ask for more than the floor of the use case. A bulk\n * operation over 3000 clusters is the same action as one over 3, and it can\n * still deserve a third pair of eyes. `ApprovalUseCaseDto.minAcks` is the\n * floor that a caller cannot go below.\n */\n requiredAcks: number\n /**\n * What a person must hold to approve, reject or run THIS request.\n *\n * It is on the request and not only on the use case, because a payload can\n * raise the guard: a generation update with `yoloForceUpdate` needs the yolo\n * permission on top of the base one, and the same use case without the flag\n * does not. The set is computed ONE time, when the request is opened, so it\n * is also the record of what the request was guarded by.\n *\n * A person needs EVERY permission in the set, not one of them.\n */\n requiredPermissions: string[]\n executionMode: ApprovalExecutionMode\n actionMode: ApprovalActionMode\n status: ApprovalJobStatus\n closedBy?: ApprovalActor\n closedReason?: string\n /** An ISO timestamp. */\n expiresAt: string\n /**\n * An ISO timestamp: when the current run took the request.\n *\n * It is set every time a run starts, so a run that the reconciler starts\n * again moves it. `attempts` says how many runs there were.\n */\n executingSince?: string\n /** How many times a run started. A recovery raises it. */\n attempts: number\n /**\n * What the action reports while it runs. See `ApprovalProgress`.\n *\n * It stays on the row after the run, so the record of a finished run says\n * how many targets it touched and how many refused.\n */\n progress?: ApprovalProgress\n executedAt?: string\n executionError?: string\n /**\n * What the action reported when it ran.\n *\n * The shape belongs to the use case. An action over many targets puts the\n * result of each target here, which is the difference between \"it failed\"\n * and \"it did 2998 of 3000, and these two refused\".\n */\n executionResult?: Record<string, unknown>\n createdAt: string\n updatedAt: string\n}\n\n/**\n * What a use case is, as the API reports it.\n *\n * The frontend needs `requiredPermission` to run the same rule as the backend.\n * PR #9301 kept this registry a second time, by hand, in the frontend. A\n * registry that the server owns means a new use case needs no frontend deploy.\n */\nexport interface ApprovalUseCaseDto {\n type: string\n /** A short name for a person to read. */\n label: string\n /** One sentence: what the action does, and what it cannot undo. */\n description: string\n /** The approvals a new request asks for when the caller names no number. */\n defaultAcks: number\n /**\n * The floor. A caller can ask for more approvals, never for fewer.\n *\n * It is a separate number from `defaultAcks` because the two answer\n * different questions: the default is what the form offers, and the floor is\n * what the service refuses to go below.\n */\n minAcks: number\n ttlDays: number\n executionMode: ApprovalExecutionMode\n /** See `ApprovalActionMode`. An action over many targets is `background`. */\n actionMode: ApprovalActionMode\n /**\n * True when a run that was interrupted can start again by itself.\n *\n * It is a promise about the ACTION, and the reconciler reads it. Set it only\n * when the action can run a second time over the same payload and do no harm\n * twice: it must read what the first run already finished, out of\n * `ApprovalJobDto.executionResult`, and skip it.\n *\n * False, the default, is the safe answer. An interrupted run then lands in\n * `failed`, which a person can run again from the detail page, and a person\n * decides whether running it twice is safe.\n */\n resumable: boolean\n /**\n * The permission that every request of this use case needs to approve,\n * reject or run. A payload can add more. See\n * `ApprovalJobDto.requiredPermissions`, which is the effective set.\n *\n * It is also the permission to SEE a request of this use case, because a\n * person who cannot act on one has no reason to read it. The person who\n * opened a request always sees that request.\n *\n * The one exception to \"or run\" is a use case with `openToEveryone`. It has\n * no approval, so this permission only decides who sees the requests of\n * other people. Any signed-in person can open and run such a request.\n */\n requiredPermission: string\n /**\n * The permission to open a request. Absent means any signed-in Camundi.\n *\n * Set it when the pool that can ASK must be as small as the pool that can\n * approve. Leave it absent when the approvals alone are the guard, and the\n * proposal is not.\n */\n createPermission?: string\n /** True when a request cannot be opened without a reason. */\n reasonRequired: boolean\n /**\n * True when the ACTION needs the email address of the person who asked.\n *\n * A token can carry no address, and the row then stores\n * `UNKNOWN_ACTOR_EMAIL`. An action that sends that placeholder to another\n * service asks it to find a user that does not exist, so the request can\n * only fail \u2014 and it fails at the END, after other people spent their\n * approvals on it. A use case that carries the creator into its call sets\n * this, and `canCreate` then refuses such a caller at the start.\n *\n * `extend-trial` and `send-to-expired-trial` are two: both send the creator\n * to accounts in an `ApprovalM2mActor`, and accounts reads the permissions\n * of that person again before it acts.\n *\n * `move-cluster-org` does NOT set this. Its envelope reaches console, which\n * reads the user id alone and needs no address.\n */\n creatorEmailRequired?: boolean\n /**\n * A use case with no approval that any signed-in person can open AND run.\n *\n * A request with a floor of zero runs when it is opened, so `canCreate`\n * asks the person to hold `requiredPermission`. This flag lifts that one\n * rule, for an action whose run needs no permission of the person. Set it\n * only when two things hold:\n *\n * - Something else bounds how often a person can ask.\n * - The service that runs the action authorizes the person again at its\n * source, so that nobody can change what is not theirs. The action can\n * touch an organization only when that check lets the person change it.\n *\n * `create-personal-organisation` and the two use cases of custom hardware\n * sizing set it. The two use cases of custom hardware sizing do the second\n * check in two ways:\n *\n * - Creating a plan, and unlinking a plan from one organization, read the\n * role of the person in that organization. Only an OWNER passes.\n * - Deleting a plan reads no role. Console checks that the person created\n * the plan. Accounts reads the organizations that the person is a member\n * of, in any role, and removes the plan from their lists.\n */\n openToEveryone?: boolean\n}\n"],
|
|
5
5
|
"mappings": "AA0BO,MAAM,wBAAwB;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAmBO,MAAM,yBAAyB;AAAA,EACpC;AAAA,EACA;AAAA,EACA;AACF;AAUO,MAAM,6BAA6B;AAcnC,MAAM,yBAAyB;AAU/B,MAAM,uBAAuB;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAOO,MAAM,wBAGT,EAAE,MAAM,aAAa,OAAO,OAAO;AAShC,MAAM,6BAA6B;AAUnC,MAAM,oBAAoB;AAS1B,MAAM,6BAA6B;AAoBnC,MAAM,6BAA6B;AA0HnC,MAAM,+BAA+B;AAAA,EAC1C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AA4CO,MAAM,mCAAmC;AAUzC,MAAM,wBAAwB,CAAC,WAAW,OAAO,MAAM;AAgCvD,MAAM,wBAAwB;AA8B9B,SAAS,iBACd,UACoB;AACpB,MAAI,CAAC,YAAY,SAAS,SAAS,GAAG;AACpC,WAAO;AAAA,EACT;AACA,SAAO,KAAK,IAAI,GAAG,SAAS,OAAO,SAAS,KAAK;AACnD;AASO,SAAS,aAAa,KAAqB,KAAoB;AACpE,MAAI,IAAI,WAAW,aAAa;AAC9B,WAAO;AAAA,EACT;AACA,QAAM,WAAW,IAAI,UAAU,aAAa,IAAI;AAChD,MAAI,CAAC,UAAU;AACb,WAAO;AAAA,EACT;AACA,SAAO,IAAI,QAAQ,IAAI,IAAI,KAAK,QAAQ,EAAE,QAAQ,IAAI;AACxD;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ClusterTtlDto } from '../types/ClusterTtl.type.js';
|
|
1
2
|
import type { ClusterPlanTypeDto } from './ClusterPlanType.dto.js';
|
|
2
3
|
/**
|
|
3
4
|
* The Connectors secrets filter mode
|
|
@@ -180,6 +181,17 @@ export interface CommonClusterDto {
|
|
|
180
181
|
multiTenancyEnabled?: boolean;
|
|
181
182
|
sessionMaxInactiveIntervalMinutes?: number;
|
|
182
183
|
sessionHeartbeatEnabled?: boolean;
|
|
184
|
+
/**
|
|
185
|
+
* The cluster TTL. Present only on dev and int (never on prod), and only
|
|
186
|
+
* when the reader is one of these:
|
|
187
|
+
*
|
|
188
|
+
* - A member of the org for whom the flag `console.clusterTtl` is on.
|
|
189
|
+
* - A Camundi with `canReadAllClusterInfo`, also while the flag is off.
|
|
190
|
+
*
|
|
191
|
+
* Thus an absent `ttl` means "not shown", not "unlimited". See
|
|
192
|
+
* `ClusterTtlDto`.
|
|
193
|
+
*/
|
|
194
|
+
ttl?: ClusterTtlDto;
|
|
183
195
|
externalIdpEnabled?: boolean;
|
|
184
196
|
externalIdpConfig?: ExternalIdpConfig;
|
|
185
197
|
secretSourceProvisioned?: boolean;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CamundaClusterStage } from '../types/CamundaClusterLabels.type.js';
|
|
2
|
+
import type { ClusterTtlDto } from '../types/ClusterTtl.type.js';
|
|
2
3
|
import type { SalesPlanType } from '../types/SalesPlanType.type.js';
|
|
3
4
|
import type { LicensingModelType } from './Organization.dto.js';
|
|
4
5
|
/** Generic cursor-paginated response. `nextCursor === null` on the last page. */
|
|
@@ -192,6 +193,13 @@ export interface ClusterListItemV2Dto {
|
|
|
192
193
|
externalMonitoringEnabled?: boolean;
|
|
193
194
|
/** The cluster has a `secure_connectivity` row (AWS PrivateLink). */
|
|
194
195
|
privateConnectivityEnabled?: boolean;
|
|
196
|
+
/**
|
|
197
|
+
* The cluster TTL. Absent where `CommonClusterDto.ttl` is absent for the
|
|
198
|
+
* same reader: on every stage other than dev and int, prod included. Also
|
|
199
|
+
* absent for a member of the org on the org clusters route, and on an older
|
|
200
|
+
* console-backend.
|
|
201
|
+
*/
|
|
202
|
+
ttl?: ClusterTtlDto;
|
|
195
203
|
}
|
|
196
204
|
/** Detail extends the list shape; richer fields are added in Phase 3. */
|
|
197
205
|
export type ClusterDetailV2Dto = ClusterListItemV2Dto;
|
|
@@ -361,8 +369,20 @@ export interface ListClustersV2Params extends CursorParams {
|
|
|
361
369
|
* `filter.status` resolves K8s state to a cluster id set.
|
|
362
370
|
*/
|
|
363
371
|
activity?: ClusterActivityBucket[];
|
|
372
|
+
/**
|
|
373
|
+
* `expiring` keeps the clusters whose TTL has an expiry, `unlimited` the
|
|
374
|
+
* clusters whose TTL is unlimited.
|
|
375
|
+
*/
|
|
376
|
+
ttl?: ClusterTtlFilter;
|
|
377
|
+
/**
|
|
378
|
+
* ISO-8601 upper bound on the TTL expiry. Exclusive, as `createdBefore`
|
|
379
|
+
* is. A cluster with an unlimited TTL never matches.
|
|
380
|
+
*/
|
|
381
|
+
ttlExpiresBefore?: string;
|
|
364
382
|
};
|
|
365
383
|
}
|
|
384
|
+
/** ClusterTtlFilter is the value of `ListClustersV2Params.filter.ttl`. */
|
|
385
|
+
export type ClusterTtlFilter = 'expiring' | 'unlimited';
|
|
366
386
|
export interface ClusterParameterOptionV2Dto {
|
|
367
387
|
id: string;
|
|
368
388
|
name: string;
|
|
@@ -32,6 +32,7 @@ export declare enum AuditEntityAttribute {
|
|
|
32
32
|
CLUSTER_BUSINESS_ID_UNIQUENESS = "Business ID Uniqueness",
|
|
33
33
|
CLUSTER_OPTIONAL_TELEMETRY = "Optional telemetry",
|
|
34
34
|
CLUSTER_NON_SPOT_NODES = "Non-spot nodes",
|
|
35
|
+
CLUSTER_TTL = "Cluster TTL",
|
|
35
36
|
CLUSTER_MULTI_TENANCY = "Multi-tenancy",
|
|
36
37
|
CLUSTER_SESSION_MAX_INACTIVE_INTERVAL = "Session idle timeout",
|
|
37
38
|
CLUSTER_SESSION_HEARTBEAT = "Session heartbeat",
|