@serve.zone/interfaces 32.16.0 → 32.17.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 +32 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/data/cluster.d.ts +50 -7
- package/dist_ts/data/cluster.js +72 -11
- package/dist_ts/data/clusternode.d.ts +3 -0
- package/dist_ts/data/clusternode.js +7 -1
- package/dist_ts/data/clusterrelaycredential.d.ts +141 -0
- package/dist_ts/data/clusterrelaycredential.js +192 -0
- package/dist_ts/data/index.d.ts +2 -0
- package/dist_ts/data/index.js +3 -1
- package/dist_ts/data/sparklegacyretirement.d.ts +137 -0
- package/dist_ts/data/sparklegacyretirement.js +206 -0
- package/dist_ts/requests/cluster.d.ts +54 -0
- package/dist_ts/requests/cluster.js +1 -1
- package/package.json +1 -1
- package/readme.md +138 -6
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/data/cluster.ts +147 -17
- package/ts/data/clusternode.ts +13 -0
- package/ts/data/clusterrelaycredential.ts +361 -0
- package/ts/data/index.ts +2 -0
- package/ts/data/sparklegacyretirement.ts +352 -0
- package/ts/requests/cluster.ts +69 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { isReleaseVersion } from '../private/releaseversion.js';
|
|
2
|
+
import { captureNetwork, networkCounter, networkDigest, networkEqual, networkHash, networkId, networkNodeId, networkRecord, networkReference, networkRevision, requireNetwork, } from '../private/networkcontract.js';
|
|
3
|
+
/** The two runtimes a released Spark could be put into by its `mode` configuration key. */
|
|
4
|
+
export const sparkLegacyRuntimeModes = ['cloudly', 'coreflow-node'];
|
|
5
|
+
/**
|
|
6
|
+
* Which Docker services a released mode owns, sorted, so a refusal names a mode instead of
|
|
7
|
+
* restating a set. A record over the mode union: a mode cannot be added without naming its
|
|
8
|
+
* services, and a reader can never meet a mode whose services it does not know.
|
|
9
|
+
*/
|
|
10
|
+
export const sparkLegacyRuntimeModeServices = Object.freeze({
|
|
11
|
+
'cloudly': Object.freeze(['cloudly']),
|
|
12
|
+
'coreflow-node': Object.freeze(['coreflow', 'corestore', 'coretraffic']),
|
|
13
|
+
});
|
|
14
|
+
export const sparkLegacyRetirementContract = Object.freeze({
|
|
15
|
+
modeRefusalDigestDomain: 'serve.zone/spark-legacy-mode-refusal',
|
|
16
|
+
configurationRetirementDigestDomain: 'serve.zone/spark-legacy-configuration-retirement',
|
|
17
|
+
});
|
|
18
|
+
/** Every reason Cloudly answers instead of advancing a node's legacy retirement state. */
|
|
19
|
+
export const sparkLegacyRetirementRefusals = Object.freeze([
|
|
20
|
+
/** The observed owner generation is below the accepted one. */
|
|
21
|
+
'legacy-retirement-generation-stale',
|
|
22
|
+
/** The observed owner generation equals the accepted one and states something else. */
|
|
23
|
+
'legacy-retirement-generation-conflict',
|
|
24
|
+
/** A node imported one source once; another source digest describes another host. */
|
|
25
|
+
'legacy-retirement-source-changed',
|
|
26
|
+
/** A removed source cannot become a retained one. */
|
|
27
|
+
'legacy-retirement-disposition-reversed',
|
|
28
|
+
/** The stated bundle is below the release whose refusal this server accepts as real. */
|
|
29
|
+
'legacy-retirement-bundle-below-minimum',
|
|
30
|
+
]);
|
|
31
|
+
const fail = () => { throw new Error('Spark legacy retirement contract rejected.'); };
|
|
32
|
+
const safe = (operationArg) => {
|
|
33
|
+
try {
|
|
34
|
+
return operationArg();
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return fail();
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const sha256Hex = (valueArg) => {
|
|
41
|
+
requireNetwork(typeof valueArg === 'string' && /^[a-f0-9]{64}$/.test(valueArg));
|
|
42
|
+
return valueArg;
|
|
43
|
+
};
|
|
44
|
+
const sourceCommit = (valueArg) => {
|
|
45
|
+
requireNetwork(typeof valueArg === 'string' && /^[a-f0-9]{40}$/.test(valueArg));
|
|
46
|
+
return valueArg;
|
|
47
|
+
};
|
|
48
|
+
const releaseVersion = (valueArg) => {
|
|
49
|
+
requireNetwork(isReleaseVersion(valueArg));
|
|
50
|
+
return valueArg;
|
|
51
|
+
};
|
|
52
|
+
const legacyRuntimeMode = (valueArg) => {
|
|
53
|
+
requireNetwork(sparkLegacyRuntimeModes.includes(valueArg));
|
|
54
|
+
return valueArg;
|
|
55
|
+
};
|
|
56
|
+
const configurationDisposition = (valueArg) => {
|
|
57
|
+
requireNetwork(valueArg === 'retained-inert' || valueArg === 'removed');
|
|
58
|
+
return valueArg;
|
|
59
|
+
};
|
|
60
|
+
/** Structural copy only; whether the tree it describes is installed is the node's to observe. */
|
|
61
|
+
export const snapshotSparkNodeBundleIdentity = (valueArg) => safe(() => {
|
|
62
|
+
const value = networkRecord(captureNetwork(valueArg), [
|
|
63
|
+
'version', 'sourceCommit', 'manifestSha256', 'unitName', 'unitDigest',
|
|
64
|
+
]);
|
|
65
|
+
return {
|
|
66
|
+
version: releaseVersion(value.version),
|
|
67
|
+
sourceCommit: sourceCommit(value.sourceCommit),
|
|
68
|
+
manifestSha256: sha256Hex(value.manifestSha256),
|
|
69
|
+
unitName: networkId(value.unitName),
|
|
70
|
+
unitDigest: networkDigest(value.unitDigest),
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
export const snapshotSparkLegacyConfigurationState = (valueArg) => safe(() => {
|
|
74
|
+
const value = networkRecord(captureNetwork(valueArg), [
|
|
75
|
+
'ownerGeneration', 'sourceSha256', 'disposition',
|
|
76
|
+
]);
|
|
77
|
+
return {
|
|
78
|
+
ownerGeneration: networkCounter(value.ownerGeneration),
|
|
79
|
+
sourceSha256: sha256Hex(value.sourceSha256),
|
|
80
|
+
disposition: configurationDisposition(value.disposition),
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
export const snapshotSparkLegacyRuntimeRetirement = (valueArg) => safe(() => {
|
|
84
|
+
const value = networkRecord(captureNetwork(valueArg), [
|
|
85
|
+
'refusedMode', 'bundle', 'configuration',
|
|
86
|
+
]);
|
|
87
|
+
return {
|
|
88
|
+
refusedMode: legacyRuntimeMode(value.refusedMode),
|
|
89
|
+
bundle: snapshotSparkNodeBundleIdentity(value.bundle),
|
|
90
|
+
configuration: snapshotSparkLegacyConfigurationState(value.configuration),
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
/**
|
|
94
|
+
* Synchronous because the member carries no digest: the observation that transports it is
|
|
95
|
+
* digested as a whole, so a second domain here would only be a second thing to keep in step.
|
|
96
|
+
*/
|
|
97
|
+
export const validateSparkLegacyRuntimeRetirement = (valueArg) => {
|
|
98
|
+
try {
|
|
99
|
+
snapshotSparkLegacyRuntimeRetirement(valueArg);
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return ['Spark legacy runtime retirement must use its exact canonical schema'];
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
/** The revision rule every accepted receipt of this contract obeys, read once for both of them. */
|
|
107
|
+
const retirementReceiptBase = (valueArg, ownKeysArg) => {
|
|
108
|
+
const value = networkRecord(captureNetwork(valueArg), [
|
|
109
|
+
'id', 'generation', 'previous', 'clusterId', 'nodeId', 'observation', 'reporterSessionId',
|
|
110
|
+
'observationSequence', 'acceptedAt', 'digest',
|
|
111
|
+
...ownKeysArg,
|
|
112
|
+
]);
|
|
113
|
+
const { id, generation, previous } = networkRevision(value);
|
|
114
|
+
return {
|
|
115
|
+
base: {
|
|
116
|
+
id,
|
|
117
|
+
generation,
|
|
118
|
+
previous,
|
|
119
|
+
clusterId: networkId(value.clusterId),
|
|
120
|
+
nodeId: networkNodeId(value.nodeId),
|
|
121
|
+
observation: networkReference(value.observation),
|
|
122
|
+
reporterSessionId: networkId(value.reporterSessionId),
|
|
123
|
+
observationSequence: networkCounter(value.observationSequence),
|
|
124
|
+
acceptedAt: networkCounter(value.acceptedAt),
|
|
125
|
+
digest: networkDigest(value.digest),
|
|
126
|
+
},
|
|
127
|
+
record: value,
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
export const snapshotSparkLegacyModeRefusal = (valueArg) => safe(() => {
|
|
131
|
+
const { base, record } = retirementReceiptBase(valueArg, ['refusedMode', 'bundle']);
|
|
132
|
+
return {
|
|
133
|
+
...base,
|
|
134
|
+
refusedMode: legacyRuntimeMode(record.refusedMode),
|
|
135
|
+
bundle: snapshotSparkNodeBundleIdentity(record.bundle),
|
|
136
|
+
};
|
|
137
|
+
});
|
|
138
|
+
export const computeSparkLegacyModeRefusalDigest = async (valueArg) => networkHash(sparkLegacyRetirementContract.modeRefusalDigestDomain, snapshotSparkLegacyModeRefusal(valueArg));
|
|
139
|
+
export const validateSparkLegacyModeRefusal = async (valueArg) => {
|
|
140
|
+
try {
|
|
141
|
+
const value = snapshotSparkLegacyModeRefusal(valueArg);
|
|
142
|
+
return value.digest === await computeSparkLegacyModeRefusalDigest(value)
|
|
143
|
+
? [] : ['Spark legacy mode refusal digest does not match'];
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return ['Spark legacy mode refusal must use its exact canonical schema'];
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
export const snapshotSparkLegacyConfigurationRetirement = (valueArg) => safe(() => {
|
|
150
|
+
const { base, record } = retirementReceiptBase(valueArg, ['configuration']);
|
|
151
|
+
return { ...base, configuration: snapshotSparkLegacyConfigurationState(record.configuration) };
|
|
152
|
+
});
|
|
153
|
+
export const computeSparkLegacyConfigurationRetirementDigest = async (valueArg) => networkHash(sparkLegacyRetirementContract.configurationRetirementDigestDomain, snapshotSparkLegacyConfigurationRetirement(valueArg));
|
|
154
|
+
export const validateSparkLegacyConfigurationRetirement = async (valueArg) => {
|
|
155
|
+
try {
|
|
156
|
+
const value = snapshotSparkLegacyConfigurationRetirement(valueArg);
|
|
157
|
+
return value.digest === await computeSparkLegacyConfigurationRetirementDigest(value)
|
|
158
|
+
? [] : ['Spark legacy configuration retirement digest does not match'];
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return ['Spark legacy configuration retirement must use its exact canonical schema'];
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
/**
|
|
165
|
+
* Whether one observed configuration state is a legal successor of the accepted one.
|
|
166
|
+
*
|
|
167
|
+
* `null` is a node's first accepted state. A replay states the accepted generation again and is
|
|
168
|
+
* therefore not an advance: it renews nothing, which is what keeps `acceptedAt` and the named
|
|
169
|
+
* observation from aging forward on a node that has stopped changing.
|
|
170
|
+
*/
|
|
171
|
+
export const advancesSparkLegacyConfigurationState = (currentArg, nextArg) => {
|
|
172
|
+
const next = snapshotSparkLegacyConfigurationState(nextArg);
|
|
173
|
+
if (currentArg === null)
|
|
174
|
+
return true;
|
|
175
|
+
if (next.sourceSha256 !== currentArg.sourceSha256)
|
|
176
|
+
return false;
|
|
177
|
+
if (next.ownerGeneration <= currentArg.ownerGeneration)
|
|
178
|
+
return false;
|
|
179
|
+
return !(currentArg.disposition === 'removed' && next.disposition !== 'removed');
|
|
180
|
+
};
|
|
181
|
+
/**
|
|
182
|
+
* Joins the two accepted receipts a Coreflow retirement cites to the one observation they were
|
|
183
|
+
* derived from.
|
|
184
|
+
*
|
|
185
|
+
* Content join only. Whether each receipt is its node's current revision, and whether the
|
|
186
|
+
* observation it names is still fresh, remain Cloudly's to decide against its own records.
|
|
187
|
+
*/
|
|
188
|
+
export const bindSparkLegacyRetirement = async (modeRefusalArg, configurationRetirementArg) => {
|
|
189
|
+
try {
|
|
190
|
+
const modeRefusal = snapshotSparkLegacyModeRefusal(modeRefusalArg);
|
|
191
|
+
const configurationRetirement = snapshotSparkLegacyConfigurationRetirement(configurationRetirementArg);
|
|
192
|
+
if ((await validateSparkLegacyModeRefusal(modeRefusal)).length
|
|
193
|
+
|| (await validateSparkLegacyConfigurationRetirement(configurationRetirement)).length)
|
|
194
|
+
fail();
|
|
195
|
+
requireNetwork(modeRefusal.clusterId === configurationRetirement.clusterId
|
|
196
|
+
&& modeRefusal.nodeId === configurationRetirement.nodeId
|
|
197
|
+
&& modeRefusal.reporterSessionId === configurationRetirement.reporterSessionId
|
|
198
|
+
&& modeRefusal.observationSequence === configurationRetirement.observationSequence
|
|
199
|
+
&& networkEqual(modeRefusal.observation, configurationRetirement.observation));
|
|
200
|
+
return { modeRefusal, configurationRetirement };
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
return fail();
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3BhcmtsZWdhY3lyZXRpcmVtZW50LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vdHMvZGF0YS9zcGFya2xlZ2FjeXJldGlyZW1lbnQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBRUEsT0FBTyxFQUFFLGdCQUFnQixFQUFFLE1BQU0sOEJBQThCLENBQUM7QUFDaEUsT0FBTyxFQUNMLGNBQWMsRUFDZCxjQUFjLEVBQ2QsYUFBYSxFQUNiLFlBQVksRUFDWixXQUFXLEVBQ1gsU0FBUyxFQUNULGFBQWEsRUFDYixhQUFhLEVBQ2IsZ0JBQWdCLEVBQ2hCLGVBQWUsRUFDZixjQUFjLEdBQ2YsTUFBTSwrQkFBK0IsQ0FBQztBQUV2QywyRkFBMkY7QUFDM0YsTUFBTSxDQUFDLE1BQU0sdUJBQXVCLEdBQUcsQ0FBQyxTQUFTLEVBQUUsZUFBZSxDQUFVLENBQUM7QUFJN0U7Ozs7R0FJRztBQUNILE1BQU0sQ0FBQyxNQUFNLDhCQUE4QixHQUFHLE1BQU0sQ0FBQyxNQUFNLENBQUM7SUFDMUQsU0FBUyxFQUFFLE1BQU0sQ0FBQyxNQUFNLENBQUMsQ0FBQyxTQUFTLENBQUMsQ0FBQztJQUNyQyxlQUFlLEVBQUUsTUFBTSxDQUFDLE1BQU0sQ0FBQyxDQUFDLFVBQVUsRUFBRSxXQUFXLEVBQUUsYUFBYSxDQUFDLENBQUM7Q0FDWixDQUFDLENBQUM7QUF5RmhFLE1BQU0sQ0FBQyxNQUFNLDZCQUE2QixHQUFHLE1BQU0sQ0FBQyxNQUFNLENBQUM7SUFDekQsdUJBQXVCLEVBQUUsc0NBQXNDO0lBQy9ELG1DQUFtQyxFQUFFLGtEQUFrRDtDQUMvRSxDQUFDLENBQUM7QUFFWiwwRkFBMEY7QUFDMUYsTUFBTSxDQUFDLE1BQU0sNkJBQTZCLEdBQUcsTUFBTSxDQUFDLE1BQU0sQ0FBQztJQUN6RCwrREFBK0Q7SUFDL0Qsb0NBQW9DO0lBQ3BDLHVGQUF1RjtJQUN2Rix1Q0FBdUM7SUFDdkMscUZBQXFGO0lBQ3JGLGtDQUFrQztJQUNsQyxxREFBcUQ7SUFDckQs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
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as userInterfaces from '../data/user.js';
|
|
2
2
|
import * as clusterInterfaces from '../data/cluster.js';
|
|
3
|
+
import type * as relayCredentialInterfaces from '../data/clusterrelaycredential.js';
|
|
3
4
|
import type * as clusterVpnInterfaces from '../data/clustervpn.js';
|
|
4
5
|
import type { IClusterRuntime, TClusterRuntimeBlocker } from '../data/clusterruntime.js';
|
|
5
6
|
import type { IServiceRuntimeAssignmentView } from '../data/serviceruntime.js';
|
|
@@ -116,6 +117,59 @@ export interface IReq_Relay_Cloudly_RegisterClusterRelay extends plugins.typedre
|
|
|
116
117
|
request: clusterInterfaces.IRegisterClusterRelayRequest;
|
|
117
118
|
response: clusterInterfaces.IRegisterClusterRelayResponse;
|
|
118
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* Mints a cluster's relay credential, or rotates it at its exact current generation.
|
|
122
|
+
*
|
|
123
|
+
* Only the verified credential is read from the caller: a credential mutation trusts no identity
|
|
124
|
+
* member a caller states about itself. The bearer is never in the answer; it is delivered as the
|
|
125
|
+
* encrypted secret version `credential.delivery` names. The first mint also records the shared
|
|
126
|
+
* authority the cluster still carries, so a replacement never exists without a named predecessor.
|
|
127
|
+
* Cloudly refuses with one of `data.clusterRelayCredentialRefusals`.
|
|
128
|
+
*/
|
|
129
|
+
export interface IReq_Admin_Cloudly_MutateClusterRelayCredential extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Admin_Cloudly_MutateClusterRelayCredential> {
|
|
130
|
+
method: 'mutateClusterRelayCredential';
|
|
131
|
+
request: {
|
|
132
|
+
identity: userInterfaces.IIdentityCredential;
|
|
133
|
+
mutation: relayCredentialInterfaces.TClusterRelayCredentialMutation;
|
|
134
|
+
};
|
|
135
|
+
response: relayCredentialInterfaces.IClusterRelayCredentialMutationResult;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* One cluster's relay authority chain as Cloudly records it: the relay credential, the shared
|
|
139
|
+
* authority it replaces and the current accepted registration, each `null` while no record exists.
|
|
140
|
+
*
|
|
141
|
+
* Liveness is not here. It is a fact of a socket, which `getClusterRelayRegistrations` answers and
|
|
142
|
+
* which Cloudly checks for itself before it accepts a retirement.
|
|
143
|
+
*/
|
|
144
|
+
export interface IReq_Any_Cloudly_GetClusterRelayCredential extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Any_Cloudly_GetClusterRelayCredential> {
|
|
145
|
+
method: 'getClusterRelayCredential';
|
|
146
|
+
request: {
|
|
147
|
+
identity: userInterfaces.IIdentityCredential;
|
|
148
|
+
clusterId: string;
|
|
149
|
+
};
|
|
150
|
+
response: {
|
|
151
|
+
credential: relayCredentialInterfaces.IClusterRelayCredential | null;
|
|
152
|
+
legacy: relayCredentialInterfaces.IClusterLegacyRelayCredential | null;
|
|
153
|
+
registration: clusterInterfaces.IClusterRelayAcceptedRegistration | null;
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Revokes a cluster's shared pre-relay authority, which is the last step of replacing it.
|
|
158
|
+
*
|
|
159
|
+
* Its own request rather than a mutation of the relay credential, because it is the one step that
|
|
160
|
+
* removes authority: Cloudly accepts it only while the named replacement is the current relay
|
|
161
|
+
* credential, the named registration is current and was made under that replacement, and a
|
|
162
|
+
* verified socket carries that registration. Every other answer is one of
|
|
163
|
+
* `data.clusterRelayCredentialRefusals`.
|
|
164
|
+
*/
|
|
165
|
+
export interface IReq_Admin_Cloudly_RetireLegacyClusterRelayCredential extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Admin_Cloudly_RetireLegacyClusterRelayCredential> {
|
|
166
|
+
method: 'retireLegacyClusterRelayCredential';
|
|
167
|
+
request: {
|
|
168
|
+
identity: userInterfaces.IIdentityCredential;
|
|
169
|
+
retirement: relayCredentialInterfaces.IClusterLegacyRelayCredentialRetirementRequest;
|
|
170
|
+
};
|
|
171
|
+
response: relayCredentialInterfaces.IClusterLegacyRelayCredentialRetirementResult;
|
|
172
|
+
}
|
|
119
173
|
/**
|
|
120
174
|
* Cloudly hands the cluster relay the DNS lease renewals it has signed ahead of time for one node.
|
|
121
175
|
*
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import * as userInterfaces from '../data/user.js';
|
|
2
2
|
import * as clusterInterfaces from '../data/cluster.js';
|
|
3
3
|
import * as plugins from '../plugins.js';
|
|
4
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
4
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2x1c3Rlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3RzL3JlcXVlc3RzL2NsdXN0ZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxLQUFLLGNBQWMsTUFBTSxpQkFBaUIsQ0FBQztBQUNsRCxPQUFPLEtBQUssaUJBQWlCLE1BQU0sb0JBQW9CLENBQUM7QUFNeEQsT0FBTyxLQUFLLE9BQU8sTUFBTSxlQUFlLENBQUMifQ==
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -1198,6 +1198,10 @@ source is identified by SHA-256 and may be `retained-inert` or `removed`; deleti
|
|
|
1198
1198
|
erasure are not cutover requirements. Its exact owner generation is retained with that disposition.
|
|
1199
1199
|
Retained recovery evidence is valid only while the released
|
|
1200
1200
|
owner cannot consume it into live mode and the old credential remains revoked.
|
|
1201
|
+
`relay.credential`, `relay.acceptedRegistration`, `legacy.credential` and `legacy.invalidation`
|
|
1202
|
+
reference the records of [Cluster Relay Credential](#cluster-relay-credential). The old-mode
|
|
1203
|
+
refusal, the configuration CAS and the Spark bundle and unit identity are Spark-owned and come
|
|
1204
|
+
from [Spark Legacy Runtime Retirement](#spark-legacy-runtime-retirement).
|
|
1201
1205
|
|
|
1202
1206
|
Before any post-pallet assignment exists, an administrator calls
|
|
1203
1207
|
`requests.fleetcutover.prepareFleetCutoverIngress` with the exact server-created service ID,
|
|
@@ -2027,12 +2031,13 @@ with `clusterRelayCertificateDomainName(relay)`, which returns the origin's host
|
|
|
2027
2031
|
Cloudly issues the certificate for and owns the A record for. An origin naming an address literal
|
|
2028
2032
|
therefore has no issuable certificate.
|
|
2029
2033
|
|
|
2030
|
-
`data.clusterRelayUndispatchedRefusals` names the
|
|
2031
|
-
custody — `relay-cluster-not-enabled`, `relay-node-foreign
|
|
2032
|
-
with `data.TClusterRelayUndispatchedRefusal` as their union.
|
|
2033
|
-
records and stating that it does not dispatch to this relay, so a
|
|
2034
|
-
it holds for its nodes instead of waiting. A refusal that only
|
|
2035
|
-
relay states nothing about dispatch and is deliberately not in
|
|
2034
|
+
`data.clusterRelayUndispatchedRefusals` names the four registration refusals that end a relay's
|
|
2035
|
+
custody — `relay-cluster-not-enabled`, `relay-node-foreign`, `relay-sequence-not-advancing` and
|
|
2036
|
+
`relay-credential-generation-stale` — with `data.TClusterRelayUndispatchedRefusal` as their union.
|
|
2037
|
+
Each is Cloudly reading its own records and stating that it does not dispatch to this relay, so a
|
|
2038
|
+
relay that hears one drops what it holds for its nodes instead of waiting. A refusal that only
|
|
2039
|
+
says Cloudly could not verify the relay states nothing about dispatch and is deliberately not in
|
|
2040
|
+
the list.
|
|
2036
2041
|
|
|
2037
2042
|
```typescript
|
|
2038
2043
|
const relay: data.IClusterRelay = {
|
|
@@ -2109,6 +2114,133 @@ const registrationReasons = data.validateRegisterClusterRelayRequest(registratio
|
|
|
2109
2114
|
How the relay reaches each node's Corestore is part of the same document: see
|
|
2110
2115
|
[Corestore Through The Relay](#corestore-through-the-relay).
|
|
2111
2116
|
|
|
2117
|
+
### Cluster Relay Credential
|
|
2118
|
+
|
|
2119
|
+
A cluster's relay authenticates with its own generation-bearing credential instead of the shared
|
|
2120
|
+
cluster authority every cluster-side component used before it. `IClusterRelayCredential` is public
|
|
2121
|
+
metadata: the cluster, a `generation` that advances by one per accepted mint or rotation, the
|
|
2122
|
+
machine `userId` it authenticates as, and `delivery`, the `ISecretVersionReference` of the
|
|
2123
|
+
immutable encrypted version the bearer was delivered as. No record, receipt, request or response
|
|
2124
|
+
of this contract carries a bearer, a hash or key material, and every reader is exact-keyed, so a
|
|
2125
|
+
peer that adds a secret-looking member is refused rather than copied.
|
|
2126
|
+
|
|
2127
|
+
The credential authenticates the cluster's whole control-plane session, exactly what the shared
|
|
2128
|
+
authority authenticated, so a relay changes one credential and keeps every call it makes. The gain
|
|
2129
|
+
is a generation Cloudly fences each socket against and an authority it can rotate and retire on
|
|
2130
|
+
its own; the scope is not narrower. One cluster has one relay and therefore one credential record,
|
|
2131
|
+
and it never expires by itself: rotation is its lifecycle.
|
|
2132
|
+
|
|
2133
|
+
`mutateClusterRelayCredential { identity, mutation }` takes `TClusterRelayCredentialMutation`.
|
|
2134
|
+
`ensure` requires `expectedGeneration: null` and an absent record; `rotate` requires the exact
|
|
2135
|
+
current generation, never a minimum. The caller retains the complete request and its `mutationId`
|
|
2136
|
+
across retries: the identical body answers with the original result and `replayed: true` without
|
|
2137
|
+
renewing delivery, generation or authority, and a changed body fails closed. The first mint also
|
|
2138
|
+
records the shared authority the cluster still carries as `IClusterLegacyRelayCredential`, so a
|
|
2139
|
+
replacement never exists without a named predecessor. Rotation has no overlap: the previous
|
|
2140
|
+
generation stops authenticating at commit, and a relay still holding it hears
|
|
2141
|
+
`relay-credential-generation-stale`, drops what it holds for its nodes, reads the delivered version
|
|
2142
|
+
and reconnects. There is no revocation of the relay credential, because a rotation already covers
|
|
2143
|
+
a compromised one without a state in which the cluster has no relay authority at all.
|
|
2144
|
+
|
|
2145
|
+
An accepted `registerClusterRelay` answers with two references beside the origin and the offer:
|
|
2146
|
+
`credential`, the generation Cloudly authenticated the socket as, and `acceptedRegistration`, the
|
|
2147
|
+
record that acceptance wrote. The relay states no generation of its own — the registration request
|
|
2148
|
+
is unchanged — because a relay naming its own generation would be asserting its own rank.
|
|
2149
|
+
`IClusterRelayAcceptedRegistration` is that record: the registration facts the served read shape
|
|
2150
|
+
also carries, its own `id`, a `generation` that advances once per acceptance, the `credential`
|
|
2151
|
+
reference and a `digest`. Liveness is not part of it. `IClusterRelayRegistration` keeps exactly its
|
|
2152
|
+
published members, the facts plus `live`.
|
|
2153
|
+
|
|
2154
|
+
`retireLegacyClusterRelayCredential { identity, retirement }` revokes the shared authority, and is
|
|
2155
|
+
the last step of replacing it. `IClusterLegacyRelayCredentialRetirementRequest` carries
|
|
2156
|
+
compare-and-swap inputs only: the exact `expectedLegacyGeneration`, the `replacement` credential
|
|
2157
|
+
reference and the `acceptedRegistration` reference the caller observed. Cloudly dereferences both
|
|
2158
|
+
against its own current records, requires the registration to have been made under that
|
|
2159
|
+
replacement, and requires a verified live socket carrying it; a caller can state no liveness or
|
|
2160
|
+
proof of its own. Acceptance writes the one-shot `IClusterLegacyRelayCredentialInvalidation`
|
|
2161
|
+
(`generation: 1`) and advances the legacy record to `state: 'revoked'` naming it. The invalidation
|
|
2162
|
+
names the legacy record at the generation before the revocation and the record names the
|
|
2163
|
+
invalidation after it, so neither digest covers the other's.
|
|
2164
|
+
`data.clusterRelayCredentialRefusals` names every other answer, with
|
|
2165
|
+
`data.TClusterRelayCredentialRefusal` as their union. `getClusterRelayCredential { identity,
|
|
2166
|
+
clusterId }` reads the chain: the credential, the legacy record and the current accepted
|
|
2167
|
+
registration, each `null` while no record exists.
|
|
2168
|
+
|
|
2169
|
+
Each digest-bearing record has a `snapshot…`, `compute…Digest` and `validate…` function under its
|
|
2170
|
+
own digest domain in `data.clusterRelayCredentialContract` and `data.clusterRelayContract`.
|
|
2171
|
+
`bindClusterLegacyRelayCredentialInvalidation(invalidation, legacyCredential,
|
|
2172
|
+
acceptedRegistration)` joins an invalidation to the legacy record it revoked — the active record
|
|
2173
|
+
it names or that record's revoked successor — and to a registration whose `credential` equals the
|
|
2174
|
+
invalidation's `replacement`. It is a content check; dereferencing each record as current and
|
|
2175
|
+
requiring the live socket remain Cloudly's.
|
|
2176
|
+
|
|
2177
|
+
All three requests read only the verified `IIdentityCredential`, never an identity member a caller
|
|
2178
|
+
states about itself.
|
|
2179
|
+
|
|
2180
|
+
### Spark Legacy Runtime Retirement
|
|
2181
|
+
|
|
2182
|
+
The Spark-owned half of `IFleetCutoverCoreflowRetirementEvidence` — the refused old mode, the
|
|
2183
|
+
configuration compare-and-swap, and the bundle and unit identity — is transported on the Swarm
|
|
2184
|
+
observation a node already posts. There is no route, request or endpoint for it. That is forced
|
|
2185
|
+
rather than preferred: the evidence requires `relay.nodeObservation` and `spark.observation` to be
|
|
2186
|
+
the identical accepted reference, so these facts must reach Cloudly on the very observation that
|
|
2187
|
+
carries the node's Docker census.
|
|
2188
|
+
|
|
2189
|
+
`data.ISparkLegacyRuntimeRetirement` is the optional observation member, available in every
|
|
2190
|
+
observation state because it describes the host and not its Docker daemon. It is
|
|
2191
|
+
`{ refusedMode, bundle, configuration }` and nothing else. It names no node: Cloudly binds it to
|
|
2192
|
+
the reporter it already authenticated, so there is no node identity on it to disagree with. It
|
|
2193
|
+
carries no digest of its own either — `computeSparkSwarmObservationDigest` canonicalizes the whole
|
|
2194
|
+
observation, so a second domain here would only be a second thing to keep in step.
|
|
2195
|
+
|
|
2196
|
+
`refusedMode` is one of `data.sparkLegacyRuntimeModes`, the two runtimes a released Spark could be
|
|
2197
|
+
put into by its `mode` configuration key. `data.sparkLegacyRuntimeModeServices` is the record over
|
|
2198
|
+
that union naming the Docker services each mode owns, so a refusal states a mode and a reader
|
|
2199
|
+
derives the set. `data.ISparkNodeBundleIdentity { version, sourceCommit, manifestSha256, unitName,
|
|
2200
|
+
unitDigest }` is read back from the installed tree and the installed unit file, never from the
|
|
2201
|
+
running process's own build constants: a build can state its own version, it cannot state that the
|
|
2202
|
+
host will start that same code again. `data.ISparkLegacyConfigurationState { ownerGeneration,
|
|
2203
|
+
sourceSha256, disposition }` names the released key-value source by digest alone. No decoded member
|
|
2204
|
+
of that file is representable in any shape here, because it carries the node's released bearer and
|
|
2205
|
+
its jump code; `sourceSha256` covers the whole file, whose smallest unguessable part is a 256-bit
|
|
2206
|
+
token. `disposition` is `retained-inert` or `removed`; erasure is not a cutover requirement.
|
|
2207
|
+
|
|
2208
|
+
Cloudly accepts an observation first and then advances two of its own records, which is what the
|
|
2209
|
+
evidence's two references point at. `data.ISparkLegacyModeRefusal` and
|
|
2210
|
+
`data.ISparkLegacyConfigurationRetirement` are revisions, not one-shot receipts: a node restates
|
|
2211
|
+
both on every observation, a bundle upgrade changes what the boot path is, and a disposition
|
|
2212
|
+
changes once. Both carry the accepted observation they were derived from, its reporter session and
|
|
2213
|
+
its sequence, and both obey the revision rule every record of this package obeys — a first
|
|
2214
|
+
generation has no predecessor and a later one names exactly the generation before it. The
|
|
2215
|
+
configuration record carries two counters on purpose: its own `generation` is contiguous because it
|
|
2216
|
+
is this record's revision, while `configuration.ownerGeneration` is the node's own and may skip,
|
|
2217
|
+
since an observation that never arrived must not look like a skipped revision.
|
|
2218
|
+
|
|
2219
|
+
`advancesSparkLegacyConfigurationState(current, next)` is the compare-and-swap rule, `null` for a
|
|
2220
|
+
node's first accepted state: the same `sourceSha256`, a strictly higher `ownerGeneration`, and no
|
|
2221
|
+
return from `removed` to `retained-inert`. Restating the accepted generation is a replay and
|
|
2222
|
+
therefore not an advance, which is what keeps `acceptedAt` and the named observation from aging
|
|
2223
|
+
forward on a node that has stopped changing. `data.sparkLegacyRetirementRefusals` names every other
|
|
2224
|
+
answer, with `data.TSparkLegacyRetirementRefusal` as their union.
|
|
2225
|
+
`bindSparkLegacyRetirement(modeRefusal, configurationRetirement)` joins the two receipts a
|
|
2226
|
+
retirement cites, requiring the same cluster, node, reporter session, sequence and accepted
|
|
2227
|
+
observation. It is a content check; whether each receipt is its node's current revision remains
|
|
2228
|
+
Cloudly's.
|
|
2229
|
+
|
|
2230
|
+
An omitted member is not a refusal and advances nothing, so the existence of these records is not
|
|
2231
|
+
what makes a retirement honest — the freshness of the observation they name is. Assembly must
|
|
2232
|
+
require that observation to still be inside `fleetCutoverContract.observationFreshnessMs`, exactly
|
|
2233
|
+
as `IFleetCutoverSparkObservationReference` already does, or a node that stopped refusing would go
|
|
2234
|
+
on looking retired.
|
|
2235
|
+
|
|
2236
|
+
Cloudly reads this member before Spark writes it. Every observation validator in this package is
|
|
2237
|
+
exact-key, so a server built against an earlier minor refuses the whole observation rather than
|
|
2238
|
+
ignoring a member it does not know — the peers of a session kind are upgraded before the side that
|
|
2239
|
+
starts depending on the later minor, and the `minimumPeerVersion` of that session kind is raised in
|
|
2240
|
+
the same commit that starts depending on it. A Spark that begins stating this member therefore
|
|
2241
|
+
raises its `sparkSwarmNode` minimum alone, which requires one minimum per session kind rather than
|
|
2242
|
+
one shared across a deployable's offers.
|
|
2243
|
+
|
|
2112
2244
|
### Cluster VPN Hub And Network
|
|
2113
2245
|
|
|
2114
2246
|
A cluster's relay is also that cluster's managed VPN hub: every node of the cluster dials it, and
|