@namzu/sandbox 26.0.0 → 27.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -0
- package/README.md +20 -7
- package/dist/backends/firecracker/protocol.d.ts +46 -0
- package/dist/backends/firecracker/protocol.d.ts.map +1 -1
- package/dist/backends/firecracker/protocol.js +31 -0
- package/dist/backends/firecracker/protocol.js.map +1 -1
- package/dist/backends/firecracker/transport.d.ts +15 -1
- package/dist/backends/firecracker/transport.d.ts.map +1 -1
- package/dist/backends/firecracker/transport.js +16 -0
- package/dist/backends/firecracker/transport.js.map +1 -1
- package/dist/backends/kubernetes/allocation.d.ts +115 -0
- package/dist/backends/kubernetes/allocation.d.ts.map +1 -0
- package/dist/backends/kubernetes/allocation.js +130 -0
- package/dist/backends/kubernetes/allocation.js.map +1 -0
- package/dist/backends/kubernetes/egress-policy.d.ts +111 -35
- package/dist/backends/kubernetes/egress-policy.d.ts.map +1 -1
- package/dist/backends/kubernetes/egress-policy.js +81 -41
- package/dist/backends/kubernetes/egress-policy.js.map +1 -1
- package/dist/backends/kubernetes/identity.d.ts +129 -0
- package/dist/backends/kubernetes/identity.d.ts.map +1 -1
- package/dist/backends/kubernetes/identity.js +150 -0
- package/dist/backends/kubernetes/identity.js.map +1 -1
- package/dist/backends/kubernetes/index.d.ts +23 -1
- package/dist/backends/kubernetes/index.d.ts.map +1 -1
- package/dist/backends/kubernetes/index.js +22 -3
- package/dist/backends/kubernetes/index.js.map +1 -1
- package/dist/backends/kubernetes/objects.d.ts +76 -0
- package/dist/backends/kubernetes/objects.d.ts.map +1 -1
- package/dist/backends/kubernetes/objects.js +61 -0
- package/dist/backends/kubernetes/objects.js.map +1 -1
- package/dist/backends/kubernetes/per-sandbox-fence.d.ts +194 -0
- package/dist/backends/kubernetes/per-sandbox-fence.d.ts.map +1 -0
- package/dist/backends/kubernetes/per-sandbox-fence.js +522 -0
- package/dist/backends/kubernetes/per-sandbox-fence.js.map +1 -0
- package/dist/backends/kubernetes/per-sandbox-policy.d.ts +45 -3
- package/dist/backends/kubernetes/per-sandbox-policy.d.ts.map +1 -1
- package/dist/backends/kubernetes/per-sandbox-policy.js +98 -8
- package/dist/backends/kubernetes/per-sandbox-policy.js.map +1 -1
- package/dist/backends/kubernetes/sandbox.d.ts +4 -9
- package/dist/backends/kubernetes/sandbox.d.ts.map +1 -1
- package/dist/backends/kubernetes/sandbox.js +4 -9
- package/dist/backends/kubernetes/sandbox.js.map +1 -1
- package/dist/backends/kubernetes/transport.d.ts +108 -1
- package/dist/backends/kubernetes/transport.d.ts.map +1 -1
- package/dist/backends/kubernetes/transport.js +150 -6
- package/dist/backends/kubernetes/transport.js.map +1 -1
- package/dist/backends/kubernetes/workspace-allocate.d.ts +206 -0
- package/dist/backends/kubernetes/workspace-allocate.d.ts.map +1 -0
- package/dist/backends/kubernetes/workspace-allocate.js +332 -0
- package/dist/backends/kubernetes/workspace-allocate.js.map +1 -0
- package/dist/backends/kubernetes/workspace-observe.d.ts +187 -0
- package/dist/backends/kubernetes/workspace-observe.d.ts.map +1 -0
- package/dist/backends/kubernetes/workspace-observe.js +343 -0
- package/dist/backends/kubernetes/workspace-observe.js.map +1 -0
- package/dist/backends/kubernetes/workspace-per-sandbox.d.ts +94 -0
- package/dist/backends/kubernetes/workspace-per-sandbox.d.ts.map +1 -0
- package/dist/backends/kubernetes/workspace-per-sandbox.js +119 -0
- package/dist/backends/kubernetes/workspace-per-sandbox.js.map +1 -0
- package/dist/backends/kubernetes/workspace.d.ts +139 -5
- package/dist/backends/kubernetes/workspace.d.ts.map +1 -1
- package/dist/backends/kubernetes/workspace.js +434 -36
- package/dist/backends/kubernetes/workspace.js.map +1 -1
- package/dist/index.d.ts +83 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +100 -6
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/backends/firecracker/protocol.ts +47 -0
- package/src/backends/firecracker/transport.ts +31 -0
- package/src/backends/kubernetes/allocation.ts +199 -0
- package/src/backends/kubernetes/egress-policy.ts +165 -42
- package/src/backends/kubernetes/identity.ts +210 -0
- package/src/backends/kubernetes/index.ts +61 -2
- package/src/backends/kubernetes/objects.ts +98 -0
- package/src/backends/kubernetes/per-sandbox-fence.ts +800 -0
- package/src/backends/kubernetes/per-sandbox-policy.ts +152 -17
- package/src/backends/kubernetes/sandbox.ts +4 -9
- package/src/backends/kubernetes/transport.ts +203 -4
- package/src/backends/kubernetes/workspace-allocate.ts +607 -0
- package/src/backends/kubernetes/workspace-observe.ts +590 -0
- package/src/backends/kubernetes/workspace-per-sandbox.ts +179 -0
- package/src/backends/kubernetes/workspace.ts +655 -34
- package/src/index.ts +193 -3
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-sandbox egress on a workspace: the pieces `createKubernetesWorkspace`,
|
|
3
|
+
* `allocateKubernetesWorkspace` and a workspace handle share.
|
|
4
|
+
*
|
|
5
|
+
* ## The shape, and where it differs from a task sandbox
|
|
6
|
+
*
|
|
7
|
+
* - **Label.** The per-sandbox selector label rides on the workspace's
|
|
8
|
+
* directly created `Sandbox` pod template, composed by
|
|
9
|
+
* `composeAdditionalPodLabels` beside the egress profile. Its VALUE is the
|
|
10
|
+
* Sandbox's own name, `namzu-ws-<workspaceId>`: deterministic, known before
|
|
11
|
+
* the POST, and stable across a suspend and a resume, because a resume
|
|
12
|
+
* patches `operatingMode` and the pod template (labels included) is carried
|
|
13
|
+
* by the object, not by the pod.
|
|
14
|
+
* - **Owner.** The `Sandbox` itself. A policy names it in `ownerReferences`
|
|
15
|
+
* with its `metadata.uid`, so deleting the workspace (`destroy({ deleteDisk:
|
|
16
|
+
* true })`, `deleteKubernetesWorkspace`) lets the cluster's garbage
|
|
17
|
+
* collector remove every policy it owns. The default `destroy()` only
|
|
18
|
+
* suspends, keeps the object, and so keeps the policy.
|
|
19
|
+
* - **Name.** `namzu-sbx-<sandbox uid>`, the same rule as a task sandbox. The
|
|
20
|
+
* workspace NAME is deterministic and its uid is not, so a workspace deleted
|
|
21
|
+
* and created again under the same name selects by the same label value
|
|
22
|
+
* and owns a policy of a different name.
|
|
23
|
+
*
|
|
24
|
+
* ## Suspend and resume
|
|
25
|
+
*
|
|
26
|
+
* The behaviour is one rule, applied on every path that is about to run the
|
|
27
|
+
* workspace's pod: a resume, and an open of an existing workspace.
|
|
28
|
+
*
|
|
29
|
+
* 1. The admission fence is re-verified from scratch (never from a memo). A
|
|
30
|
+
* fence that is missing or would not enforce REFUSES the resume, before
|
|
31
|
+
* the patch: the workspace stays suspended, which is the only state that
|
|
32
|
+
* is safe without the fence, and nothing is written.
|
|
33
|
+
* 2. Every per-sandbox policy in the namespace that selects this workspace
|
|
34
|
+
* (name prefix `namzu-sbx-`, selector label value equal to the Sandbox's
|
|
35
|
+
* name) and that this Sandbox does not own (no `ownerReferences` entry of
|
|
36
|
+
* kind `Sandbox` with this name and the uid the cluster reports NOW) is
|
|
37
|
+
* deleted. Such a policy belongs to a workspace that was deleted and
|
|
38
|
+
* created again under the same name and has not been garbage-collected
|
|
39
|
+
* yet; left alone it would grant the new workspace what the old one's
|
|
40
|
+
* consent covered.
|
|
41
|
+
* 3. A policy this Sandbox owns is KEPT, unchanged. A suspend is not a
|
|
42
|
+
* revocation: the grant was admitted by the fence when it was written and
|
|
43
|
+
* the resumed pod carries the same label, so the policy applies to it
|
|
44
|
+
* again. A host that wants the grant gone calls `setNetworkPolicy({
|
|
45
|
+
* allowedHosts: [] })` (which deletes it) before it suspends.
|
|
46
|
+
*
|
|
47
|
+
* A freshly created workspace gets step 2 after the create reply, with the uid
|
|
48
|
+
* the reply carried.
|
|
49
|
+
*
|
|
50
|
+
* ## What is NOT proven here
|
|
51
|
+
*
|
|
52
|
+
* That any of it is enforced. Enforcement is the CNI's data plane: it needs a
|
|
53
|
+
* cluster running Cilium, the same as the task path.
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
import type { KubernetesEgressConfig, KubernetesPerSandboxEgressConfig } from './egress-policy.js'
|
|
57
|
+
import { perSandboxEgressLabelKey } from './egress-policy.js'
|
|
58
|
+
import { KubernetesAlreadyGoneError, type KubernetesClient } from './k8s-client.js'
|
|
59
|
+
import { ciliumNetworkPolicyCollectionPath, ciliumNetworkPolicyPath } from './objects.js'
|
|
60
|
+
import { PER_SANDBOX_POLICY_NAME_PREFIX, type PerSandboxPolicyFence } from './per-sandbox-policy.js'
|
|
61
|
+
|
|
62
|
+
/** The profile variant a workspace with per-sandbox egress configured carries. */
|
|
63
|
+
export const KUBERNETES_PER_SANDBOX_PROFILE_VARIANT = 'core/no-network+per-sandbox'
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The extra pod-label map a workspace named `sandboxName` contributes to
|
|
67
|
+
* `composeAdditionalPodLabels`, or `undefined` when `perSandbox` is not
|
|
68
|
+
* configured, which is what keeps that workspace's request log unchanged.
|
|
69
|
+
*/
|
|
70
|
+
export function perSandboxWorkspaceLabels(
|
|
71
|
+
egress: KubernetesEgressConfig | undefined,
|
|
72
|
+
sandboxName: string,
|
|
73
|
+
): Readonly<Record<string, string>> | undefined {
|
|
74
|
+
const key = perSandboxEgressLabelKey(egress)
|
|
75
|
+
return key === undefined ? undefined : { [key]: sandboxName }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** `labels` without the per-sandbox key `extra` carries; `labels` itself when there is none. */
|
|
79
|
+
export function withoutPerSandboxLabel(
|
|
80
|
+
labels: Readonly<Record<string, string>>,
|
|
81
|
+
extra: Readonly<Record<string, string>> | undefined,
|
|
82
|
+
): Readonly<Record<string, string>> {
|
|
83
|
+
if (extra === undefined) return labels
|
|
84
|
+
return Object.fromEntries(Object.entries(labels).filter(([key]) => !(key in extra)))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** What a list of CiliumNetworkPolicies answers with. */
|
|
88
|
+
interface CiliumPolicyList {
|
|
89
|
+
readonly items?: readonly {
|
|
90
|
+
readonly metadata?: {
|
|
91
|
+
readonly name?: string
|
|
92
|
+
readonly ownerReferences?: readonly {
|
|
93
|
+
readonly kind?: string
|
|
94
|
+
readonly name?: string
|
|
95
|
+
readonly uid?: string
|
|
96
|
+
}[]
|
|
97
|
+
}
|
|
98
|
+
readonly spec?: {
|
|
99
|
+
readonly endpointSelector?: { readonly matchLabels?: Readonly<Record<string, unknown>> }
|
|
100
|
+
}
|
|
101
|
+
}[]
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The names of the per-sandbox policies this workspace's label selects and
|
|
106
|
+
* its Sandbox does not own. Read only.
|
|
107
|
+
*/
|
|
108
|
+
export async function findForeignPerSandboxPolicies(
|
|
109
|
+
client: KubernetesClient,
|
|
110
|
+
namespace: string,
|
|
111
|
+
labelKey: string,
|
|
112
|
+
sandboxName: string,
|
|
113
|
+
ownerUid: string,
|
|
114
|
+
signal?: AbortSignal,
|
|
115
|
+
): Promise<readonly string[]> {
|
|
116
|
+
const list = await client.request<CiliumPolicyList>(
|
|
117
|
+
'GET',
|
|
118
|
+
ciliumNetworkPolicyCollectionPath(namespace),
|
|
119
|
+
undefined,
|
|
120
|
+
signal,
|
|
121
|
+
)
|
|
122
|
+
const foreign: string[] = []
|
|
123
|
+
for (const item of list?.items ?? []) {
|
|
124
|
+
const name = item?.metadata?.name
|
|
125
|
+
if (typeof name !== 'string' || !name.startsWith(PER_SANDBOX_POLICY_NAME_PREFIX)) continue
|
|
126
|
+
if (item?.spec?.endpointSelector?.matchLabels?.[labelKey] !== sandboxName) continue
|
|
127
|
+
const owned = (item.metadata?.ownerReferences ?? []).some(
|
|
128
|
+
(ref) => ref?.kind === 'Sandbox' && ref.name === sandboxName && ref.uid === ownerUid,
|
|
129
|
+
)
|
|
130
|
+
if (!owned) foreign.push(name)
|
|
131
|
+
}
|
|
132
|
+
return foreign
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** What {@link reverifyPerSandboxEgress} needs. */
|
|
136
|
+
export interface PerSandboxReverification {
|
|
137
|
+
readonly client: KubernetesClient
|
|
138
|
+
readonly namespace: string
|
|
139
|
+
readonly sandboxName: string
|
|
140
|
+
/** The uid the cluster reports for the Sandbox NOW. */
|
|
141
|
+
readonly ownerUid: string
|
|
142
|
+
readonly egress: KubernetesEgressConfig & {
|
|
143
|
+
readonly perSandbox: KubernetesPerSandboxEgressConfig
|
|
144
|
+
}
|
|
145
|
+
/** A fence whose memo has not been used yet. */
|
|
146
|
+
readonly fence: PerSandboxPolicyFence
|
|
147
|
+
readonly signal?: AbortSignal
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Steps 1 and 2 of the resume rule above: prove the fence, then delete the
|
|
152
|
+
* policies that select this workspace and that its Sandbox does not own.
|
|
153
|
+
* Returns the names it deleted. Throws the fence's own refusal, having
|
|
154
|
+
* written nothing, when the fence does not verify.
|
|
155
|
+
*/
|
|
156
|
+
export async function reverifyPerSandboxEgress(
|
|
157
|
+
options: PerSandboxReverification,
|
|
158
|
+
): Promise<readonly string[]> {
|
|
159
|
+
const { client, namespace, sandboxName, ownerUid, egress, fence, signal } = options
|
|
160
|
+
await fence.assertApplied(signal)
|
|
161
|
+
const labelKey = perSandboxEgressLabelKey(egress)
|
|
162
|
+
if (labelKey === undefined) return []
|
|
163
|
+
const foreign = await findForeignPerSandboxPolicies(
|
|
164
|
+
client,
|
|
165
|
+
namespace,
|
|
166
|
+
labelKey,
|
|
167
|
+
sandboxName,
|
|
168
|
+
ownerUid,
|
|
169
|
+
signal,
|
|
170
|
+
)
|
|
171
|
+
for (const name of foreign) {
|
|
172
|
+
try {
|
|
173
|
+
await client.request('DELETE', ciliumNetworkPolicyPath(namespace, name), undefined, signal)
|
|
174
|
+
} catch (err) {
|
|
175
|
+
if (!(err instanceof KubernetesAlreadyGoneError)) throw err
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return foreign
|
|
179
|
+
}
|