@serve.zone/interfaces 13.0.1 → 15.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.md +17 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/data/deploymentoperation.d.ts +161 -0
- package/dist_ts/data/deploymentoperation.js +213 -0
- package/dist_ts/data/deploymentpreflight.d.ts +18 -16
- package/dist_ts/data/deploymentpreflight.js +75 -42
- package/dist_ts/data/immutableimage.d.ts +35 -1
- package/dist_ts/data/immutableimage.js +88 -1
- package/dist_ts/data/index.d.ts +1 -0
- package/dist_ts/data/index.js +2 -1
- package/dist_ts/data/service.d.ts +3 -1
- package/dist_ts/data/user.d.ts +14 -2
- package/dist_ts/data/user.js +54 -2
- package/dist_ts/requests/admin.d.ts +4 -6
- package/dist_ts/requests/service.d.ts +80 -5
- package/package.json +1 -1
- package/readme.md +14 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/data/deploymentoperation.ts +441 -0
- package/ts/data/deploymentpreflight.ts +128 -73
- package/ts/data/immutableimage.ts +120 -1
- package/ts/data/index.ts +1 -0
- package/ts/data/service.ts +3 -1
- package/ts/data/user.ts +85 -2
- package/ts/requests/admin.ts +4 -6
- package/ts/requests/service.ts +109 -5
|
@@ -2,13 +2,15 @@ import * as plugins from '../plugins.js';
|
|
|
2
2
|
import type { IService } from '../data/service.js';
|
|
3
3
|
import type { TServiceWritableData } from '../data/service.js';
|
|
4
4
|
import type { IRegistryTarget } from '../data/registry.js';
|
|
5
|
-
import type { IIdentity } from '../data/user.js';
|
|
5
|
+
import type { IIdentity, IIdentityCredential } from '../data/user.js';
|
|
6
6
|
import type { IImageRelease } from '../data/immutableimage.js';
|
|
7
7
|
import type { IDeploymentPreflightReport, IDeploymentPreflightRequestData } from '../data/deploymentpreflight.js';
|
|
8
|
+
import type { IDeploymentReservationInput, IServiceDeploymentOperation, IServiceDeploymentStatus } from '../data/deploymentoperation.js';
|
|
9
|
+
import type { TSha256Digest } from '../data/immutableimage.js';
|
|
8
10
|
export interface IRequest_Any_Cloudly_GetDeploymentPreflight extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IRequest_Any_Cloudly_GetDeploymentPreflight> {
|
|
9
11
|
method: 'getDeploymentPreflight';
|
|
10
12
|
request: IDeploymentPreflightRequestData & {
|
|
11
|
-
identity:
|
|
13
|
+
identity: IIdentityCredential;
|
|
12
14
|
};
|
|
13
15
|
response: {
|
|
14
16
|
report: IDeploymentPreflightReport;
|
|
@@ -47,8 +49,9 @@ export interface IRequest_Any_Cloudly_GetServiceRegistryTarget extends plugins.t
|
|
|
47
49
|
export interface IRequest_Any_Cloudly_GetServiceImageReleases extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IRequest_Any_Cloudly_GetServiceImageReleases> {
|
|
48
50
|
method: 'getServiceImageReleases';
|
|
49
51
|
request: {
|
|
50
|
-
identity:
|
|
52
|
+
identity: IIdentityCredential;
|
|
51
53
|
serviceId: string;
|
|
54
|
+
operationId?: string;
|
|
52
55
|
};
|
|
53
56
|
response: {
|
|
54
57
|
releases: IImageRelease[];
|
|
@@ -57,9 +60,14 @@ export interface IRequest_Any_Cloudly_GetServiceImageReleases extends plugins.ty
|
|
|
57
60
|
export interface IRequest_Any_Cloudly_PromoteServiceImageRelease extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IRequest_Any_Cloudly_PromoteServiceImageRelease> {
|
|
58
61
|
method: 'promoteServiceImageRelease';
|
|
59
62
|
request: {
|
|
60
|
-
identity:
|
|
63
|
+
identity: IIdentityCredential;
|
|
64
|
+
operationId: string;
|
|
61
65
|
serviceId: string;
|
|
62
66
|
releaseId: string;
|
|
67
|
+
expectedRepository: string;
|
|
68
|
+
expectedTag: string;
|
|
69
|
+
expectedDigest: TSha256Digest;
|
|
70
|
+
expectedOperationRevision: number;
|
|
63
71
|
idempotencyKey: string;
|
|
64
72
|
expectedRolloutGeneration: number;
|
|
65
73
|
expectedCurrentRolloutId?: string;
|
|
@@ -68,20 +76,87 @@ export interface IRequest_Any_Cloudly_PromoteServiceImageRelease extends plugins
|
|
|
68
76
|
};
|
|
69
77
|
response: {
|
|
70
78
|
service: IService;
|
|
79
|
+
operation: IServiceDeploymentOperation;
|
|
71
80
|
};
|
|
72
81
|
}
|
|
73
82
|
export interface IRequest_Any_Cloudly_RollbackServiceImageRelease extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IRequest_Any_Cloudly_RollbackServiceImageRelease> {
|
|
74
83
|
method: 'rollbackServiceImageRelease';
|
|
75
84
|
request: {
|
|
76
|
-
identity:
|
|
85
|
+
identity: IIdentityCredential;
|
|
86
|
+
operationId: string;
|
|
77
87
|
serviceId: string;
|
|
78
88
|
releaseId: string;
|
|
89
|
+
expectedRepository: string;
|
|
90
|
+
expectedTag: string;
|
|
91
|
+
expectedDigest: TSha256Digest;
|
|
92
|
+
expectedOperationRevision: number;
|
|
79
93
|
idempotencyKey: string;
|
|
80
94
|
expectedCurrentRolloutId: string;
|
|
81
95
|
expectedRolloutGeneration: number;
|
|
82
96
|
};
|
|
83
97
|
response: {
|
|
84
98
|
service: IService;
|
|
99
|
+
operation: IServiceDeploymentOperation;
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
export interface IRequest_Any_Cloudly_ReserveServiceDeployment extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IRequest_Any_Cloudly_ReserveServiceDeployment> {
|
|
103
|
+
method: 'reserveServiceDeployment';
|
|
104
|
+
request: {
|
|
105
|
+
identity: IIdentityCredential;
|
|
106
|
+
} & IDeploymentReservationInput;
|
|
107
|
+
response: {
|
|
108
|
+
operation: IServiceDeploymentOperation;
|
|
109
|
+
registryTarget: IRegistryTarget;
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
export interface IRequest_Any_Cloudly_GetServiceDeploymentStatus extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IRequest_Any_Cloudly_GetServiceDeploymentStatus> {
|
|
113
|
+
method: 'getServiceDeploymentStatus';
|
|
114
|
+
request: {
|
|
115
|
+
identity: IIdentityCredential;
|
|
116
|
+
serviceId: string;
|
|
117
|
+
operationId: string;
|
|
118
|
+
};
|
|
119
|
+
response: {
|
|
120
|
+
status: IServiceDeploymentStatus;
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
export interface IRequest_Any_Cloudly_PromoteServiceDeploymentRoute extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IRequest_Any_Cloudly_PromoteServiceDeploymentRoute> {
|
|
124
|
+
method: 'promoteServiceDeploymentRoute';
|
|
125
|
+
request: {
|
|
126
|
+
identity: IIdentityCredential;
|
|
127
|
+
serviceId: string;
|
|
128
|
+
operationId: string;
|
|
129
|
+
expectedOperationRevision: number;
|
|
130
|
+
expectedRolloutId: string;
|
|
131
|
+
expectedRolloutGeneration: number;
|
|
132
|
+
expectedDigest: TSha256Digest;
|
|
133
|
+
};
|
|
134
|
+
response: {
|
|
135
|
+
status: IServiceDeploymentStatus;
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
export interface IRequest_Any_Cloudly_RetryServiceDeployment extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IRequest_Any_Cloudly_RetryServiceDeployment> {
|
|
139
|
+
method: 'retryServiceDeployment';
|
|
140
|
+
request: {
|
|
141
|
+
identity: IIdentityCredential;
|
|
142
|
+
serviceId: string;
|
|
143
|
+
operationId: string;
|
|
144
|
+
expectedOperationRevision: number;
|
|
145
|
+
};
|
|
146
|
+
response: {
|
|
147
|
+
operation: IServiceDeploymentOperation;
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
export interface IRequest_Any_Cloudly_CleanupServiceDeployment extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IRequest_Any_Cloudly_CleanupServiceDeployment> {
|
|
151
|
+
method: 'cleanupServiceDeployment';
|
|
152
|
+
request: {
|
|
153
|
+
identity: IIdentityCredential;
|
|
154
|
+
serviceId: string;
|
|
155
|
+
operationId: string;
|
|
156
|
+
expectedOperationRevision: number;
|
|
157
|
+
};
|
|
158
|
+
response: {
|
|
159
|
+
operation: IServiceDeploymentOperation;
|
|
85
160
|
};
|
|
86
161
|
}
|
|
87
162
|
export interface IRequest_Any_Cloudly_CreateService extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IRequest_Any_Cloudly_CreateService> {
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -140,6 +140,20 @@ interface IExampleRequest {
|
|
|
140
140
|
|
|
141
141
|
`requests.config.IRequest_Any_Cloudly_GetClusterConfig` accepts `data.IIdentityCredential`, which contains only the JWT needed for server-side identity resolution. Username/password login and machine-token exchange responses continue to return the full `data.IIdentity`.
|
|
142
142
|
|
|
143
|
+
### Immutable Deployment Contracts
|
|
144
|
+
|
|
145
|
+
Cloudly deployment authority is expressed as an exact `data.IServiceDeploymentGrant`. A `service` grant applies to one owned existing service. An `organization-service-slot` grant reserves authority for one exact future service ID in an organization; it is not an organization-wide wildcard. `configureServiceDeploymentMachineUser` accepts this discriminated `grant` object instead of separate service and capability fields.
|
|
146
|
+
|
|
147
|
+
The immutable deployment workflow is:
|
|
148
|
+
|
|
149
|
+
1. Reserve the exact service, namespace, registry repository/tag, and route intent with `reserveServiceDeployment`.
|
|
150
|
+
2. Push the OCI index to the returned exact tag with the authenticated deployer identity.
|
|
151
|
+
3. Promote the authenticated release evidence with `promoteServiceImageRelease`.
|
|
152
|
+
4. Observe exact rollout and runtime-digest evidence with `getServiceDeploymentStatus`.
|
|
153
|
+
5. For a greenfield service, expose and verify its public route with `promoteServiceDeploymentRoute` only after the immutable rollout succeeds.
|
|
154
|
+
|
|
155
|
+
`data.IServiceDeploymentOperation` is the durable revisioned compare-and-set fence for this workflow. Image promotion requires Cloudly-created trusted evidence that binds the operation, actor, repository, exact tag, root digest, and OCI index media type. The service request group also exposes deployment preflight, exact-digest rollback, retry, and cleanup contracts.
|
|
156
|
+
|
|
143
157
|
Gateway request contracts include `getGatewayClientRoutes` (`requests.gateway.IReq_GetGatewayClientRoutes`) for listing owned `IGatewayClientRoute[]` route views, and `syncGatewayClientRoute` for idempotently syncing or deleting hostname-owned, routeRef-owned, and combined hostname-plus-routeRef routes. A client can label canonical intent with `managedRouteKind: 'letsencrypt-http01-forward'` and set a higher `priority` for a path-specific HTTP-01 route while retaining a separate normal route for the same hostname. Mail request contracts include `syncMailAddressBinding`, `deleteMailAddressBinding`, `rotateMailCredential`, and `getMailDeliveryStatus`. `IReq_GetMailDeliveryStatus` looks up a delivery spool item by `spoolItemId`, returns `data.IMailDeliveryStatus`, and accepts `IMailSubmissionRequestAuth` so service-mail credentials can query their own accepted, queued, deferred, delivered, or failed status. `TMailAddressBindingSync.outboundEnabled` explicitly controls whether a gateway should maintain a managed outbound SMTP credential for an address binding. Binding credential metadata is public; `rotateMailCredential` returns the new secret only in its one-time `IMailCredentialOneTimeSecret` response.
|
|
144
158
|
|
|
145
159
|
Request groups are exported by product area:
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import type { IDeploymentPreflightProposedConfiguration, IDeploymentPreflightRuntimeTask } from './deploymentpreflight.js';
|
|
2
|
+
import type { IImageRolloutStatus, IImmutableImageDeploymentPlan, TSha256Digest } from './immutableimage.js';
|
|
3
|
+
import { normalizeImmutableReleaseTag, normalizeSha256Digest } from './immutableimage.js';
|
|
4
|
+
import type { IRegistryTarget } from './registry.js';
|
|
5
|
+
import type { TServiceTargetPortRef } from './serviceports.js';
|
|
6
|
+
|
|
7
|
+
export type TDeploymentOperationMode = 'existing-service' | 'greenfield';
|
|
8
|
+
export type TDeploymentOperationPhase = 'reserve' | 'promote' | 'route';
|
|
9
|
+
|
|
10
|
+
interface IDeploymentReservationInputBase {
|
|
11
|
+
organizationId: string;
|
|
12
|
+
serviceId: string;
|
|
13
|
+
idempotencyKey: string;
|
|
14
|
+
sourceRevision: string;
|
|
15
|
+
version: string;
|
|
16
|
+
intentDigest: TSha256Digest;
|
|
17
|
+
proposedConfiguration: IDeploymentPreflightProposedConfiguration;
|
|
18
|
+
releaseTag: string;
|
|
19
|
+
routes: IDeploymentRouteRequest[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface IExistingServiceDeploymentReservationInput
|
|
23
|
+
extends IDeploymentReservationInputBase {
|
|
24
|
+
mode: 'existing-service';
|
|
25
|
+
expectedRolloutGeneration: number;
|
|
26
|
+
expectedCurrentRolloutId?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface IGreenfieldServiceDeploymentReservationInput
|
|
30
|
+
extends IDeploymentReservationInputBase {
|
|
31
|
+
mode: 'greenfield';
|
|
32
|
+
expectedRolloutGeneration?: never;
|
|
33
|
+
expectedCurrentRolloutId?: never;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type IDeploymentReservationInput =
|
|
37
|
+
| IExistingServiceDeploymentReservationInput
|
|
38
|
+
| IGreenfieldServiceDeploymentReservationInput;
|
|
39
|
+
|
|
40
|
+
export type TDeploymentOperationState =
|
|
41
|
+
| 'reserving'
|
|
42
|
+
| 'reserved'
|
|
43
|
+
| 'provisioning'
|
|
44
|
+
| 'awaiting-image'
|
|
45
|
+
| 'image-recorded'
|
|
46
|
+
| 'promoting-image'
|
|
47
|
+
| 'rolling-out'
|
|
48
|
+
| 'ready-for-route'
|
|
49
|
+
| 'promoting-route'
|
|
50
|
+
| 'verifying-route'
|
|
51
|
+
| 'succeeded'
|
|
52
|
+
| 'rolling-back'
|
|
53
|
+
| 'rolled-back'
|
|
54
|
+
| 'failed'
|
|
55
|
+
| 'cleaning'
|
|
56
|
+
| 'cleaned';
|
|
57
|
+
|
|
58
|
+
export interface IDeploymentRouteRequest {
|
|
59
|
+
hostname: string;
|
|
60
|
+
targetPort: Extract<TServiceTargetPortRef, number>;
|
|
61
|
+
/** Required exact one-label hostname used to verify a wildcard route. */
|
|
62
|
+
verificationHostname?: string;
|
|
63
|
+
readinessPath?: string;
|
|
64
|
+
expectedStatusCodes?: number[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface IDeploymentRouteClaimTarget {
|
|
68
|
+
hostname: string;
|
|
69
|
+
bucketHostname: string;
|
|
70
|
+
wildcard: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const canonicalHostnameRegex = /^(?:\*\.)?[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$/;
|
|
74
|
+
|
|
75
|
+
export const canonicalizeDeploymentHostname = (hostnameArg: unknown): string | undefined => {
|
|
76
|
+
if (typeof hostnameArg !== 'string') return undefined;
|
|
77
|
+
const hostname = hostnameArg.trim().toLowerCase().replace(/\.$/, '');
|
|
78
|
+
return hostname.length <= 253 && canonicalHostnameRegex.test(hostname)
|
|
79
|
+
? hostname
|
|
80
|
+
: undefined;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Wildcards match exactly one label. Exact siblings share a bucket document
|
|
85
|
+
* but may coexist; a wildcard claim excludes every exact name in its bucket.
|
|
86
|
+
*/
|
|
87
|
+
export const getDeploymentRouteClaimTarget = (
|
|
88
|
+
hostnameArg: string,
|
|
89
|
+
): IDeploymentRouteClaimTarget | undefined => {
|
|
90
|
+
const hostname = canonicalizeDeploymentHostname(hostnameArg);
|
|
91
|
+
if (!hostname) return undefined;
|
|
92
|
+
const wildcard = hostname.startsWith('*.');
|
|
93
|
+
const labels = (wildcard ? hostname.slice(2) : hostname).split('.');
|
|
94
|
+
const bucketHostname = wildcard ? labels.join('.') : labels.slice(1).join('.');
|
|
95
|
+
if (!bucketHostname || (wildcard && labels.length < 2)) return undefined;
|
|
96
|
+
return { hostname, bucketHostname, wildcard };
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const getDeploymentRouteVerificationHostname = (
|
|
100
|
+
routeArg: IDeploymentRouteRequest,
|
|
101
|
+
): string | undefined => {
|
|
102
|
+
const target = getDeploymentRouteClaimTarget(routeArg.hostname);
|
|
103
|
+
if (!target) return undefined;
|
|
104
|
+
if (!target.wildcard) {
|
|
105
|
+
return routeArg.verificationHostname === undefined
|
|
106
|
+
|| routeArg.verificationHostname === target.hostname
|
|
107
|
+
? target.hostname
|
|
108
|
+
: undefined;
|
|
109
|
+
}
|
|
110
|
+
if (!routeArg.verificationHostname) return undefined;
|
|
111
|
+
const verificationTarget = getDeploymentRouteClaimTarget(routeArg.verificationHostname);
|
|
112
|
+
return verificationTarget
|
|
113
|
+
&& !verificationTarget.wildcard
|
|
114
|
+
&& verificationTarget.bucketHostname === target.bucketHostname
|
|
115
|
+
? verificationTarget.hostname
|
|
116
|
+
: undefined;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export const normalizeDeploymentReadinessPath = (pathArg?: unknown): string | undefined => {
|
|
120
|
+
if (pathArg !== undefined && typeof pathArg !== 'string') return undefined;
|
|
121
|
+
const path = pathArg || '/';
|
|
122
|
+
return /^\/(?!\/)[A-Za-z0-9._~!$&'()*+,;=:@%\/-]{0,1023}$/.test(path)
|
|
123
|
+
? path
|
|
124
|
+
: undefined;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const deploymentIdentifierRegex = /^[A-Za-z0-9][A-Za-z0-9:._-]{0,199}$/;
|
|
128
|
+
const fullGitRevisionRegex = /^[a-f0-9]{40}$/;
|
|
129
|
+
const semanticVersionRegex = /^(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)(?:-(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
130
|
+
|
|
131
|
+
const isRecord = (valueArg: unknown): valueArg is Record<string, unknown> => (
|
|
132
|
+
Boolean(valueArg) && typeof valueArg === 'object' && !Array.isArray(valueArg)
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const validateStringArray = (
|
|
136
|
+
valueArg: unknown,
|
|
137
|
+
maxItemsArg: number,
|
|
138
|
+
validatorArg: (valueArg: string) => boolean,
|
|
139
|
+
): valueArg is string[] => Array.isArray(valueArg)
|
|
140
|
+
&& valueArg.length <= maxItemsArg
|
|
141
|
+
&& valueArg.every((itemArg) => typeof itemArg === 'string'
|
|
142
|
+
&& itemArg.length > 0
|
|
143
|
+
&& itemArg.length <= 253
|
|
144
|
+
&& validatorArg(itemArg))
|
|
145
|
+
&& new Set(valueArg).size === valueArg.length;
|
|
146
|
+
|
|
147
|
+
export const validateDeploymentProposedConfiguration = (
|
|
148
|
+
configurationArg: unknown,
|
|
149
|
+
): string[] => {
|
|
150
|
+
if (!isRecord(configurationArg)) return ['proposedConfiguration must be an object'];
|
|
151
|
+
const errors: string[] = [];
|
|
152
|
+
if (configurationArg.schemaVersion !== 1) errors.push('schemaVersion must be 1');
|
|
153
|
+
if (typeof configurationArg.declarationComplete !== 'boolean') {
|
|
154
|
+
errors.push('declarationComplete must be boolean');
|
|
155
|
+
}
|
|
156
|
+
if (typeof configurationArg.projectName !== 'string'
|
|
157
|
+
|| !configurationArg.projectName.trim()
|
|
158
|
+
|| configurationArg.projectName.length > 200) {
|
|
159
|
+
errors.push('projectName must be bounded and non-empty');
|
|
160
|
+
}
|
|
161
|
+
if (!validateStringArray(configurationArg.targetPlatforms, 8, (platformArg) => (
|
|
162
|
+
/^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*$/.test(platformArg)
|
|
163
|
+
))) {
|
|
164
|
+
errors.push('targetPlatforms must be unique canonical platform identifiers');
|
|
165
|
+
}
|
|
166
|
+
if (!validateStringArray(configurationArg.publicDomains, 32, (domainArg) => (
|
|
167
|
+
canonicalizeDeploymentHostname(domainArg) === domainArg
|
|
168
|
+
))) {
|
|
169
|
+
errors.push('publicDomains must be unique canonical exact or wildcard hostnames');
|
|
170
|
+
}
|
|
171
|
+
if (!validateStringArray(configurationArg.environmentVariableNames, 256, (nameArg) => (
|
|
172
|
+
nameArg.length <= 253 && /^[A-Z_][A-Z0-9_]*$/.test(nameArg)
|
|
173
|
+
))) {
|
|
174
|
+
errors.push('environmentVariableNames must be unique canonical names');
|
|
175
|
+
}
|
|
176
|
+
if (!validateStringArray(configurationArg.secretReferenceIds, 64, (idArg) => (
|
|
177
|
+
idArg.length <= 253 && /^[A-Za-z0-9][A-Za-z0-9:._-]*$/.test(idArg)
|
|
178
|
+
))) {
|
|
179
|
+
errors.push('secretReferenceIds must be unique canonical identifiers');
|
|
180
|
+
}
|
|
181
|
+
if (!Array.isArray(configurationArg.containerPorts)
|
|
182
|
+
|| configurationArg.containerPorts.length > 64
|
|
183
|
+
|| configurationArg.containerPorts.some((portArg) => !Number.isSafeInteger(portArg)
|
|
184
|
+
|| portArg < 1 || portArg > 65535)
|
|
185
|
+
|| new Set(configurationArg.containerPorts).size !== configurationArg.containerPorts.length) {
|
|
186
|
+
errors.push('containerPorts must be unique valid ports');
|
|
187
|
+
}
|
|
188
|
+
if (!Array.isArray(configurationArg.volumeMounts)
|
|
189
|
+
|| configurationArg.volumeMounts.length > 32
|
|
190
|
+
|| configurationArg.volumeMounts.some((mountArg) => !isRecord(mountArg)
|
|
191
|
+
|| typeof mountArg.mountPath !== 'string'
|
|
192
|
+
|| !/^\/[A-Za-z0-9._/-]{0,254}$/.test(mountArg.mountPath)
|
|
193
|
+
|| (mountArg.storageClass !== 'corestore' && mountArg.storageClass !== 'ephemeral')
|
|
194
|
+
|| (mountArg.capability !== undefined
|
|
195
|
+
&& mountArg.capability !== 'database'
|
|
196
|
+
&& mountArg.capability !== 'objectstorage'))) {
|
|
197
|
+
errors.push('volumeMounts must be canonical supported declarations');
|
|
198
|
+
}
|
|
199
|
+
if (!Array.isArray(configurationArg.requiredCapabilities)
|
|
200
|
+
|| configurationArg.requiredCapabilities.length > 2
|
|
201
|
+
|| configurationArg.requiredCapabilities.some((capabilityArg) => (
|
|
202
|
+
capabilityArg !== 'database' && capabilityArg !== 'objectstorage'
|
|
203
|
+
))
|
|
204
|
+
|| new Set(configurationArg.requiredCapabilities).size
|
|
205
|
+
!== configurationArg.requiredCapabilities.length) {
|
|
206
|
+
errors.push('requiredCapabilities must be a unique supported capability set');
|
|
207
|
+
}
|
|
208
|
+
return errors;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
export const validateDeploymentReservationInput = (inputArg: unknown): string[] => {
|
|
212
|
+
if (!isRecord(inputArg)) return ['reservation input must be an object'];
|
|
213
|
+
const errors: string[] = [];
|
|
214
|
+
if (inputArg.mode !== 'existing-service' && inputArg.mode !== 'greenfield') {
|
|
215
|
+
errors.push('mode must identify an existing-service or greenfield deployment');
|
|
216
|
+
}
|
|
217
|
+
for (const field of ['organizationId', 'serviceId', 'idempotencyKey'] as const) {
|
|
218
|
+
if (typeof inputArg[field] !== 'string'
|
|
219
|
+
|| !deploymentIdentifierRegex.test(inputArg[field] as string)) {
|
|
220
|
+
errors.push(`${field} must be a bounded canonical identifier`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (typeof inputArg.sourceRevision !== 'string'
|
|
224
|
+
|| !fullGitRevisionRegex.test(inputArg.sourceRevision)) {
|
|
225
|
+
errors.push('sourceRevision must be a full lowercase git revision');
|
|
226
|
+
}
|
|
227
|
+
if (typeof inputArg.version !== 'string' || !semanticVersionRegex.test(inputArg.version)) {
|
|
228
|
+
errors.push('version must be a canonical semantic version');
|
|
229
|
+
}
|
|
230
|
+
if (!normalizeSha256Digest(inputArg.intentDigest)) {
|
|
231
|
+
errors.push('intentDigest must be a canonical sha256 digest');
|
|
232
|
+
}
|
|
233
|
+
if (!normalizeImmutableReleaseTag(inputArg.releaseTag)) {
|
|
234
|
+
errors.push('releaseTag must be a canonical non-latest OCI tag');
|
|
235
|
+
}
|
|
236
|
+
const configuration = isRecord(inputArg.proposedConfiguration)
|
|
237
|
+
? inputArg.proposedConfiguration
|
|
238
|
+
: undefined;
|
|
239
|
+
errors.push(...validateDeploymentProposedConfiguration(inputArg.proposedConfiguration));
|
|
240
|
+
const containerPorts = configuration && Array.isArray(configuration.containerPorts)
|
|
241
|
+
? configuration.containerPorts
|
|
242
|
+
: undefined;
|
|
243
|
+
const publicDomains = configuration && Array.isArray(configuration.publicDomains)
|
|
244
|
+
? configuration.publicDomains.filter((domainArg): domainArg is string => (
|
|
245
|
+
typeof domainArg === 'string'
|
|
246
|
+
))
|
|
247
|
+
: undefined;
|
|
248
|
+
if (configuration?.declarationComplete !== true) {
|
|
249
|
+
errors.push('proposedConfiguration must declare itself complete');
|
|
250
|
+
}
|
|
251
|
+
const routes = Array.isArray(inputArg.routes) ? inputArg.routes : undefined;
|
|
252
|
+
const routeHostnames = routes?.map((routeArg) => (
|
|
253
|
+
isRecord(routeArg) && typeof routeArg.hostname === 'string' ? routeArg.hostname : undefined
|
|
254
|
+
));
|
|
255
|
+
if (!routes
|
|
256
|
+
|| routes.length > 32
|
|
257
|
+
|| !routeHostnames
|
|
258
|
+
|| new Set(routeHostnames).size !== routeHostnames.length
|
|
259
|
+
|| routes.some((routeArg) => {
|
|
260
|
+
if (!isRecord(routeArg)
|
|
261
|
+
|| typeof routeArg.hostname !== 'string'
|
|
262
|
+
|| canonicalizeDeploymentHostname(routeArg.hostname) !== routeArg.hostname
|
|
263
|
+
|| !Number.isSafeInteger(routeArg.targetPort)
|
|
264
|
+
|| (routeArg.targetPort as number) < 1
|
|
265
|
+
|| (routeArg.targetPort as number) > 65535
|
|
266
|
+
|| !containerPorts?.includes(routeArg.targetPort)) {
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
const typedRoute = routeArg as unknown as IDeploymentRouteRequest;
|
|
270
|
+
return getDeploymentRouteVerificationHostname(typedRoute) === undefined
|
|
271
|
+
|| (routeArg.readinessPath !== undefined
|
|
272
|
+
&& (typeof routeArg.readinessPath !== 'string'
|
|
273
|
+
|| normalizeDeploymentReadinessPath(routeArg.readinessPath) !== routeArg.readinessPath))
|
|
274
|
+
|| (routeArg.expectedStatusCodes !== undefined
|
|
275
|
+
&& (!Array.isArray(routeArg.expectedStatusCodes)
|
|
276
|
+
|| routeArg.expectedStatusCodes.length < 1
|
|
277
|
+
|| routeArg.expectedStatusCodes.length > 16
|
|
278
|
+
|| new Set(routeArg.expectedStatusCodes).size !== routeArg.expectedStatusCodes.length
|
|
279
|
+
|| routeArg.expectedStatusCodes.some((statusArg) => !Number.isSafeInteger(statusArg)
|
|
280
|
+
|| statusArg < 200 || statusArg > 299)));
|
|
281
|
+
})) {
|
|
282
|
+
errors.push('routes must be unique canonical declarations over declared numeric ports');
|
|
283
|
+
}
|
|
284
|
+
if (inputArg.mode === 'greenfield') {
|
|
285
|
+
if (inputArg.expectedRolloutGeneration !== undefined
|
|
286
|
+
|| inputArg.expectedCurrentRolloutId !== undefined) {
|
|
287
|
+
errors.push('greenfield reservation must not carry an existing rollout fence');
|
|
288
|
+
}
|
|
289
|
+
if (publicDomains && routeHostnames) {
|
|
290
|
+
const declared = [...new Set(publicDomains)].sort();
|
|
291
|
+
const routed = [...new Set(routeHostnames.filter((valueArg): valueArg is string => (
|
|
292
|
+
typeof valueArg === 'string'
|
|
293
|
+
)))].sort();
|
|
294
|
+
if (JSON.stringify(declared) !== JSON.stringify(routed)) {
|
|
295
|
+
errors.push('greenfield routes must exactly match proposed publicDomains');
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (inputArg.mode === 'existing-service') {
|
|
300
|
+
if (!Number.isSafeInteger(inputArg.expectedRolloutGeneration)
|
|
301
|
+
|| (inputArg.expectedRolloutGeneration as number) < 0) {
|
|
302
|
+
errors.push('existing-service reservation requires an explicit rollout generation fence');
|
|
303
|
+
}
|
|
304
|
+
if (inputArg.expectedCurrentRolloutId !== undefined
|
|
305
|
+
&& (typeof inputArg.expectedCurrentRolloutId !== 'string'
|
|
306
|
+
|| !deploymentIdentifierRegex.test(inputArg.expectedCurrentRolloutId))) {
|
|
307
|
+
errors.push('expectedCurrentRolloutId must be a bounded canonical identifier');
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return errors;
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
interface IDeploymentRouteVerificationEvidenceBase {
|
|
314
|
+
hostname: string;
|
|
315
|
+
url: string;
|
|
316
|
+
checkedAt: number;
|
|
317
|
+
resolvedAddresses: string[];
|
|
318
|
+
rolloutId: string;
|
|
319
|
+
rolloutGeneration: number;
|
|
320
|
+
expectedDigest: TSha256Digest;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export interface IDeploymentRouteVerificationSuccessEvidence
|
|
324
|
+
extends IDeploymentRouteVerificationEvidenceBase {
|
|
325
|
+
outcome: 'succeeded';
|
|
326
|
+
tls: {
|
|
327
|
+
authorized: true;
|
|
328
|
+
servername: string;
|
|
329
|
+
subjectAlternativeNames: string[];
|
|
330
|
+
validFrom?: string;
|
|
331
|
+
validTo?: string;
|
|
332
|
+
};
|
|
333
|
+
http: {
|
|
334
|
+
statusCode: number;
|
|
335
|
+
redirected: false;
|
|
336
|
+
};
|
|
337
|
+
readiness: {
|
|
338
|
+
path: string;
|
|
339
|
+
expectedStatusCodes: number[];
|
|
340
|
+
ready: true;
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export interface IDeploymentRouteVerificationFailureEvidence
|
|
345
|
+
extends IDeploymentRouteVerificationEvidenceBase {
|
|
346
|
+
outcome: 'failed';
|
|
347
|
+
stage: 'dns' | 'address-policy' | 'connect' | 'tls' | 'http' | 'readiness';
|
|
348
|
+
code: string;
|
|
349
|
+
message: string;
|
|
350
|
+
tls?: IDeploymentRouteVerificationSuccessEvidence['tls'];
|
|
351
|
+
http?: IDeploymentRouteVerificationSuccessEvidence['http'];
|
|
352
|
+
readiness?: IDeploymentRouteVerificationSuccessEvidence['readiness'];
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export type IDeploymentRouteVerificationEvidence =
|
|
356
|
+
| IDeploymentRouteVerificationSuccessEvidence
|
|
357
|
+
| IDeploymentRouteVerificationFailureEvidence;
|
|
358
|
+
|
|
359
|
+
export interface IDeploymentOperationFailure {
|
|
360
|
+
code: string;
|
|
361
|
+
message: string;
|
|
362
|
+
retryable: boolean;
|
|
363
|
+
failedAt: number;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Durable deployment fence. All state transitions compare-and-set both id and
|
|
368
|
+
* revision. Claim keys are server-generated and never accepted as authority.
|
|
369
|
+
*/
|
|
370
|
+
export interface IServiceDeploymentOperation {
|
|
371
|
+
id: string;
|
|
372
|
+
revision: number;
|
|
373
|
+
data: {
|
|
374
|
+
mode: TDeploymentOperationMode;
|
|
375
|
+
organizationId: string;
|
|
376
|
+
serviceId: string;
|
|
377
|
+
actorUserId: string;
|
|
378
|
+
idempotencyKey: string;
|
|
379
|
+
requestDigest: TSha256Digest;
|
|
380
|
+
sourceRevision: string;
|
|
381
|
+
version: string;
|
|
382
|
+
intentDigest: TSha256Digest;
|
|
383
|
+
configurationDigest: TSha256Digest;
|
|
384
|
+
proposedConfiguration: IDeploymentPreflightProposedConfiguration;
|
|
385
|
+
namespace: string;
|
|
386
|
+
registryTarget: IRegistryTarget;
|
|
387
|
+
releaseTag: string;
|
|
388
|
+
routes: IDeploymentRouteRequest[];
|
|
389
|
+
claimKeys: string[];
|
|
390
|
+
state: TDeploymentOperationState;
|
|
391
|
+
releaseId?: string;
|
|
392
|
+
registryRootDigest?: TSha256Digest;
|
|
393
|
+
rolloutId?: string;
|
|
394
|
+
rolloutGeneration?: number;
|
|
395
|
+
routeGeneration?: number;
|
|
396
|
+
routeVerification?: IDeploymentRouteVerificationEvidence[];
|
|
397
|
+
failure?: IDeploymentOperationFailure;
|
|
398
|
+
createdAt: number;
|
|
399
|
+
updatedAt: number;
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
export interface IServiceDeploymentStatus {
|
|
404
|
+
operation: IServiceDeploymentOperation;
|
|
405
|
+
imageDeployment?: IImmutableImageDeploymentPlan;
|
|
406
|
+
imageRolloutStatus?: IImageRolloutStatus;
|
|
407
|
+
runtimeTasks: IDeploymentRuntimeTaskEvidence[];
|
|
408
|
+
routeVerification: IDeploymentRouteVerificationEvidence[];
|
|
409
|
+
immutableRolloutSucceeded: boolean;
|
|
410
|
+
runtimeDigestVerified: boolean;
|
|
411
|
+
routeVerified: boolean;
|
|
412
|
+
succeeded: boolean;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
interface IDeploymentRuntimeTaskEvidenceBase extends Omit<
|
|
416
|
+
IDeploymentPreflightRuntimeTask,
|
|
417
|
+
'verificationStatus' | 'observedDigest' | 'reportedDigest'
|
|
418
|
+
> {
|
|
419
|
+
rolloutId: string;
|
|
420
|
+
rolloutGeneration: number;
|
|
421
|
+
expectedDigest: TSha256Digest;
|
|
422
|
+
observedAt: number;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
export interface IVerifiedDeploymentRuntimeTaskEvidence
|
|
426
|
+
extends IDeploymentRuntimeTaskEvidenceBase {
|
|
427
|
+
verificationStatus: 'verified';
|
|
428
|
+
observedDigest: TSha256Digest;
|
|
429
|
+
reportedDigest: TSha256Digest;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export interface IUnverifiedDeploymentRuntimeTaskEvidence
|
|
433
|
+
extends IDeploymentRuntimeTaskEvidenceBase {
|
|
434
|
+
verificationStatus: 'missing' | 'mismatched';
|
|
435
|
+
observedDigest?: TSha256Digest;
|
|
436
|
+
reportedDigest?: TSha256Digest;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
export type IDeploymentRuntimeTaskEvidence =
|
|
440
|
+
| IVerifiedDeploymentRuntimeTaskEvidence
|
|
441
|
+
| IUnverifiedDeploymentRuntimeTaskEvidence;
|