@serve.zone/interfaces 32.59.0 → 32.60.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 +72 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/data/externalregistry.d.ts +37 -1
- package/dist_ts/data/index.d.ts +1 -0
- package/dist_ts/data/index.js +2 -1
- package/dist_ts/data/launchersecretowner.d.ts +60 -0
- package/dist_ts/data/launchersecretowner.js +94 -0
- package/dist_ts/data/secret.d.ts +44 -1
- package/dist_ts/data/secret.js +27 -6
- package/dist_ts/data/serviceruntime.d.ts +9 -2
- package/dist_ts/data/serviceruntime.js +2 -1
- package/dist_ts/platform/objectstorageenvironment.d.ts +12 -3
- package/dist_ts/platform/objectstorageenvironment.js +22 -6
- package/dist_ts/requests/externalregistry.d.ts +12 -6
- package/dist_ts/requests/service.d.ts +16 -1
- package/package.json +1 -1
- package/readme.md +75 -11
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/data/externalregistry.ts +60 -2
- package/ts/data/index.ts +1 -0
- package/ts/data/launchersecretowner.ts +145 -0
- package/ts/data/secret.ts +65 -7
- package/ts/data/serviceruntime.ts +9 -1
- package/ts/platform/objectstorageenvironment.ts +26 -8
- package/ts/requests/externalregistry.ts +12 -6
- package/ts/requests/service.ts +16 -1
|
@@ -8,7 +8,7 @@ export interface IReq_GetRegistryById extends plugins.typedrequestInterfaces.imp
|
|
|
8
8
|
id: string;
|
|
9
9
|
};
|
|
10
10
|
response: {
|
|
11
|
-
registry: data.
|
|
11
|
+
registry: data.IExternalRegistryPublic;
|
|
12
12
|
};
|
|
13
13
|
}
|
|
14
14
|
export interface IReq_GetRegistries extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_GetRegistries> {
|
|
@@ -17,7 +17,7 @@ export interface IReq_GetRegistries extends plugins.typedrequestInterfaces.imple
|
|
|
17
17
|
identity: userInterfaces.IIdentity;
|
|
18
18
|
};
|
|
19
19
|
response: {
|
|
20
|
-
registries: data.
|
|
20
|
+
registries: data.IExternalRegistryPublic[];
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
23
|
export interface IReq_CreateRegistry extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_CreateRegistry> {
|
|
@@ -27,7 +27,7 @@ export interface IReq_CreateRegistry extends plugins.typedrequestInterfaces.impl
|
|
|
27
27
|
registryData: data.IExternalRegistry['data'];
|
|
28
28
|
};
|
|
29
29
|
response: {
|
|
30
|
-
registry: data.
|
|
30
|
+
registry: data.IExternalRegistryPublic;
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
33
|
export interface IReq_UpdateRegistry extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_UpdateRegistry> {
|
|
@@ -35,10 +35,14 @@ export interface IReq_UpdateRegistry extends plugins.typedrequestInterfaces.impl
|
|
|
35
35
|
request: {
|
|
36
36
|
identity: userInterfaces.IIdentity;
|
|
37
37
|
registryId: string;
|
|
38
|
-
|
|
38
|
+
/**
|
|
39
|
+
* The operator fields to change; a field left out keeps its stored value. `password` is
|
|
40
|
+
* write-only: missing or empty keeps the stored credential, and `authType: 'none'` removes it.
|
|
41
|
+
*/
|
|
42
|
+
registryData: Partial<data.TExternalRegistryOperatorData>;
|
|
39
43
|
};
|
|
40
44
|
response: {
|
|
41
|
-
resultRegistry: data.
|
|
45
|
+
resultRegistry: data.IExternalRegistryPublic;
|
|
42
46
|
};
|
|
43
47
|
}
|
|
44
48
|
export interface IReq_DeleteRegistryById extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_DeleteRegistryById> {
|
|
@@ -58,8 +62,10 @@ export interface IReq_VerifyRegistry extends plugins.typedrequestInterfaces.impl
|
|
|
58
62
|
registryId: string;
|
|
59
63
|
};
|
|
60
64
|
response: {
|
|
65
|
+
/** True exactly for the `authenticated` and `anonymous` outcomes. */
|
|
61
66
|
success: boolean;
|
|
67
|
+
outcome: data.TExternalRegistryVerifyOutcome;
|
|
62
68
|
message?: string;
|
|
63
|
-
registry
|
|
69
|
+
registry: data.IExternalRegistryPublic;
|
|
64
70
|
};
|
|
65
71
|
}
|
|
@@ -317,7 +317,22 @@ export interface IRequest_Any_Cloudly_UpdateService extends plugins.typedrequest
|
|
|
317
317
|
request: {
|
|
318
318
|
identity: IIdentity;
|
|
319
319
|
serviceId: string;
|
|
320
|
-
|
|
320
|
+
/**
|
|
321
|
+
* The fields to change, merged over the stored service one key deep: a key the update omits
|
|
322
|
+
* keeps its stored value, and a key it states replaces the stored value whole (a stated
|
|
323
|
+
* `ports`, `mail` or `domains` is the complete new value, not a patch of the old one).
|
|
324
|
+
* `containerArgs: null` removes the stored argument vector; no other field takes `null`.
|
|
325
|
+
* `registryTarget` is the exception to "replaces": Cloudly derives it from the service and
|
|
326
|
+
* its image version, so a stated one only makes Cloudly derive it again and is never stored.
|
|
327
|
+
* An optional field cannot be cleared yet: a key whose value is `undefined` is dropped in
|
|
328
|
+
* transit, so it reads as omitted (explicit `null` removal for optional fields is a follow-up).
|
|
329
|
+
*
|
|
330
|
+
* Cloudly's guards ask which keys an update states, so send only the fields that change: a
|
|
331
|
+
* service with a runtime spec refuses any update stating `placement` or `scaleFactor`, and an
|
|
332
|
+
* immutable service refuses `imageId`, `imageVersion`, `registryTarget`, `scaleFactor` and
|
|
333
|
+
* `placement`, even when the stated value repeats the stored one.
|
|
334
|
+
*/
|
|
335
|
+
serviceData: Partial<TServiceUpdateWritableData>;
|
|
321
336
|
};
|
|
322
337
|
response: {
|
|
323
338
|
service: IService;
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -2103,15 +2103,17 @@ evidence has not arrived). `refused` carries a `TServiceRuntimeRefusal`:
|
|
|
2103
2103
|
`node-authority-unsupported`, `network-policy-missing`, `network-policy-mismatch`,
|
|
2104
2104
|
`network-workload-pool-missing`, `network-workload-pool-exhausted`, `volume-driver-unsupported`,
|
|
2105
2105
|
`volume-options-unsupported`, `volume-shared-unsupported`, `volume-placement-unsupported`,
|
|
2106
|
-
`volume-initialization-unknown`, `volume-ownership-unknown`, `
|
|
2107
|
-
`objectstorage-binding-invalid`, `objectstorage-environment-conflict`,
|
|
2106
|
+
`volume-initialization-unknown`, `volume-ownership-unknown`, `launcher-secret-ownership-unknown`,
|
|
2107
|
+
`objectstorage-endpoint-missing`, `objectstorage-binding-invalid`, `objectstorage-environment-conflict`,
|
|
2108
2108
|
`objectstorage-endpoint-unselected`, `database-endpoint-missing`, `database-endpoint-unselected`,
|
|
2109
2109
|
`cluster-relay-missing` or `cluster-ingress-environment-conflict`.
|
|
2110
2110
|
The first four volume
|
|
2111
2111
|
refusals name what a local storage claim cannot express: a volume driver other than a node-local
|
|
2112
2112
|
directory, driver or mount options, one volume mounted by more than one replica or service, and a
|
|
2113
2113
|
placement that does not pin each volume to one node. The other two name what Cloudly will not guess,
|
|
2114
|
-
see [Service Volume Initialization](#service-volume-initialization).
|
|
2114
|
+
see [Service Volume Initialization](#service-volume-initialization).
|
|
2115
|
+
`launcher-secret-ownership-unknown` is a launcher-environment secret file owner Cloudly cannot know,
|
|
2116
|
+
see [Launcher Secret Ownership](#launcher-secret-ownership). Of the four object-storage
|
|
2115
2117
|
refusals, `objectstorage-endpoint-missing`, `objectstorage-binding-invalid` and
|
|
2116
2118
|
`objectstorage-environment-conflict` name why a binding's environment cannot be composed, see
|
|
2117
2119
|
[Object-Storage Binding Environment](#object-storage-binding-environment), and
|
|
@@ -2194,6 +2196,48 @@ Cloudly knows. The rule decides the first generation of a claim only: every late
|
|
|
2194
2196
|
that claim's own initialization and ownership, whatever `runAs` says by then, since the node refuses
|
|
2195
2197
|
a claim whose initial ownership changed.
|
|
2196
2198
|
|
|
2199
|
+
### Launcher Secret Ownership
|
|
2200
|
+
|
|
2201
|
+
WorkloadInit reads a workload's launcher-environment secret files as the workload's own process
|
|
2202
|
+
identity, so their owner comes from that identity and never from a fixed `0:0`.
|
|
2203
|
+
`deriveLauncherSecretDeliveryOwner({ runAs, imageUsers, launcherDeliveries })` is the one rule, and
|
|
2204
|
+
answers `TLauncherSecretDeliveryOwner`: `derived` with `{ uid, gid }`, `not-required` for a workload
|
|
2205
|
+
without launcher deliveries, or `refused` with `reason: 'launcher-secret-ownership-unknown'`,
|
|
2206
|
+
`variableName` (the group-readable delivery that caused it, else `null`) and a `message`.
|
|
2207
|
+
|
|
2208
|
+
`runAs` is the runtime spec's `profile.runAs`. `imageUsers` holds the image user of the pinned
|
|
2209
|
+
digest's image config in Cloudly's canonical form (the registry manager's `parseCanonicalImageUser`
|
|
2210
|
+
answer, `null` for an image without a user or with an empty `User`, never the raw registry value):
|
|
2211
|
+
one entry for a single-platform manifest, one per platform of a multi-platform index.
|
|
2212
|
+
`launcherDeliveries` are the workload's launcher deliveries as their callers state them
|
|
2213
|
+
(`variableName` and `mode`); group readability is computed from them, never passed in.
|
|
2214
|
+
|
|
2215
|
+
| `runAs` | Image user | Owner |
|
|
2216
|
+
| --- | --- | --- |
|
|
2217
|
+
| `{ user, group }` | any | `user:group` |
|
|
2218
|
+
| `null` | none | `0:0`, the runtime config's root default |
|
|
2219
|
+
| `null` | numeric `u:g` (`root` is `0`) | `u:g` |
|
|
2220
|
+
| `null` | numeric `u`, or `u:<name>` | `u:0` while every delivery is `0o400`; refused, naming the first `0o440` delivery by variable name, otherwise |
|
|
2221
|
+
| `null` | a named user other than `root` | refused |
|
|
2222
|
+
| `null` | a numeric part above `2147483647` | refused |
|
|
2223
|
+
|
|
2224
|
+
An owner-readable (`0o400`) file grants its group nothing, so an unstated group is `0`; a `0o440`
|
|
2225
|
+
file would grant group `0` instead of the image's own group, so it refuses. Every platform of a
|
|
2226
|
+
multi-platform index must derive the same owner, or the workload is refused: it gets one delivery,
|
|
2227
|
+
never one per platform. Without launcher deliveries nothing about the image refuses. Only a
|
|
2228
|
+
structurally malformed input throws.
|
|
2229
|
+
|
|
2230
|
+
The owner travels in the resolved manifest, not in what a caller states. `TSecretDelivery`
|
|
2231
|
+
(`ISecretLauncherEnvironmentDelivery | ISecretFileDelivery`) is a caller's delivery: a Secret's
|
|
2232
|
+
`defaultDelivery`, a SecretSet attachment override, a create request, an App Store declaration. Its
|
|
2233
|
+
launcher member's `uid` and `gid` are optional and deprecated: the resolver ignores them, and
|
|
2234
|
+
`validateSecretDelivery` prefers neither and tolerates both (bounded) until the next major removes
|
|
2235
|
+
them. `TSecretResolvedDelivery` (`ISecretResolvedLauncherEnvironmentDelivery | ISecretFileDelivery`)
|
|
2236
|
+
is `IResolvedSecretManifestEntry.delivery`: its launcher member requires `uid` and `gid` under the
|
|
2237
|
+
same keys, so the manifest's wire format and digest are unchanged for every node.
|
|
2238
|
+
`createResolvedSecretDelivery(delivery, owner)` builds it from a caller's delivery and the derived
|
|
2239
|
+
owner, and `validateSecretResolvedDelivery` checks it. File deliveries keep their stated owner.
|
|
2240
|
+
|
|
2197
2241
|
### Runtime Assignment Views
|
|
2198
2242
|
|
|
2199
2243
|
`data.IServiceRuntimeAssignmentView` is one replica slot's current assignment as an administrator
|
|
@@ -2998,6 +3042,15 @@ Common data contracts include:
|
|
|
2998
3042
|
`proposeClusterNodeName`) and the named `clusterNodeHostnameRefusals` of
|
|
2999
3043
|
`setClusterNodeHostname` and `resetClusterNodeHostname`.
|
|
3000
3044
|
- `IService`, `IDeployment`, `IImage`, `IRegistryTarget`, and `IExternalRegistry` for workload delivery.
|
|
3045
|
+
- External registries: `IExternalRegistry` is the stored registry, credential included, and is
|
|
3046
|
+
never answered. Every `requests.externalRegistry` response carries `IExternalRegistryPublic`: the
|
|
3047
|
+
registry without `data.password` and with `passwordConfigured`. `data.password` (a password, access
|
|
3048
|
+
token or API key) is write-only: create stores it; an update (`registryData`, a `Partial` of
|
|
3049
|
+
`TExternalRegistryOperatorData`, never status, errors or timestamps) keeps the stored one when it
|
|
3050
|
+
is missing or empty, and `authType: 'none'` removes it. `verifyExternalRegistry` answers a
|
|
3051
|
+
`TExternalRegistryVerifyOutcome`: only `authenticated` proves the credentials;
|
|
3052
|
+
`credentials-not-exercised` means the registry answered anonymously although credentials are
|
|
3053
|
+
configured.
|
|
3001
3054
|
- Service port contracts including `IServiceTargetPort`, `IServiceDomainRoute`, and `IServicePublicPortMapping` for canonical backend targets, domain target references, and edge/Coretraffic TCP/UDP public exposure.
|
|
3002
3055
|
- `IDomain`, `IDnsEntry`, and traffic contracts for routing and DNS management. `IDomain.data.gatewayListedAt` records when the dcrouter gateway last listed the domain to this Cloudly and is absent while the gateway does not list it; it is separate from the operator's `activationState`.
|
|
3003
3056
|
- Traffic and gateway route contracts including `ICoretrafficPortRouteConfig`, routing `portRoutes`, and `IGatewayClientRoute` client-owned route views. Gateway route intent supports optional match `domains`, `transport`, and `remoteIngress`, plus explicit route `priority` and `managedRouteKind`. Ownership can combine `hostname` with `routeRef` so a normal route and a path-specific managed route for the same hostname reconcile independently.
|
|
@@ -4612,12 +4665,20 @@ to a workload environment and refuses, naming every key, a name that is already
|
|
|
4612
4665
|
service itself or by a second object-storage binding — instead of letting one value replace
|
|
4613
4666
|
another.
|
|
4614
4667
|
|
|
4615
|
-
**Reporting.** `composeObjectStorageServiceEnvironment(serviceEnvironment, sources
|
|
4616
|
-
whole service and answers in the service runtime vocabulary,
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4668
|
+
**Reporting.** `composeObjectStorageServiceEnvironment(serviceEnvironment, sources,
|
|
4669
|
+
secretEnvironmentNames?)` composes a whole service and answers in the service runtime vocabulary,
|
|
4670
|
+
so a producer never classifies the errors above. Each
|
|
4671
|
+
`IObjectStorageBindingEnvironmentSource { binding, endpoint }` is an enabled object-storage binding
|
|
4672
|
+
and the `corestoreObjectStorage` of the node it is placed on, `null` when that node states none; a
|
|
4673
|
+
binding placed on no node is not a source, and its service waits as `bindings-pending`.
|
|
4674
|
+
`secretEnvironmentNames` names, without values, the variables the workload receives as
|
|
4675
|
+
launcher-environment secrets (the `variableName` of each `launcher-environment` delivery in its
|
|
4676
|
+
desired secret manifest; none when omitted). The launcher sets a secret over a plain variable of
|
|
4677
|
+
the same name, so a secret named like a binding's name would silently replace the binding's
|
|
4678
|
+
endpoint: it claims the name exactly as the service's own environment does. Secrets by any other
|
|
4679
|
+
name, including the binding's own credentials (`corestoreObjectStorageCredentialSecretKeys`, never
|
|
4680
|
+
plain names), do not concern the composition. `secretEnvironmentNames` becomes required in the
|
|
4681
|
+
coordinated major. The answer is a `TObjectStorageServiceEnvironmentComposition`:
|
|
4621
4682
|
|
|
4622
4683
|
| `status` | `reason` | When |
|
|
4623
4684
|
| --- | --- | --- |
|
|
@@ -4625,7 +4686,7 @@ that node states none; a binding placed on no node is not a source, and its serv
|
|
|
4625
4686
|
| `waiting` | `objectstorage-retention-pending` | A binding declares compliance retention whose evidence has not arrived. |
|
|
4626
4687
|
| `refused` | `objectstorage-endpoint-missing` | The binding's node states no valid `corestoreObjectStorage`. |
|
|
4627
4688
|
| `refused` | `objectstorage-binding-invalid` | `createObjectStorageBindingEnvironment()` refuses the binding: not `objectstorage`, no canonical bucket, no canonical retention intent, or evidence that does not prove this binding. |
|
|
4628
|
-
| `refused` | `objectstorage-environment-conflict` | The service's own environment or another binding already sets one of the binding's names. |
|
|
4689
|
+
| `refused` | `objectstorage-environment-conflict` | The service's own environment, one of its launcher-environment secrets or another binding already sets one of the binding's names. |
|
|
4629
4690
|
|
|
4630
4691
|
`waiting` and `refused` carry the `bindingId` and the contract's `message`. Bindings are taken in
|
|
4631
4692
|
id order, so the verdict does not depend on the order of the sources, and a refusal outranks a
|
|
@@ -4682,7 +4743,10 @@ object-storage endpoint. What they are addressed to is a fact about the node, ow
|
|
|
4682
4743
|
### Launcher Environment Delivery
|
|
4683
4744
|
|
|
4684
4745
|
Launcher environment delivery uses stable `szsv-<base32-sha256>` Docker
|
|
4685
|
-
resource names and `/run/serve.zone/secrets/<resource>` source paths.
|
|
4746
|
+
resource names and `/run/serve.zone/secrets/<resource>` source paths. Each
|
|
4747
|
+
source file is owned by the workload identity the resolved manifest states,
|
|
4748
|
+
derived by `deriveLauncherSecretDeliveryOwner` (see
|
|
4749
|
+
[Launcher Secret Ownership](#launcher-secret-ownership)). The
|
|
4686
4750
|
nonsecret map is written to `/run/serve.zone/workloadinit-map.json` with mode
|
|
4687
4751
|
0444. Runtime assets under `/opt/serve.zone/runtime-assets` are read-only, the
|
|
4688
4752
|
wrapper itself lives at
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import * as plugins from '../plugins.js';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* An external registry as Cloudly stores it, credential included. Never answered: every external
|
|
5
|
+
* registry response carries `IExternalRegistryPublic`.
|
|
6
|
+
*/
|
|
3
7
|
export interface IExternalRegistry {
|
|
4
8
|
id: string;
|
|
5
9
|
data: {
|
|
@@ -24,7 +28,10 @@ export interface IExternalRegistry {
|
|
|
24
28
|
username?: string;
|
|
25
29
|
|
|
26
30
|
/**
|
|
27
|
-
* Password, access token
|
|
31
|
+
* Password, access token or API key for authentication (optional for public registries).
|
|
32
|
+
*
|
|
33
|
+
* Write-only: Cloudly stores it and uses it server-side, and never answers it. Reads carry
|
|
34
|
+
* `IExternalRegistryPublic.passwordConfigured` instead.
|
|
28
35
|
*/
|
|
29
36
|
password?: string;
|
|
30
37
|
|
|
@@ -107,4 +114,55 @@ export interface IExternalRegistry {
|
|
|
107
114
|
*/
|
|
108
115
|
updatedAt?: number;
|
|
109
116
|
};
|
|
110
|
-
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* An external registry as Cloudly answers it: the stored registry without its credential, and
|
|
121
|
+
* whether one is stored.
|
|
122
|
+
*/
|
|
123
|
+
export interface IExternalRegistryPublic {
|
|
124
|
+
id: string;
|
|
125
|
+
data: Omit<IExternalRegistry['data'], 'password'>;
|
|
126
|
+
/** Whether a password, access token or API key is stored for the registry. */
|
|
127
|
+
passwordConfigured: boolean;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** The registry fields an operator writes; Cloudly owns status, errors and timestamps. */
|
|
131
|
+
export type TExternalRegistryOperatorField =
|
|
132
|
+
| 'type'
|
|
133
|
+
| 'name'
|
|
134
|
+
| 'url'
|
|
135
|
+
| 'username'
|
|
136
|
+
| 'password'
|
|
137
|
+
| 'description'
|
|
138
|
+
| 'isDefault'
|
|
139
|
+
| 'authType'
|
|
140
|
+
| 'insecure'
|
|
141
|
+
| 'namespace'
|
|
142
|
+
| 'proxy'
|
|
143
|
+
| 'config';
|
|
144
|
+
|
|
145
|
+
export type TExternalRegistryOperatorData = Pick<IExternalRegistry['data'], TExternalRegistryOperatorField>;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* What a registry verification proved.
|
|
149
|
+
*
|
|
150
|
+
* - `authenticated`: the registry admitted the stored credentials (Basic, or a token its Bearer
|
|
151
|
+
* realm issued for them).
|
|
152
|
+
* - `anonymous`: the registry answers without authentication and none is configured.
|
|
153
|
+
* - `credentials-not-exercised`: credentials are configured, but the registry answers without
|
|
154
|
+
* authentication, so they were never checked.
|
|
155
|
+
* - `authentication-failed`: the registry, or its token realm, refused the credentials, or it
|
|
156
|
+
* requires authentication and none is configured.
|
|
157
|
+
* - `unreachable`: no usable answer (connection, TLS, timeout or an unexpected HTTP status).
|
|
158
|
+
* - `unsupported`: the registry or its configuration cannot be verified (an npm registry, OAuth2,
|
|
159
|
+
* an unknown or missing authentication challenge, a redirect, or credentials over plain HTTP
|
|
160
|
+
* without `insecure`).
|
|
161
|
+
*/
|
|
162
|
+
export type TExternalRegistryVerifyOutcome =
|
|
163
|
+
| 'authenticated'
|
|
164
|
+
| 'anonymous'
|
|
165
|
+
| 'credentials-not-exercised'
|
|
166
|
+
| 'authentication-failed'
|
|
167
|
+
| 'unreachable'
|
|
168
|
+
| 'unsupported';
|
package/ts/data/index.ts
CHANGED
|
@@ -82,6 +82,7 @@ export * from './servicecredential.js';
|
|
|
82
82
|
export * from './serviceplatformownership.js';
|
|
83
83
|
export * from './serviceports.js';
|
|
84
84
|
export * from './serviceruntime.js';
|
|
85
|
+
export * from './launchersecretowner.js';
|
|
85
86
|
export * from './servicevolumeinitialization.js';
|
|
86
87
|
export * from './status.js';
|
|
87
88
|
export * from './taskexecution.js';
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { ISecretLauncherEnvironmentDelivery } from './secret.js';
|
|
2
|
+
import type { IServiceRuntimeProfile, TServiceRuntimeRefusal } from './serviceruntime.js';
|
|
3
|
+
import { hasExactKeys, isPlainRecord } from '../private/dataproperties.js';
|
|
4
|
+
import { isRuntimeIdentityNumber, runtimePolicyLimits } from '../private/runtimepolicy.js';
|
|
5
|
+
|
|
6
|
+
/** What the owner of a workload's launcher-environment secret files derives from. */
|
|
7
|
+
export interface ILauncherSecretDeliveryOwnerInput {
|
|
8
|
+
/** The runtime spec's `profile.runAs`; null runs the image's own OCI user. */
|
|
9
|
+
runAs: IServiceRuntimeProfile['runAs'];
|
|
10
|
+
/**
|
|
11
|
+
* The unresolved OCI `config.User` of the pinned digest's image config in Cloudly's canonical
|
|
12
|
+
* form (what its registry manager's `parseCanonicalImageUser` answers, never the raw registry
|
|
13
|
+
* value): `user` or `user:group`, each part a numeric identity or a name, and null for an image
|
|
14
|
+
* that states none or an empty `User`. One entry for a single-platform manifest, one per
|
|
15
|
+
* platform of a multi-platform index; never empty.
|
|
16
|
+
*/
|
|
17
|
+
imageUsers: (string | null)[];
|
|
18
|
+
/**
|
|
19
|
+
* Every launcher-environment delivery the workload receives, as its caller states it. A `0o440`
|
|
20
|
+
* delivery grants the derived group, so that group has to be one the image states numerically.
|
|
21
|
+
*/
|
|
22
|
+
launcherDeliveries: Pick<ISecretLauncherEnvironmentDelivery, 'variableName' | 'mode'>[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The numeric owner of a workload's launcher-environment secret files, or why it derives none.
|
|
27
|
+
*
|
|
28
|
+
* `not-required` is a workload without launcher-environment deliveries: no file needs an owner, so
|
|
29
|
+
* nothing about the image refuses it.
|
|
30
|
+
*
|
|
31
|
+
* `launcher-secret-ownership-unknown` is a runtime spec that runs the image's own user
|
|
32
|
+
* (`runAs: null`) while that user's numeric owner is not known: a named user other than `root`, a
|
|
33
|
+
* numeric part above the runtime identity bound, a group-readable (`0o440`) delivery whose group
|
|
34
|
+
* the image does not state numerically, or platforms of a multi-platform index that derive
|
|
35
|
+
* different owners. `variableName` names the group-readable delivery that caused it (the first by
|
|
36
|
+
* name), null for the others; `message` states the cause.
|
|
37
|
+
*/
|
|
38
|
+
export type TLauncherSecretDeliveryOwner =
|
|
39
|
+
| { status: 'derived'; uid: number; gid: number }
|
|
40
|
+
| { status: 'not-required' }
|
|
41
|
+
| {
|
|
42
|
+
status: 'refused';
|
|
43
|
+
reason: Extract<TServiceRuntimeRefusal, 'launcher-secret-ownership-unknown'>;
|
|
44
|
+
variableName: string | null;
|
|
45
|
+
message: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const imageUserPartPattern = /^[^:\u0000-\u001f\u007f]+$/;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* One part of an image user: a numeric identity (`root` is `0`), a numeric part above the runtime
|
|
52
|
+
* identity bound, or a name only the image's root filesystem resolves.
|
|
53
|
+
*/
|
|
54
|
+
type TImageUserPart = number | 'unbounded' | 'named';
|
|
55
|
+
|
|
56
|
+
const imageUserPart = (partArg: string): TImageUserPart => {
|
|
57
|
+
if (partArg === 'root') return 0;
|
|
58
|
+
if (!/^[0-9]+$/.test(partArg)) return 'named';
|
|
59
|
+
const identity = Number(partArg);
|
|
60
|
+
return isRuntimeIdentityNumber(identity) ? identity : 'unbounded';
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/** One platform's user and group; an absent group is `undefined`. A malformed image user throws. */
|
|
64
|
+
const imageUserParts = (imageUserArg: unknown): { user: TImageUserPart; group: TImageUserPart | undefined } => {
|
|
65
|
+
if (imageUserArg === null) return { user: 0, group: 0 };
|
|
66
|
+
const parts = typeof imageUserArg === 'string' ? imageUserArg.split(':') : [];
|
|
67
|
+
if (parts.length === 0 || parts.length > 2 || !parts.every((partArg) => imageUserPartPattern.test(partArg))) {
|
|
68
|
+
throw new Error('image user must be null, user or user:group');
|
|
69
|
+
}
|
|
70
|
+
return { user: imageUserPart(parts[0]), group: parts[1] === undefined ? undefined : imageUserPart(parts[1]) };
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/** The names of the group-readable (`0o440`) deliveries, sorted; a malformed delivery throws. */
|
|
74
|
+
const groupReadableVariableNames = (deliveriesArg: unknown): string[] => {
|
|
75
|
+
if (!Array.isArray(deliveriesArg)) throw new Error('launcherDeliveries must list the launcher deliveries');
|
|
76
|
+
const names: string[] = [];
|
|
77
|
+
for (const delivery of deliveriesArg) {
|
|
78
|
+
if (!isPlainRecord(delivery) || typeof delivery.variableName !== 'string' || delivery.variableName === ''
|
|
79
|
+
|| (delivery.mode !== 0o400 && delivery.mode !== 0o440)) {
|
|
80
|
+
throw new Error('a launcher delivery must state its variableName and a 0400 or 0440 mode');
|
|
81
|
+
}
|
|
82
|
+
if (delivery.mode === 0o440) names.push(delivery.variableName);
|
|
83
|
+
}
|
|
84
|
+
return names.sort();
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const refusal = (variableNameArg: string | null, messageArg: string): TLauncherSecretDeliveryOwner => ({
|
|
88
|
+
status: 'refused', reason: 'launcher-secret-ownership-unknown', variableName: variableNameArg, message: messageArg,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The owner Cloudly gives every launcher-environment secret file of a workload, which WorkloadInit
|
|
93
|
+
* reads as the workload's own process identity before it starts the image.
|
|
94
|
+
*
|
|
95
|
+
* Without launcher deliveries it is `not-required`. A `runAs` pair wins over the image. Under
|
|
96
|
+
* `runAs: null` an image without a user runs as root (`0:0`, the runtime config's own default), a
|
|
97
|
+
* numeric `user:group` is taken as stated and `root` stands for `0` in either part. A numeric user
|
|
98
|
+
* without a group, or with a named group, is `user:0` while every delivery is owner-readable
|
|
99
|
+
* (`0o400`), which grants the group nothing; a `0o440` delivery would grant it, so it refuses and
|
|
100
|
+
* names that delivery. A named user, or a numeric part above the runtime identity bound, refuses.
|
|
101
|
+
* Every platform of a multi-platform index must derive
|
|
102
|
+
* the same owner, or it refuses: a workload gets one delivery, never one per platform. Nothing is
|
|
103
|
+
* guessed. A structurally malformed input throws, whatever it would answer.
|
|
104
|
+
*/
|
|
105
|
+
export const deriveLauncherSecretDeliveryOwner = (
|
|
106
|
+
inputArg: ILauncherSecretDeliveryOwnerInput,
|
|
107
|
+
): TLauncherSecretDeliveryOwner => {
|
|
108
|
+
const { runAs, imageUsers, launcherDeliveries } = inputArg;
|
|
109
|
+
if (runAs !== null && !(isPlainRecord(runAs) && hasExactKeys(runAs, ['user', 'group'])
|
|
110
|
+
&& isRuntimeIdentityNumber(runAs.user) && isRuntimeIdentityNumber(runAs.group))) {
|
|
111
|
+
throw new Error('runAs must be null or an exact user and group');
|
|
112
|
+
}
|
|
113
|
+
if (!Array.isArray(imageUsers) || imageUsers.length === 0) {
|
|
114
|
+
throw new Error('imageUsers must name the user of every platform');
|
|
115
|
+
}
|
|
116
|
+
const platforms = imageUsers.map(imageUserParts);
|
|
117
|
+
const groupReadable = groupReadableVariableNames(launcherDeliveries);
|
|
118
|
+
if (launcherDeliveries.length === 0) return { status: 'not-required' };
|
|
119
|
+
if (runAs !== null) return { status: 'derived', uid: runAs.user, gid: runAs.group };
|
|
120
|
+
const userOf = (predicateArg: (platformArg: typeof platforms[number]) => boolean): string | null | undefined => (
|
|
121
|
+
imageUsers.find((_imageUserArg, indexArg) => predicateArg(platforms[indexArg])));
|
|
122
|
+
const unbounded = userOf((platformArg) => platformArg.user === 'unbounded' || platformArg.group === 'unbounded');
|
|
123
|
+
if (unbounded !== undefined) {
|
|
124
|
+
return refusal(null, `image user '${unbounded}' states an identity above `
|
|
125
|
+
+ `${runtimePolicyLimits.maximumIdentityNumber}`);
|
|
126
|
+
}
|
|
127
|
+
const namedUser = userOf((platformArg) => platformArg.user === 'named');
|
|
128
|
+
if (namedUser !== undefined) {
|
|
129
|
+
return refusal(null, `image user '${namedUser}' names a user only the image's root filesystem resolves`);
|
|
130
|
+
}
|
|
131
|
+
const ungrouped = userOf((platformArg) => typeof platformArg.group !== 'number');
|
|
132
|
+
if (ungrouped !== undefined && groupReadable.length > 0) {
|
|
133
|
+
return refusal(groupReadable[0], `launcher-environment secret ${groupReadable.join(', ')} is group-readable `
|
|
134
|
+
+ `(0440), but image user '${ungrouped}' states no numeric group`);
|
|
135
|
+
}
|
|
136
|
+
// Every part is now a bounded number, or a group that an owner-readable file does not grant.
|
|
137
|
+
const [first, ...rest] = platforms.map((platformArg) => ({
|
|
138
|
+
uid: platformArg.user as number, gid: typeof platformArg.group === 'number' ? platformArg.group : 0,
|
|
139
|
+
}));
|
|
140
|
+
if (rest.some((ownerArg) => ownerArg.uid !== first.uid || ownerArg.gid !== first.gid)) {
|
|
141
|
+
const users = imageUsers.map((userArg) => userArg === null ? 'none' : `'${userArg}'`).join(', ');
|
|
142
|
+
return refusal(null, `the platforms' image users ${users} derive different owners`);
|
|
143
|
+
}
|
|
144
|
+
return { status: 'derived', uid: first.uid, gid: first.gid };
|
|
145
|
+
};
|
package/ts/data/secret.ts
CHANGED
|
@@ -228,7 +228,33 @@ export interface ISecretSetConsumerRolloutStatus {
|
|
|
228
228
|
updatedAt: number;
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
+
/**
|
|
232
|
+
* A launcher-environment delivery as a caller states it: on a Secret's `defaultDelivery`, a
|
|
233
|
+
* SecretSet attachment override, a create request or an App Store declaration. The owner of the
|
|
234
|
+
* file WorkloadInit reads is not the caller's to state; the resolver derives it.
|
|
235
|
+
*/
|
|
231
236
|
export interface ISecretLauncherEnvironmentDelivery {
|
|
237
|
+
type: 'launcher-environment';
|
|
238
|
+
variableName: string;
|
|
239
|
+
/**
|
|
240
|
+
* @deprecated Derived by the resolver from the workload identity
|
|
241
|
+
* (`deriveLauncherSecretDeliveryOwner`); ignored; removed in the next major.
|
|
242
|
+
*/
|
|
243
|
+
uid?: number;
|
|
244
|
+
/**
|
|
245
|
+
* @deprecated Derived by the resolver from the workload identity
|
|
246
|
+
* (`deriveLauncherSecretDeliveryOwner`); ignored; removed in the next major.
|
|
247
|
+
*/
|
|
248
|
+
gid?: number;
|
|
249
|
+
mode: 0o400 | 0o440;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* A launcher-environment delivery of a resolved secret manifest. `uid` and `gid` are the owner the
|
|
254
|
+
* resolver derived from the workload identity (`deriveLauncherSecretDeliveryOwner`), written by
|
|
255
|
+
* `createResolvedSecretDelivery`; the node mounts the file with them.
|
|
256
|
+
*/
|
|
257
|
+
export interface ISecretResolvedLauncherEnvironmentDelivery {
|
|
232
258
|
type: 'launcher-environment';
|
|
233
259
|
variableName: string;
|
|
234
260
|
uid: number;
|
|
@@ -244,8 +270,12 @@ export interface ISecretFileDelivery {
|
|
|
244
270
|
mode: 0o400 | 0o440;
|
|
245
271
|
}
|
|
246
272
|
|
|
273
|
+
/** A delivery as a caller states it. */
|
|
247
274
|
export type TSecretDelivery = ISecretLauncherEnvironmentDelivery | ISecretFileDelivery;
|
|
248
275
|
|
|
276
|
+
/** A delivery of a resolved secret manifest: every owner is stated. */
|
|
277
|
+
export type TSecretResolvedDelivery = ISecretResolvedLauncherEnvironmentDelivery | ISecretFileDelivery;
|
|
278
|
+
|
|
249
279
|
export const secretValueLimits = Object.freeze({
|
|
250
280
|
maximumBytes: 500 * 1024,
|
|
251
281
|
minimumGeneratedBytes: 16,
|
|
@@ -299,7 +329,7 @@ export interface IResolvedSecretManifestEntry {
|
|
|
299
329
|
key: string;
|
|
300
330
|
environment: TSecretEnvironment;
|
|
301
331
|
owner: TWorkloadSecretOwner;
|
|
302
|
-
delivery:
|
|
332
|
+
delivery: TSecretResolvedDelivery;
|
|
303
333
|
}
|
|
304
334
|
|
|
305
335
|
/** Immutable, value-free runtime snapshot. */
|
|
@@ -709,14 +739,16 @@ export const validateSecretManagementScope = (
|
|
|
709
739
|
: ['management scope prefix must match management source'];
|
|
710
740
|
};
|
|
711
741
|
|
|
712
|
-
|
|
742
|
+
const validateDelivery = (deliveryArg: unknown, launcherOwnerArg: 'tolerated' | 'required'): string[] => {
|
|
713
743
|
if (!isRecord(deliveryArg)) return ['delivery must be an object'];
|
|
714
744
|
const isLauncher = deliveryArg.type === 'launcher-environment';
|
|
715
745
|
if (!isLauncher && deliveryArg.type !== 'file') {
|
|
716
746
|
return ['delivery type must be launcher-environment or file'];
|
|
717
747
|
}
|
|
748
|
+
const statesOwner = !isLauncher || launcherOwnerArg === 'required'
|
|
749
|
+
|| deliveryArg.uid !== undefined || deliveryArg.gid !== undefined;
|
|
718
750
|
const expectedKeys = isLauncher
|
|
719
|
-
? ['type', 'variableName', 'uid', 'gid', 'mode']
|
|
751
|
+
? ['type', 'variableName', ...(statesOwner ? ['uid', 'gid'] : []), 'mode']
|
|
720
752
|
: ['type', 'targetPath', 'uid', 'gid', 'mode'];
|
|
721
753
|
if (!hasExactKeys(deliveryArg, expectedKeys)) {
|
|
722
754
|
return ['delivery must use its exact schema'];
|
|
@@ -730,7 +762,7 @@ export const validateSecretDelivery = (deliveryArg: unknown): string[] => {
|
|
|
730
762
|
|| !/^\/run\/secrets\/[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(deliveryArg.targetPath))) {
|
|
731
763
|
errors.push('file delivery targetPath must be a canonical file below /run/secrets');
|
|
732
764
|
}
|
|
733
|
-
for (const field of ['uid', 'gid'] as const) {
|
|
765
|
+
for (const field of statesOwner ? ['uid', 'gid'] as const : []) {
|
|
734
766
|
const value = deliveryArg[field];
|
|
735
767
|
if (!Number.isSafeInteger(value)
|
|
736
768
|
|| (value as number) < 0
|
|
@@ -744,6 +776,30 @@ export const validateSecretDelivery = (deliveryArg: unknown): string[] => {
|
|
|
744
776
|
return errors;
|
|
745
777
|
};
|
|
746
778
|
|
|
779
|
+
/**
|
|
780
|
+
* Validates a delivery as a caller states it. A launcher delivery should state no owner; one that
|
|
781
|
+
* states the deprecated `uid` and `gid` (both, bounded) is tolerated until the next major.
|
|
782
|
+
*/
|
|
783
|
+
export const validateSecretDelivery = (deliveryArg: unknown): string[] => validateDelivery(deliveryArg, 'tolerated');
|
|
784
|
+
|
|
785
|
+
/** Validates a resolved manifest's delivery: a launcher delivery states its derived owner. */
|
|
786
|
+
export const validateSecretResolvedDelivery = (deliveryArg: unknown): string[] => validateDelivery(
|
|
787
|
+
deliveryArg, 'required');
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* The resolved manifest delivery of a caller's delivery: a launcher delivery takes the owner the
|
|
791
|
+
* resolver derived (`deriveLauncherSecretDeliveryOwner`) and drops any deprecated owner the caller
|
|
792
|
+
* stated; a file delivery keeps its own.
|
|
793
|
+
*/
|
|
794
|
+
export const createResolvedSecretDelivery = (
|
|
795
|
+
deliveryArg: TSecretDelivery,
|
|
796
|
+
ownerArg: { uid: number; gid: number },
|
|
797
|
+
): TSecretResolvedDelivery => (deliveryArg.type === 'launcher-environment'
|
|
798
|
+
? { type: 'launcher-environment', variableName: deliveryArg.variableName, uid: ownerArg.uid, gid: ownerArg.gid,
|
|
799
|
+
mode: deliveryArg.mode }
|
|
800
|
+
: { type: 'file', targetPath: deliveryArg.targetPath, uid: deliveryArg.uid, gid: deliveryArg.gid,
|
|
801
|
+
mode: deliveryArg.mode });
|
|
802
|
+
|
|
747
803
|
export const validateSecretMetadata = (secretArg: unknown): string[] => {
|
|
748
804
|
if (!isRecord(secretArg)) return ['secret metadata must be an object'];
|
|
749
805
|
const expectedKeys = [
|
|
@@ -1269,7 +1325,7 @@ export const validateResolvedSecretManifest = (
|
|
|
1269
1325
|
`secret manifest entries[${index}].owner serviceId must match the manifest serviceId`,
|
|
1270
1326
|
);
|
|
1271
1327
|
}
|
|
1272
|
-
errors.push(...
|
|
1328
|
+
errors.push(...validateSecretResolvedDelivery(candidate.delivery).map(
|
|
1273
1329
|
(errorArg) => `secret manifest entries[${index}].${errorArg}`,
|
|
1274
1330
|
));
|
|
1275
1331
|
if (isRecord(candidate.delivery)) {
|
|
@@ -1281,7 +1337,7 @@ export const validateResolvedSecretManifest = (
|
|
|
1281
1337
|
} else if (
|
|
1282
1338
|
candidate.delivery.type === 'file'
|
|
1283
1339
|
&& typeof candidate.delivery.targetPath === 'string'
|
|
1284
|
-
&&
|
|
1340
|
+
&& validateSecretResolvedDelivery(candidate.delivery).length === 0
|
|
1285
1341
|
) {
|
|
1286
1342
|
fileDeliveryTargets.push(candidate.delivery.targetPath);
|
|
1287
1343
|
}
|
|
@@ -1584,7 +1640,7 @@ export const createWorkloadInitEnvironmentMap = async (
|
|
|
1584
1640
|
.map(async (entryArg) => {
|
|
1585
1641
|
const resource = await createDockerSecretResourceName(entryArg.secretVersionId);
|
|
1586
1642
|
return {
|
|
1587
|
-
variableName: (entryArg.delivery as
|
|
1643
|
+
variableName: (entryArg.delivery as ISecretResolvedLauncherEnvironmentDelivery).variableName,
|
|
1588
1644
|
resource,
|
|
1589
1645
|
source: createLauncherSecretSourcePath(resource),
|
|
1590
1646
|
};
|
|
@@ -1818,6 +1874,8 @@ const canonicalSecretOwner = (ownerArg: TSecretOwner): Record<string, string> =>
|
|
|
1818
1874
|
}
|
|
1819
1875
|
};
|
|
1820
1876
|
|
|
1877
|
+
// A launcher delivery that still states the deprecated owner keeps the context it was sealed
|
|
1878
|
+
// under; JSON leaves an absent owner out.
|
|
1821
1879
|
const canonicalSecretDelivery = (deliveryArg: TSecretDelivery): Record<string, unknown> => (
|
|
1822
1880
|
deliveryArg.type === 'launcher-environment'
|
|
1823
1881
|
? {
|
|
@@ -89,6 +89,7 @@ const serviceRuntimeRefusals = [
|
|
|
89
89
|
'network-workload-pool-missing', 'network-workload-pool-exhausted',
|
|
90
90
|
'volume-driver-unsupported', 'volume-options-unsupported', 'volume-shared-unsupported',
|
|
91
91
|
'volume-placement-unsupported', 'volume-initialization-unknown', 'volume-ownership-unknown',
|
|
92
|
+
'launcher-secret-ownership-unknown',
|
|
92
93
|
'objectstorage-endpoint-missing', 'objectstorage-binding-invalid', 'objectstorage-environment-conflict',
|
|
93
94
|
'objectstorage-endpoint-unselected', 'database-endpoint-missing', 'database-endpoint-unselected',
|
|
94
95
|
'published-port-reserved', 'published-port-symmetric-without-egress', 'readiness-network-unsupported',
|
|
@@ -128,11 +129,18 @@ export type TServiceRuntimeWaitingReason = typeof serviceRuntimeWaitingReasons[n
|
|
|
128
129
|
* `volume-ownership-unknown` a `run-as` ownership under `runAs: null`
|
|
129
130
|
* (`deriveServiceVolumeClaimInitialization`); neither is defaulted.
|
|
130
131
|
*
|
|
132
|
+
* `launcher-secret-ownership-unknown` is a service with launcher-environment secrets whose runtime
|
|
133
|
+
* spec runs the image's own user (`runAs: null`) while that user's numeric owner is not known: a
|
|
134
|
+
* named non-root user, a numeric part above the identity bound, a group a `0o440` delivery would grant that the image does not state
|
|
135
|
+
* numerically, or platforms of the pinned index that derive different owners
|
|
136
|
+
* (`deriveLauncherSecretDeliveryOwner`). WorkloadInit could not read secret files owned by a guess.
|
|
137
|
+
*
|
|
131
138
|
* The object-storage refusals name why a binding's environment cannot be composed
|
|
132
139
|
* (`composeObjectStorageServiceEnvironment`): `objectstorage-endpoint-missing` a binding whose node
|
|
133
140
|
* states no valid `corestoreObjectStorage`, `objectstorage-binding-invalid` a binding
|
|
134
141
|
* `createObjectStorageBindingEnvironment` refuses, and `objectstorage-environment-conflict` a name
|
|
135
|
-
* the service's own environment or another binding already
|
|
142
|
+
* the service's own environment, one of its launcher-environment secrets or another binding already
|
|
143
|
+
* sets.
|
|
136
144
|
*
|
|
137
145
|
* `database-endpoint-missing` is an enabled database binding placed on a node that states no valid
|
|
138
146
|
* `corestoreDatabase`: its credential cannot be addressed to any host a workload reaches, so it is
|