@serve.zone/interfaces 32.29.0 → 32.30.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 +49 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/data/networkprojection.d.ts +74 -3
- package/dist_ts/data/networkprojection.js +21 -2
- package/dist_ts/data/serviceruntime.d.ts +19 -3
- package/dist_ts/data/serviceruntime.js +2 -2
- package/dist_ts/platform/corestoreendpointselection.d.ts +37 -0
- package/dist_ts/platform/corestoreendpointselection.js +45 -0
- package/dist_ts/platform/index.d.ts +3 -1
- package/dist_ts/platform/index.js +4 -2
- package/dist_ts/platform/objectstorage.js +5 -1
- package/dist_ts/private/networkcontract.d.ts +2 -0
- package/dist_ts/private/networkcontract.js +9 -4
- package/dist_ts/requests/network.d.ts +60 -2
- package/package.json +3 -3
- package/readme.md +64 -7
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/data/networkprojection.ts +96 -4
- package/ts/data/serviceruntime.ts +19 -3
- package/ts/platform/corestoreendpointselection.ts +79 -0
- package/ts/platform/index.ts +3 -0
- package/ts/platform/objectstorage.ts +4 -0
- package/ts/private/networkcontract.ts +7 -3
- package/ts/requests/network.ts +56 -2
|
@@ -33,7 +33,8 @@ export interface IServiceRuntimeProfile {
|
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
35
|
* `isolated` gives the workload no network authority. `attached` joins the runtime network with
|
|
36
|
-
* public egress on or off and the platform endpoints it may reach, by id: sorted and unique.
|
|
36
|
+
* public egress on or off and the platform endpoints it may reach, by id: sorted and unique. The
|
|
37
|
+
* service's `IServiceNetworkEgress` must state the same values, or the workload is not admitted.
|
|
37
38
|
*/
|
|
38
39
|
export type TServiceRuntimeNetwork =
|
|
39
40
|
| { mode: 'isolated' }
|
|
@@ -89,7 +90,7 @@ const serviceRuntimeRefusals = [
|
|
|
89
90
|
'volume-driver-unsupported', 'volume-options-unsupported', 'volume-shared-unsupported',
|
|
90
91
|
'volume-placement-unsupported', 'volume-initialization-unknown', 'volume-ownership-unknown',
|
|
91
92
|
'objectstorage-endpoint-missing', 'objectstorage-binding-invalid', 'objectstorage-environment-conflict',
|
|
92
|
-
'database-endpoint-missing',
|
|
93
|
+
'objectstorage-endpoint-unselected', 'database-endpoint-missing', 'database-endpoint-unselected',
|
|
93
94
|
] as const;
|
|
94
95
|
|
|
95
96
|
/**
|
|
@@ -114,7 +115,8 @@ export type TServiceRuntimeWaitingReason = typeof serviceRuntimeWaitingReasons[n
|
|
|
114
115
|
* `port-protocol-unsupported` is about one port's protocol — and `node-authority-unsupported` is a
|
|
115
116
|
* node whose session resolved fewer workload authorities than this spec needs, which the admit step
|
|
116
117
|
* refuses rather than handing the node work it would silently park. An attached network needs an
|
|
117
|
-
* explicit matching policy
|
|
118
|
+
* explicit matching policy (`IServiceNetworkEgress`, written by `setServiceNetworkEgress`) and a
|
|
119
|
+
* current workload address pool; neither is defaulted.
|
|
118
120
|
*
|
|
119
121
|
* The volume refusals name what a local storage claim cannot express: `volume-driver-unsupported`
|
|
120
122
|
* a volume of a driver other than a node-local directory, `volume-options-unsupported` driver or
|
|
@@ -133,6 +135,20 @@ export type TServiceRuntimeWaitingReason = typeof serviceRuntimeWaitingReasons[n
|
|
|
133
135
|
* `database-endpoint-missing` is an enabled database binding placed on a node that states no valid
|
|
134
136
|
* `corestoreDatabase`: its credential cannot be addressed to any host a workload reaches, so it is
|
|
135
137
|
* neither granted nor published, and the workload is not started with a guessed one.
|
|
138
|
+
*
|
|
139
|
+
* The two `-unselected` refusals name a binding endpoint the workload's network cannot reach, so a
|
|
140
|
+
* workload is refused rather than started against a connection that stays dark. A workload reaches
|
|
141
|
+
* a platform endpoint only when its spec's attached network selects it (`platformEndpointIds`), and
|
|
142
|
+
* an `isolated` network selects none. `database-endpoint-unselected` is an enabled database binding
|
|
143
|
+
* whose node's valid `corestoreDatabase` is not a selected endpoint: none of the selected
|
|
144
|
+
* `IRuntimeNetworkProtectedAuthority.platformEndpoints` has the endpoint's `host` as its `address`,
|
|
145
|
+
* `tcp` as its `protocol` and the endpoint's `port` as its `port`. `objectstorage-endpoint-unselected`
|
|
146
|
+
* is the same for an enabled object-storage binding's node's valid `corestoreObjectStorage`, whose
|
|
147
|
+
* `endpoint` origin states the host and the port (the scheme's default, 80 for `http:` and 443 for
|
|
148
|
+
* `https:`, when it states none). Platform endpoints are IPv4, so an endpoint whose host is a DNS name
|
|
149
|
+
* or an IPv6 literal is never selected. Each is judged only for an endpoint that is valid: a node
|
|
150
|
+
* stating none, or an invalid one, stays `database-endpoint-missing` or `objectstorage-endpoint-missing`.
|
|
151
|
+
* `platform.findUnselectedCorestoreEndpoints` is this rule; a producer answers what it returns.
|
|
136
152
|
*/
|
|
137
153
|
export type TServiceRuntimeRefusal = typeof serviceRuntimeRefusals[number];
|
|
138
154
|
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { IRuntimeNetworkProtectedAuthority } from '../data/networkauthority.js';
|
|
2
|
+
import type { TServiceRuntimeNetwork, TServiceRuntimeRefusal } from '../data/serviceruntime.js';
|
|
3
|
+
import { isNetworkIpv4 } from '../private/networkcontract.js';
|
|
4
|
+
import { validatePlatformDatabaseEndpoint, type IPlatformDatabaseEndpoint } from './database.js';
|
|
5
|
+
import { validatePlatformObjectStorageEndpoint, type IPlatformObjectStorageEndpoint } from './objectstorage.js';
|
|
6
|
+
|
|
7
|
+
/** The service runtime refusals a Corestore endpoint the workload's network does not select raises. */
|
|
8
|
+
export type TCorestoreEndpointSelectionRefusal = Extract<TServiceRuntimeRefusal,
|
|
9
|
+
'database-endpoint-unselected' | 'objectstorage-endpoint-unselected'>;
|
|
10
|
+
|
|
11
|
+
/** What `findUnselectedCorestoreEndpoints` judges for one node a service's bindings are placed on. */
|
|
12
|
+
export interface ICorestoreEndpointSelection {
|
|
13
|
+
/**
|
|
14
|
+
* The node's `corestoreDatabase` when an enabled database binding of the service is placed on it,
|
|
15
|
+
* otherwise `null`. A node stating none is `database-endpoint-missing`, never judged here.
|
|
16
|
+
*/
|
|
17
|
+
database: IPlatformDatabaseEndpoint | null;
|
|
18
|
+
/**
|
|
19
|
+
* The node's `corestoreObjectStorage` when an enabled object-storage binding of the service is
|
|
20
|
+
* placed on it, otherwise `null`. A node stating none is `objectstorage-endpoint-missing`.
|
|
21
|
+
*/
|
|
22
|
+
objectStorage: IPlatformObjectStorageEndpoint | null;
|
|
23
|
+
/** The current protected authority's platform endpoints, which the selected ids resolve against. */
|
|
24
|
+
platformEndpoints: IRuntimeNetworkProtectedAuthority['platformEndpoints'];
|
|
25
|
+
/** The runtime spec's network: `attached` selects its `platformEndpointIds`, `isolated` selects none. */
|
|
26
|
+
network: TServiceRuntimeNetwork;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type TPlatformEndpoint = IRuntimeNetworkProtectedAuthority['platformEndpoints'][number];
|
|
30
|
+
|
|
31
|
+
const selectedPlatformEndpoints = (selectionArg: ICorestoreEndpointSelection): TPlatformEndpoint[] => {
|
|
32
|
+
if (selectionArg.network.mode !== 'attached') return [];
|
|
33
|
+
const selectedIds = new Set(selectionArg.network.platformEndpointIds);
|
|
34
|
+
return selectionArg.platformEndpoints.filter((endpointArg) => selectedIds.has(endpointArg.id));
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Platform endpoints are IPv4, so only a canonical IPv4 host can be one; a DNS name or IPv6 literal never is. */
|
|
38
|
+
const selectsTcp = (endpointsArg: TPlatformEndpoint[], hostArg: string, portArg: number): boolean => (
|
|
39
|
+
isNetworkIpv4(hostArg) && endpointsArg.some((endpointArg) => endpointArg.address === hostArg
|
|
40
|
+
&& endpointArg.protocol === 'tcp' && endpointArg.port === portArg)
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
/** The origin's port, or the scheme's default (80 for `http:`, 443 for `https:`) when it states none. */
|
|
44
|
+
const objectStorageOriginPort = (originArg: URL): number => (
|
|
45
|
+
originArg.port === '' ? (originArg.protocol === 'https:' ? 443 : 80) : Number(originArg.port)
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Which of a node's Corestore endpoints a service's workload would dial without its network
|
|
50
|
+
* selecting them, as the service runtime refusals that name them; empty when none.
|
|
51
|
+
*
|
|
52
|
+
* An endpoint is selected when one of the spec's selected platform endpoints has the endpoint's host
|
|
53
|
+
* as its `address`, `tcp` as its `protocol` and the endpoint's port as its `port`: the database
|
|
54
|
+
* endpoint's `host` and `port`, and the object-storage endpoint origin's host and port, or 80 for
|
|
55
|
+
* `http:` and 443 for `https:` when the origin states none. An `isolated` network selects nothing, a
|
|
56
|
+
* selected id the protected authority does not hold selects nothing, and a DNS-name or IPv6 host is
|
|
57
|
+
* never selected. Only a valid endpoint is judged: one `validatePlatformDatabaseEndpoint` or
|
|
58
|
+
* `validatePlatformObjectStorageEndpoint` refuses is the `-missing` refusal's, not this one's. The
|
|
59
|
+
* answer is sorted, so it does not depend on the order of the inputs.
|
|
60
|
+
*/
|
|
61
|
+
export const findUnselectedCorestoreEndpoints = (
|
|
62
|
+
selectionArg: ICorestoreEndpointSelection,
|
|
63
|
+
): TCorestoreEndpointSelectionRefusal[] => {
|
|
64
|
+
const selected = selectedPlatformEndpoints(selectionArg);
|
|
65
|
+
const refusals: TCorestoreEndpointSelectionRefusal[] = [];
|
|
66
|
+
const database = selectionArg.database;
|
|
67
|
+
if (database !== null && validatePlatformDatabaseEndpoint(database).length === 0
|
|
68
|
+
&& !selectsTcp(selected, database.host, database.port)) {
|
|
69
|
+
refusals.push('database-endpoint-unselected');
|
|
70
|
+
}
|
|
71
|
+
const objectStorage = selectionArg.objectStorage;
|
|
72
|
+
if (objectStorage !== null && validatePlatformObjectStorageEndpoint(objectStorage).length === 0) {
|
|
73
|
+
const origin = new URL(objectStorage.endpoint);
|
|
74
|
+
if (!selectsTcp(selected, origin.hostname, objectStorageOriginPort(origin))) {
|
|
75
|
+
refusals.push('objectstorage-endpoint-unselected');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return refusals;
|
|
79
|
+
};
|
package/ts/platform/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as ai from './ai.js';
|
|
2
2
|
import * as backup from './backup.js';
|
|
3
|
+
import * as corestoreendpointselection from './corestoreendpointselection.js';
|
|
3
4
|
import * as database from './database.js';
|
|
4
5
|
import * as email from './email.js';
|
|
5
6
|
import * as letter from './letter.js';
|
|
@@ -17,6 +18,7 @@ import * as types from './types.js';
|
|
|
17
18
|
export {
|
|
18
19
|
ai,
|
|
19
20
|
backup,
|
|
21
|
+
corestoreendpointselection,
|
|
20
22
|
database,
|
|
21
23
|
email,
|
|
22
24
|
letter,
|
|
@@ -32,6 +34,7 @@ export {
|
|
|
32
34
|
types,
|
|
33
35
|
};
|
|
34
36
|
|
|
37
|
+
export * from './corestoreendpointselection.js';
|
|
35
38
|
export * from './database.js';
|
|
36
39
|
export * from './storage.js';
|
|
37
40
|
export * from './objectstorage.js';
|
|
@@ -102,6 +102,10 @@ export const validatePlatformObjectStorageEndpoint = (
|
|
|
102
102
|
errors.push(`${pathArg} endpoint must be a canonical origin without a path, query, `
|
|
103
103
|
+ 'fragment, trailing slash or default port');
|
|
104
104
|
}
|
|
105
|
+
// A URL parser refuses a port above 65535 but reads port 0 as stated; no workload dials it.
|
|
106
|
+
if (parsedEndpoint.port !== '' && Number(parsedEndpoint.port) < 1) {
|
|
107
|
+
errors.push(`${pathArg} endpoint port must be from 1 to 65535`);
|
|
108
|
+
}
|
|
105
109
|
}
|
|
106
110
|
}
|
|
107
111
|
if (typeof valueArg.region !== 'string'
|
|
@@ -209,10 +209,14 @@ export const networkHash = async (domain: string, value: { digest: TSha256Digest
|
|
|
209
209
|
const { digest: omitted, ...payload } = value;
|
|
210
210
|
return `sha256:${await createCanonicalJsonSha256Hex(networkCanonical({ domain, payload }), failNetwork)}`;
|
|
211
211
|
};
|
|
212
|
-
|
|
213
|
-
|
|
212
|
+
/** A canonical dotted-quad IPv4 literal: four decimal octets, no leading zeros. */
|
|
213
|
+
export const isNetworkIpv4 = (value: unknown): value is string => {
|
|
214
|
+
if (typeof value !== 'string' || value.length > 15) return false;
|
|
214
215
|
const parts = value.split('.');
|
|
215
|
-
|
|
216
|
+
return parts.length === 4 && parts.every(part => /^(?:0|[1-9][0-9]{0,2})$/.test(part) && Number(part) <= 255);
|
|
217
|
+
};
|
|
218
|
+
export const networkIpv4 = (value: unknown): string => {
|
|
219
|
+
requireNetwork(isNetworkIpv4(value));
|
|
216
220
|
return value;
|
|
217
221
|
};
|
|
218
222
|
export const networkIpv4Number = (value: string): number => value.split('.').reduce((sum, part) => sum * 256 + Number(part), 0);
|
package/ts/requests/network.ts
CHANGED
|
@@ -6,7 +6,9 @@ import type {
|
|
|
6
6
|
IPrivateNetwork, IServicePrivateNetworks, ISetServicePrivateNetworks,
|
|
7
7
|
TPrivateNetworkMutation,
|
|
8
8
|
} from '../data/privatenetwork.js';
|
|
9
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
IServiceNetworkEgress, IServicePublishedPorts, ISetServiceNetworkEgress, ISetServicePublishedPorts,
|
|
11
|
+
} from '../data/networkprojection.js';
|
|
10
12
|
|
|
11
13
|
/** Current organization picker data. Entries are never cached authorization grants. */
|
|
12
14
|
export interface IReq_Cloudly_ListOwnedOrganizations extends plugins.typedrequestInterfaces.implementsTR<
|
|
@@ -71,7 +73,7 @@ export interface IReq_Cloudly_SetServicePrivateNetworks extends plugins.typedreq
|
|
|
71
73
|
response: { membership: IServicePrivateNetworks; replayed: boolean };
|
|
72
74
|
}
|
|
73
75
|
|
|
74
|
-
/** null means the service has never had
|
|
76
|
+
/** null means the service has never had a policy document, neither published ports nor egress. */
|
|
75
77
|
export interface IReq_Cloudly_GetServicePublishedPorts extends plugins.typedrequestInterfaces.implementsTR<
|
|
76
78
|
plugins.typedrequestInterfaces.ITypedRequest, IReq_Cloudly_GetServicePublishedPorts
|
|
77
79
|
> {
|
|
@@ -80,6 +82,17 @@ export interface IReq_Cloudly_GetServicePublishedPorts extends plugins.typedrequ
|
|
|
80
82
|
response: { publishedPorts: IServicePublishedPorts | null };
|
|
81
83
|
}
|
|
82
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Writes the published-port half of one service's policy document, carrying its egress forward.
|
|
87
|
+
*
|
|
88
|
+
* One document per service holds its published ports and its egress under one `revision`. The
|
|
89
|
+
* first accepted write of either (`expectedRevision: null`) creates it: `setServicePublishedPorts`
|
|
90
|
+
* with egress `publicEgress: false` and `platformEndpointIds: []`, `setServiceNetworkEgress` with
|
|
91
|
+
* no published ports. From then on both reads return it, never null. It is an explicit policy: an
|
|
92
|
+
* attached spec stating `publicEgress: false` and `platformEndpointIds: []` matches it and reaches
|
|
93
|
+
* no public address and no platform endpoint; `network-policy-missing` means the service has no
|
|
94
|
+
* document at all.
|
|
95
|
+
*/
|
|
83
96
|
export interface IReq_Cloudly_SetServicePublishedPorts extends plugins.typedrequestInterfaces.implementsTR<
|
|
84
97
|
plugins.typedrequestInterfaces.ITypedRequest, IReq_Cloudly_SetServicePublishedPorts
|
|
85
98
|
> {
|
|
@@ -93,6 +106,47 @@ export interface IReq_Cloudly_SetServicePublishedPorts extends plugins.typedrequ
|
|
|
93
106
|
response: { publishedPorts: IServicePublishedPorts; replayed: boolean };
|
|
94
107
|
}
|
|
95
108
|
|
|
109
|
+
/**
|
|
110
|
+
* The egress half of one service's policy document; null means the service has never had a policy
|
|
111
|
+
* document. Only a verified administrator reads it: platform endpoints are platform authority.
|
|
112
|
+
*/
|
|
113
|
+
export interface IReq_Admin_Cloudly_GetServiceNetworkEgress extends plugins.typedrequestInterfaces.implementsTR<
|
|
114
|
+
plugins.typedrequestInterfaces.ITypedRequest, IReq_Admin_Cloudly_GetServiceNetworkEgress
|
|
115
|
+
> {
|
|
116
|
+
method: 'getServiceNetworkEgress';
|
|
117
|
+
request: { identity: IIdentityCredential; organizationId: string; serviceId: string };
|
|
118
|
+
response: { networkEgress: IServiceNetworkEgress | null };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Writes the egress half of one service's policy document, carrying its published ports forward.
|
|
123
|
+
*
|
|
124
|
+
* Only a verified administrator writes it, as only an administrator writes the runtime spec whose
|
|
125
|
+
* attached network must state the same values. `expectedRevision` is the revision of the one
|
|
126
|
+
* document shared with the published ports; any other stored revision refuses the write, and an id
|
|
127
|
+
* that names no current platform endpoint refuses it. A retry of the exact body under the same
|
|
128
|
+
* `mutationId` answers the stored document with `replayed: true`.
|
|
129
|
+
*
|
|
130
|
+
* One document per service holds its published ports and its egress under one `revision`. The
|
|
131
|
+
* first accepted write of either (`expectedRevision: null`) creates it: `setServicePublishedPorts`
|
|
132
|
+
* with egress `publicEgress: false` and `platformEndpointIds: []`, `setServiceNetworkEgress` with
|
|
133
|
+
* no published ports. From then on both reads return it, never null. It is an explicit policy: an
|
|
134
|
+
* attached spec stating `publicEgress: false` and `platformEndpointIds: []` matches it and reaches
|
|
135
|
+
* no public address and no platform endpoint; `network-policy-missing` means the service has no
|
|
136
|
+
* document at all.
|
|
137
|
+
*/
|
|
138
|
+
export interface IReq_Admin_Cloudly_SetServiceNetworkEgress extends plugins.typedrequestInterfaces.implementsTR<
|
|
139
|
+
plugins.typedrequestInterfaces.ITypedRequest, IReq_Admin_Cloudly_SetServiceNetworkEgress
|
|
140
|
+
> {
|
|
141
|
+
method: 'setServiceNetworkEgress';
|
|
142
|
+
request: { identity: IIdentityCredential; mutation: ISetServiceNetworkEgress };
|
|
143
|
+
/**
|
|
144
|
+
* The accepted document including its `revision`; historical on replay. Acceptance is neither an
|
|
145
|
+
* admitted workload nor applied packet state.
|
|
146
|
+
*/
|
|
147
|
+
response: { networkEgress: IServiceNetworkEgress; replayed: boolean };
|
|
148
|
+
}
|
|
149
|
+
|
|
96
150
|
export interface IRequest_Any_Cloudly_GetNetworkNodes {
|
|
97
151
|
method: 'getNetworkNodes';
|
|
98
152
|
request: {};
|