@lakutata/containerd 3.0.0-beta.6 → 3.1.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/README.md +88 -79
- package/README.zh-CN.md +374 -0
- package/package.json +27 -6
package/README.md
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
# @lakutata/containerd
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
English | [中文](README.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
The containerd component of the lakutata framework. It manages the images, containers, CNI networks and named volumes of
|
|
6
|
+
a [containerd](https://containerd.io) namespace by talking directly to containerd's gRPC socket, without a Docker daemon
|
|
7
|
+
or nerdctl (the gRPC client is written in TypeScript, with no native addon). It targets Linux machines that run
|
|
8
|
+
containerd alone, such as embedded devices, and its API follows that of the Docker component. Images are pulled with
|
|
9
|
+
registry credentials, pushed, tagged, imported, exported and pruned. Containers are created with ports, volumes,
|
|
10
|
+
devices, seccomp and resource limits; they can then be started and stopped, run commands, stream their logs, report
|
|
11
|
+
their stats, and be updated and committed, and have directories exported from them. Builds are sent to BuildKit. The
|
|
12
|
+
containers it creates are the same as those nerdctl creates, so `nerdctl ps` lists them. The umbrella package `lakutata`
|
|
13
|
+
re-exports it as `lakutata/com/containerd`.
|
|
11
14
|
|
|
12
15
|
## Installation
|
|
13
16
|
|
|
@@ -15,11 +18,11 @@ package `lakutata` re-exports it as `lakutata/com/containerd`.
|
|
|
15
18
|
npm install lakutata
|
|
16
19
|
```
|
|
17
20
|
|
|
18
|
-
The component has no optional npm
|
|
21
|
+
The component has no optional npm dependencies. The machine needs:
|
|
19
22
|
|
|
20
23
|
| Feature | Needs |
|
|
21
24
|
|---|---|
|
|
22
|
-
| Everything | Linux, containerd 1.6 or later with runc (tested with 1.6, 1.7 and 2.4), Node.js 22
|
|
25
|
+
| Everything | Linux, containerd 1.6 or later with runc (tested with 1.6, 1.7 and 2.4), Node.js `^22.13.0 || >=24.11.0`; the application runs as root or as a member of the group that owns containerd's socket |
|
|
23
26
|
| CNI networks, published ports | The CNI plugins (`bridge`, `portmap`, `host-local`… in `/opt/cni/bin`, `/usr/lib/cni` or `/usr/libexec/cni`) and iptables |
|
|
24
27
|
| `exec()` with `stdin`, separate `stdout`/`stderr`, terminals | `mkfifo` on the host (without it, `exec()` puts both outputs in `stdout`) |
|
|
25
28
|
| Builds | A BuildKit daemon (`buildkitd`): local, on its unix socket, or remote over TCP/TLS |
|
|
@@ -29,9 +32,10 @@ The component has no optional npm dependency. The machine needs:
|
|
|
29
32
|
|
|
30
33
|
### The component in an application
|
|
31
34
|
|
|
32
|
-
Register `Containerd` in the `components`
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
Register `Containerd` in the application's `components` and get it with `getObject()` or `@Inject`. It connects when
|
|
36
|
+
it is created: without containerd on `socketPath`, its creation fails with `ContainerdConnectionException`, and the
|
|
37
|
+
application fails to start when the component is listed in `bootstrap`, as here. The component creates its namespace
|
|
38
|
+
if it does not exist.
|
|
35
39
|
|
|
36
40
|
```typescript
|
|
37
41
|
import {Application} from 'lakutata'
|
|
@@ -52,7 +56,7 @@ Application
|
|
|
52
56
|
cgroupManager: 'cgroupfs',
|
|
53
57
|
//The containers' logs, hosts, resolv.conf, settings, and the named volumes
|
|
54
58
|
dataRoot: '/var/lib/lakutata/containerd',
|
|
55
|
-
//A container's log file rotated beyond 10 MiB when the component starts
|
|
59
|
+
//A container's log file is rotated beyond 10 MiB when the component starts the container (default 0: never)
|
|
56
60
|
logMaxSize: 10 * 1024 * 1024,
|
|
57
61
|
registryAuths: {'registry.example.com': {username: 'deploy', password: 'secret'}},
|
|
58
62
|
insecureRegistries: ['localhost:5000']
|
|
@@ -74,17 +78,20 @@ Application
|
|
|
74
78
|
|
|
75
79
|
### What differs from the Docker component
|
|
76
80
|
|
|
77
|
-
-
|
|
78
|
-
containers' OCI specifications, as nerdctl does. The containers carry nerdctl's labels
|
|
79
|
-
CNI network configurations are nerdctl's (`nerdctl-<name>.conflist`)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
the
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
-
|
|
81
|
+
- There is no daemon in between: the component itself pulls, unpacks, pushes, imports and exports images, and
|
|
82
|
+
generates the containers' OCI specifications, as nerdctl does. The containers carry nerdctl's labels
|
|
83
|
+
(`nerdctl/name`…), and the CNI network configurations are nerdctl's (`nerdctl-<name>.conflist`), so nerdctl and the
|
|
84
|
+
component share them.
|
|
85
|
+
- containerd's shim writes a container's output to a log file, and containerd's restart monitor applies its restart
|
|
86
|
+
policy, so both keep working while the application is stopped. The logs have no timestamps.
|
|
87
|
+
- `createContainer()` takes the image in its options and never pulls it: pull it first. As with Docker, a container
|
|
88
|
+
joins the default network, `bridge`, unless `network` or `networks` says otherwise (a CNI network created on first
|
|
89
|
+
use, so the CNI plugins are needed; see the installation), and its ports are published there; `network: 'host'`
|
|
90
|
+
gives it the host's network.
|
|
91
|
+
- There is no `loginRepository()`: the credentials come from the `registryAuths` option, or from the `auth` of each
|
|
92
|
+
call.
|
|
93
|
+
- `importImage()` returns every image in the archive, and `prune()` returns what it removed.
|
|
94
|
+
- The component works within one namespace: the images, containers and volumes of other namespaces are not visible.
|
|
88
95
|
|
|
89
96
|
### Images
|
|
90
97
|
|
|
@@ -93,7 +100,7 @@ import {type Containerd, type ContainerdImage, type ContainerdPruneResult} from
|
|
|
93
100
|
|
|
94
101
|
declare const containerd: Containerd
|
|
95
102
|
|
|
96
|
-
//Pulled for the machine's platform, unpacked into the snapshotter (only the platforms the machine
|
|
103
|
+
//Pulled for the machine's platform, unpacked into the snapshotter (only the platforms the machine can run are unpacked)
|
|
97
104
|
const image: ContainerdImage = await containerd.pullImage({
|
|
98
105
|
image: 'alpine:3.20',
|
|
99
106
|
progressCallback: (progress): void => console.log(progress)
|
|
@@ -101,7 +108,7 @@ const image: ContainerdImage = await containerd.pullImage({
|
|
|
101
108
|
//docker.io/library/alpine:3.20, ['linux/arm64/v8'], {env, cmd, entrypoint, workingDir, user, exposedPorts…}
|
|
102
109
|
console.log(image.name, image.platforms, image.config)
|
|
103
110
|
|
|
104
|
-
//Other platforms (or 'all'),
|
|
111
|
+
//Other platforms (or 'all'), and credentials for a single pull
|
|
105
112
|
await containerd.pullImage({image: 'alpine:3.20', platform: ['linux/amd64', 'linux/arm/v7']})
|
|
106
113
|
await containerd.pullImage({image: 'registry.example.com/team/app:1.0', auth: {username: 'u', password: 'p'}})
|
|
107
114
|
|
|
@@ -115,7 +122,7 @@ await image.export({destination: '/tmp/alpine.tar'})
|
|
|
115
122
|
await containerd.exportImages(['alpine:3.20', 'busybox'], {destination: '/tmp/images.tar'})
|
|
116
123
|
const imported: ContainerdImage[] = await containerd.importImage({source: '/tmp/images.tar'})
|
|
117
124
|
console.log(imported.length, (await containerd.listImages()).length)
|
|
118
|
-
//
|
|
125
|
+
//A new name: only for an archive with a single image
|
|
119
126
|
await containerd.importImage({source: '/tmp/alpine.tar', name: 'alpine:backup'})
|
|
120
127
|
|
|
121
128
|
await tagged.remove()
|
|
@@ -124,8 +131,8 @@ const pruned: ContainerdPruneResult = await containerd.prune({containers: true,
|
|
|
124
131
|
console.log(pruned.images)
|
|
125
132
|
```
|
|
126
133
|
|
|
127
|
-
|
|
128
|
-
content while an image or a container uses it. A multi-platform image is described
|
|
134
|
+
Names are normalized as Docker does (`alpine` is `docker.io/library/alpine:latest`). containerd keeps an image's
|
|
135
|
+
content while an image or a container uses it. A multi-platform image is described by the first of its platforms that
|
|
129
136
|
the machine runs.
|
|
130
137
|
|
|
131
138
|
### Containers
|
|
@@ -148,7 +155,7 @@ const container: ContainerdContainer = await containerd.createContainer({
|
|
|
148
155
|
//A device, and the USB serial ports plugged later (major 188)
|
|
149
156
|
devices: [{hostPath: '/dev/ttyUSB0'}],
|
|
150
157
|
deviceCgroupRules: ['c 188:* rwm'],
|
|
151
|
-
//
|
|
158
|
+
//Plus the same amount of swap (memorySwap: twice the memory by default, -1 for unlimited)
|
|
152
159
|
memoryLimit: 128 * 1024 * 1024,
|
|
153
160
|
cpus: 0.5,
|
|
154
161
|
restartPolicy: 'always'
|
|
@@ -167,7 +174,7 @@ for await (const chunk of await container.logs({tail: 100, follow: false})) proc
|
|
|
167
174
|
const stats: ContainerdContainerStats = await container.stats()
|
|
168
175
|
console.log(stats.cpu.percent, stats.memory.usage, stats.pids.current)
|
|
169
176
|
|
|
170
|
-
//Resources and restart policy
|
|
177
|
+
//Resources and the restart policy change live (0 removes a limit); any other setting recreates the container, keeping its name
|
|
171
178
|
await container.update({memoryLimit: 256 * 1024 * 1024, restartPolicy: 'unless-stopped'})
|
|
172
179
|
await container.update({cpus: 0})
|
|
173
180
|
await container.update({env: {MODE: 'debug'}})
|
|
@@ -181,33 +188,33 @@ await container.pause()
|
|
|
181
188
|
await container.unpause()
|
|
182
189
|
//Its stop signal, then SIGKILL after 10 seconds
|
|
183
190
|
await container.stop({timeout: 10})
|
|
184
|
-
//
|
|
191
|
+
//For a container that is not running: its last exit code, at once
|
|
185
192
|
console.log(await container.wait())
|
|
186
193
|
await container.remove()
|
|
187
194
|
|
|
188
195
|
console.log(await containerd.listContainers({app: 'worker'}))
|
|
189
196
|
```
|
|
190
197
|
|
|
191
|
-
-
|
|
192
|
-
nerdctl
|
|
193
|
-
|
|
194
|
-
- `devices` creates
|
|
195
|
-
For
|
|
196
|
-
`--device-cgroup-rule
|
|
197
|
-
them).
|
|
198
|
-
-
|
|
198
|
+
- Containers get containerd's default seccomp profile, with the system calls their `capabilities` allow (as with
|
|
199
|
+
nerdctl); `seccompProfile: 'unconfined'` disables it, and a file path gives an OCI profile. Privileged containers are
|
|
200
|
+
unconfined.
|
|
201
|
+
- `devices` creates host devices in the container (a single device, or every device in a directory such as
|
|
202
|
+
`/dev/snd`). For devices plugged in later, bind their directory and allow their numbers with `deviceCgroupRules`, as
|
|
203
|
+
with Docker's `--device-cgroup-rule`. A `privileged` container gets every device in the host's `/dev` (as nerdctl
|
|
204
|
+
gives them).
|
|
205
|
+
- On cgroup v2, containers have their own cgroup namespace (`cgroupns: 'host'` shares the host's).
|
|
199
206
|
- `privileged`, `capabilities`, `capabilitiesDrop`, `readonlyRootfs`, `pidMode: 'host'`, `ipcMode: 'host'`, `sysctls`,
|
|
200
|
-
`extraHosts`, `dns`, `shmSize`, `pidsLimit`, `cpuSet`, `cpuShares`, `memorySwap` and `OOMKillDisable`
|
|
201
|
-
options of the same names. As with Docker, `memorySwap` is the memory and the swap together,
|
|
202
|
-
and
|
|
203
|
-
- `update()` with other
|
|
207
|
+
`extraHosts`, `dns`, `shmSize`, `pidsLimit`, `cpuSet`, `cpuShares`, `memorySwap` and `OOMKillDisable` work like the
|
|
208
|
+
Docker options of the same names. As with Docker, `memorySwap` is the memory and the swap together, requires
|
|
209
|
+
`memoryLimit`, and defaults to twice `memoryLimit`.
|
|
210
|
+
- `update()` with settings other than the resources and the restart policy only works for containers the component
|
|
204
211
|
created (it keeps their settings in `dataRoot`). A resource set to `0` (`cpuSet: []`) is removed at once, from the
|
|
205
212
|
running container too.
|
|
206
|
-
- `wait()`
|
|
207
|
-
`0`
|
|
213
|
+
- `wait()` on a container that is not running resolves at once, as `docker wait` does, with the exit code of its last
|
|
214
|
+
run (`0` if it was never started).
|
|
208
215
|
|
|
209
|
-
Terminals run a process with a pseudo terminal in a running container: `/bin/sh` by default
|
|
210
|
-
through `/bin/sh -c
|
|
216
|
+
Terminals run a process with a pseudo terminal in a running container: `/bin/sh` by default. A string command runs
|
|
217
|
+
through `/bin/sh -c`, as with `exec()`, and an array runs as is.
|
|
211
218
|
|
|
212
219
|
```typescript
|
|
213
220
|
import {type Containerd, type ContainerdContainer, type ContainerdContainerTTY} from 'lakutata/com/containerd'
|
|
@@ -227,17 +234,17 @@ await tty.close()
|
|
|
227
234
|
|
|
228
235
|
### Networks
|
|
229
236
|
|
|
230
|
-
Besides `host` and `none`,
|
|
231
|
-
created
|
|
232
|
-
IPv6 subnets. Each container's networks are set up by its runc hooks, so
|
|
233
|
-
without the application.
|
|
237
|
+
Besides `host` and `none`, containers join CNI networks: nerdctl's default `bridge` (`nerdctl0`, `10.4.0.0/24`,
|
|
238
|
+
created on first use; the network of containers created without `network` or `networks`) and the networks you create
|
|
239
|
+
(bridges, macvlans or ipvlans, with IPv4 and IPv6 subnets). Each container's networks are set up by its runc hooks, so
|
|
240
|
+
they also work when containerd restarts the container without the application.
|
|
234
241
|
|
|
235
242
|
```typescript
|
|
236
243
|
import {type Containerd, type ContainerdContainer, type ContainerdNetworkInfo} from 'lakutata/com/containerd'
|
|
237
244
|
|
|
238
245
|
declare const containerd: Containerd
|
|
239
246
|
|
|
240
|
-
//
|
|
247
|
+
//Without subnet: a free subnet of 10.4.0.0/16
|
|
241
248
|
const backend: ContainerdNetworkInfo = await containerd.createNetwork({name: 'backend', subnet: '10.4.10.0/24'})
|
|
242
249
|
await containerd.createNetwork({name: 'dual', subnet: '10.4.11.0/24', ipv6Subnet: 'fd00:4:11::/64'})
|
|
243
250
|
await containerd.createNetwork({name: 'lan', driver: 'macvlan', parent: 'eth0', subnet: '192.168.1.0/24', gateway: '192.168.1.1'})
|
|
@@ -275,13 +282,13 @@ console.log(await containerd.listVolumes())
|
|
|
275
282
|
await containerd.prune({networks: true, volumes: true})
|
|
276
283
|
```
|
|
277
284
|
|
|
278
|
-
`removeVolume()` refuses a volume that a container mounts, even a stopped
|
|
285
|
+
`removeVolume()` refuses a volume that a container mounts, even a stopped container.
|
|
279
286
|
|
|
280
287
|
### Builds with BuildKit
|
|
281
288
|
|
|
282
289
|
`buildImage()` sends the build context (without the files its `.dockerignore` excludes) and the Dockerfile to BuildKit
|
|
283
|
-
over
|
|
284
|
-
`buildkitd` (its unix socket, with its containerd or runc worker) or a remote one over TCP, with TLS:
|
|
290
|
+
over a BuildKit session, as `docker buildx` does, then imports the built image into containerd. BuildKit is either a
|
|
291
|
+
local `buildkitd` (on its unix socket, with its containerd or runc worker) or a remote one over TCP, with TLS:
|
|
285
292
|
|
|
286
293
|
```typescript
|
|
287
294
|
import {readFileSync} from 'node:fs'
|
|
@@ -322,9 +329,9 @@ BuildKit pulls the base images with the component's registry credentials (or `au
|
|
|
322
329
|
### Namespaces, platforms and events
|
|
323
330
|
|
|
324
331
|
Each component works in one namespace; register one component per namespace to manage several (`k8s.io` is the
|
|
325
|
-
Kubernetes
|
|
326
|
-
`binfmt_misc`, so
|
|
327
|
-
containerd's events
|
|
332
|
+
Kubernetes namespace). `supportedPlatforms()` lists the platforms the machine can run: its own, and those of the
|
|
333
|
+
emulators registered in `binfmt_misc`, so a container of another architecture runs once its emulator is registered.
|
|
334
|
+
`events()` streams containerd's events for the namespace.
|
|
328
335
|
|
|
329
336
|
```typescript
|
|
330
337
|
import {Application} from 'lakutata'
|
|
@@ -365,46 +372,48 @@ Application
|
|
|
365
372
|
| `ContainerdContainerTTY` | A terminal in a container: `raw`, `readable`, `writable`, `onData`, `write`, `resize`, `wait`, `close` |
|
|
366
373
|
| `ImagePullOptions`, `ImagePushOptions`, `ImageBuildOptions`, `ImageImportOptions`, `ImageExportOptions` | The options of the image operations |
|
|
367
374
|
| `ContainerCreateOptions`, `ContainerStopOptions`, `ContainerKillOptions`, `ContainerRemoveOptions`, `ContainerExecOptions`, `ContainerLogsOptions`, `ContainerTTYOptions`, `ContainerTTYConsoleSizeOptions`, `ContainerResourceOptions`, `ContainerCommitOptions`, `ContainerExportDirectoryOptions` | The options of the container operations |
|
|
368
|
-
| `NetworkCreateOptions`, `VolumeCreateOptions`, `ContainerdPruneOptions`, `ContainerdRegistryAuthOptions` | The options
|
|
369
|
-
| `ContainerdBind`, `ContainerdVolume`, `ContainerdTmpfs`, `ContainerdDevice`, `ContainerdContainerPort`, `ContainerdRestartPolicy` |
|
|
375
|
+
| `NetworkCreateOptions`, `VolumeCreateOptions`, `ContainerdPruneOptions`, `ContainerdRegistryAuthOptions` | The options for networks, volumes, `prune` and registry credentials |
|
|
376
|
+
| `ContainerdBind`, `ContainerdVolume`, `ContainerdTmpfs`, `ContainerdDevice`, `ContainerdContainerPort`, `ContainerdRestartPolicy` | Parts of the container options |
|
|
370
377
|
| `ContainerdContainerState`, `ContainerdContainerNetwork`, `ContainerdContainerStats`, `ContainerdExecResult`, `ContainerdImageConfig`, `ContainerdNetworkInfo`, `ContainerdVolumeInfo`, `ContainerdPruneResult`, `ContainerdEvent` | The information returned |
|
|
371
|
-
| `ContainerdProgressCallback`, `ContainerdBuildOutputCallback` | The callbacks
|
|
378
|
+
| `ContainerdProgressCallback`, `ContainerdBuildOutputCallback` | The callbacks for transfers and builds |
|
|
372
379
|
| `ContainerdConnectionException` and the other `Containerd*Exception` | The exceptions (see below) |
|
|
373
380
|
|
|
374
|
-
All are exported by `lakutata/com/containerd` and `@lakutata/containerd`. The typings document each declaration with
|
|
381
|
+
All are exported by `lakutata/com/containerd` and `@lakutata/containerd`. The typings document each declaration, with
|
|
375
382
|
examples.
|
|
376
383
|
|
|
377
384
|
## Errors
|
|
378
385
|
|
|
379
386
|
| Exception | When |
|
|
380
387
|
|---|---|
|
|
381
|
-
| `ContainerdConnectionException` (`E_CONTAINERD_CONNECTION`) | The component is created and containerd does not answer on `socketPath` (not running,
|
|
382
|
-
| `ContainerdImageNotFoundException` (`E_CONTAINERD_IMAGE_NOT_FOUND`) | No image has the name: `getImage()`, `createContainer()`
|
|
383
|
-
| `ContainerdPlatformException` (`E_CONTAINERD_PLATFORM`) | The image has no manifest for the platform
|
|
388
|
+
| `ContainerdConnectionException` (`E_CONTAINERD_CONNECTION`) | The component is created and containerd does not answer on `socketPath` (it is not running, or the socket is not accessible), or `buildkitTLS` is given with a unix socket `buildkitAddress` |
|
|
389
|
+
| `ContainerdImageNotFoundException` (`E_CONTAINERD_IMAGE_NOT_FOUND`) | No image has the name: `getImage()`, `createContainer()` with an image that was not pulled, an operation on a removed image |
|
|
390
|
+
| `ContainerdPlatformException` (`E_CONTAINERD_PLATFORM`) | The image has no manifest for the requested platform (pull, push, container creation) |
|
|
384
391
|
| `ContainerdImagePullException` (`E_CONTAINERD_IMAGE_PULL`) | The registry cannot be reached, refuses the credentials or does not have the reference; the transfer or the unpacking fails |
|
|
385
392
|
| `ContainerdImagePushException` (`E_CONTAINERD_IMAGE_PUSH`) | The registry cannot be reached or refuses the push |
|
|
386
|
-
| `ContainerdImageImportException` (`E_CONTAINERD_IMAGE_IMPORT`) | The archive cannot be read or
|
|
393
|
+
| `ContainerdImageImportException` (`E_CONTAINERD_IMAGE_IMPORT`) | The archive cannot be read or contains no image; a `name` is given with an archive of several images; the unpacking fails |
|
|
387
394
|
| `ContainerdImageExportException` (`E_CONTAINERD_IMAGE_EXPORT`) | The archive cannot be written |
|
|
388
|
-
| `ContainerdImageBuildException` (`E_CONTAINERD_IMAGE_BUILD`) | BuildKit cannot be reached, the build fails, or the image
|
|
389
|
-
| `ContainerdContainerNotFoundException` (`E_CONTAINERD_CONTAINER_NOT_FOUND`) | No container has the
|
|
395
|
+
| `ContainerdImageBuildException` (`E_CONTAINERD_IMAGE_BUILD`) | BuildKit cannot be reached, the build fails, or the built image cannot be imported |
|
|
396
|
+
| `ContainerdContainerNotFoundException` (`E_CONTAINERD_CONTAINER_NOT_FOUND`) | No container has the id, name or id prefix |
|
|
390
397
|
| `ContainerdContainerNameConflictException` (`E_CONTAINERD_CONTAINER_NAME_CONFLICT`) | Another container has the name |
|
|
391
|
-
| `ContainerdContainerStateException` (`E_CONTAINERD_CONTAINER_STATE`) | `exec()`, `createTTY()`, `stats()`, `kill()`, `pause()`, `unpause()
|
|
392
|
-
| `ContainerdPathNotFoundException` (`E_CONTAINERD_PATH_NOT_FOUND`) | `exportDirectory()` of a path
|
|
393
|
-
| `ContainerdNetworkNotFoundException` (`E_CONTAINERD_NETWORK_NOT_FOUND`) | No network has the name or
|
|
398
|
+
| `ContainerdContainerStateException` (`E_CONTAINERD_CONTAINER_STATE`) | `exec()`, `createTTY()`, `stats()`, `kill()`, `pause()`, `unpause()` or `processes()` on a container that is not running; `start()` on a paused one; `remove()` on a running one without `force`; an `update()` that would recreate a container the component did not create |
|
|
399
|
+
| `ContainerdPathNotFoundException` (`E_CONTAINERD_PATH_NOT_FOUND`) | `exportDirectory()` of a path that does not exist in the container |
|
|
400
|
+
| `ContainerdNetworkNotFoundException` (`E_CONTAINERD_NETWORK_NOT_FOUND`) | No network has the name or id; a container is created on a missing network |
|
|
394
401
|
| `ContainerdNetworkConflictException` (`E_CONTAINERD_NETWORK_CONFLICT`) | The network exists, its subnet overlaps another one, a container uses it, it is the default `bridge`, or nerdctl did not create it |
|
|
395
402
|
| `ContainerdVolumeNotFoundException` (`E_CONTAINERD_VOLUME_NOT_FOUND`) | The volume does not exist |
|
|
396
403
|
| `ContainerdVolumeInUseException` (`E_CONTAINERD_VOLUME_IN_USE`) | A container mounts the volume |
|
|
397
|
-
| `InvalidValueException` (from `lakutata`) | The component's options are invalid, when it is created; a live `update()` of
|
|
404
|
+
| `InvalidValueException` (from `lakutata`) | The component's options are invalid, when it is created; a live `update()` of `memorySwap` without a memory limit, or below it |
|
|
398
405
|
| `InvalidMethodAcceptException` (from `lakutata`) | The arguments of a call do not match its options (a container name with a space, a missing `image`, a `memorySwap` without `memoryLimit`) |
|
|
399
|
-
| `GRPCError` (from `@lakutata/grpc`) | `wait()` aborted (the object destroyed); containerd lost |
|
|
400
|
-
| `Error` | The settings of a container cannot be applied (ports without a CNI network, no command, no CNI plugin, a user missing from the image); a container prefix
|
|
406
|
+
| `GRPCError` (from `@lakutata/grpc`) | `wait()` is aborted (the object is destroyed); the connection to containerd is lost |
|
|
407
|
+
| `Error` | The settings of a container cannot be applied (ports without a CNI network, no command, no CNI plugin, a user missing from the image); a container prefix matches several containers |
|
|
401
408
|
|
|
402
409
|
## See also
|
|
403
410
|
|
|
404
|
-
-
|
|
411
|
+
- The guide on the website: [Containerd Integration](https://lakutata.com/guide/ContainerdIntegration).
|
|
412
|
+
- `doc/en/ContainerdIntegration.md` in the `lakutata` package (Chinese version in `doc/zh`): the guide to the
|
|
413
|
+
component.
|
|
405
414
|
- `@lakutata/docker` (`lakutata/com/docker`): the same operations through a Docker daemon.
|
|
406
|
-
- `@lakutata/grpc`: the gRPC client the component and its builds use.
|
|
407
|
-
- `@lakutata/core` (`lakutata`):
|
|
415
|
+
- `@lakutata/grpc`: the gRPC client that the component and its builds use.
|
|
416
|
+
- `@lakutata/core` (`lakutata`): components, `Application` and dependency injection.
|
|
408
417
|
- `npm run test:engines` (in this package) runs the engine tests against containerd 1.6, 1.7 and 2.4 in privileged
|
|
409
418
|
Docker containers (`test-env/run.sh 2.4` for one version, `--build` to rebuild its image), with a local and a remote
|
|
410
419
|
BuildKit; see `test-env/README.md`.
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
# @lakutata/containerd
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 中文
|
|
4
|
+
|
|
5
|
+
lakutata 框架的 containerd 组件。它直接与 containerd 的 gRPC 套接字通信,管理一个 [containerd](https://containerd.io) 命名空间中的镜像、容器、CNI 网络和命名卷,不需要 Docker 守护进程,也不需要 nerdctl(gRPC 客户端用 TypeScript 编写,没有原生插件)。它面向只运行 containerd 的 Linux 机器,例如嵌入式设备,API 沿用 Docker 组件的 API。镜像可以拉取(支持镜像仓库凭据)、推送、打标签、导入、导出和清理。创建容器时可以设置端口、卷、设备、seccomp 和资源限制;之后可以启动和停止容器、在容器中执行命令、以流的形式读取日志、获取统计数据、更新容器、把容器提交为镜像,以及从容器中导出目录。构建交给 BuildKit 完成。它创建的容器与 nerdctl 创建的相同,因此 `nerdctl ps` 能列出它们。`lakutata` 包把它重新导出为 `lakutata/com/containerd`。
|
|
6
|
+
|
|
7
|
+
## 安装
|
|
8
|
+
|
|
9
|
+
```shell
|
|
10
|
+
npm install lakutata
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
组件没有可选的 npm 依赖。机器需要具备:
|
|
14
|
+
|
|
15
|
+
| 功能 | 需要 |
|
|
16
|
+
|---|---|
|
|
17
|
+
| 全部功能 | Linux,带 runc 的 containerd 1.6 或更高版本(已在 1.6、1.7 和 2.4 上测试),Node.js `^22.13.0 || >=24.11.0`;应用以 root 运行,或属于拥有 containerd 套接字的用户组 |
|
|
18
|
+
| CNI 网络、端口发布 | CNI 插件(`bridge`、`portmap`、`host-local` 等,位于 `/opt/cni/bin`、`/usr/lib/cni` 或 `/usr/libexec/cni`)以及 iptables |
|
|
19
|
+
| 带 `stdin` 的 `exec()`、分开的 `stdout`/`stderr`、终端 | 主机上的 `mkfifo`(没有时,`exec()` 把两种输出都放在 `stdout` 中) |
|
|
20
|
+
| 构建 | BuildKit 守护进程(`buildkitd`):可以在本机,通过它的 unix 套接字连接;也可以在远程,通过 TCP/TLS 连接 |
|
|
21
|
+
| 其他架构的容器 | 在 `binfmt_misc` 中注册的 QEMU(或 Rosetta)模拟器 |
|
|
22
|
+
|
|
23
|
+
## 用法
|
|
24
|
+
|
|
25
|
+
### 在应用中使用组件
|
|
26
|
+
|
|
27
|
+
在应用的 `components` 中注册 `Containerd`,再用 `getObject()` 或 `@Inject` 获取它。组件在创建时连接 containerd:`socketPath` 上没有 containerd 时,组件创建失败,抛出 `ContainerdConnectionException`;组件像这里一样列在 `bootstrap` 中时,应用会因此启动失败。命名空间不存在时,组件会创建它。
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import {Application} from 'lakutata'
|
|
31
|
+
import {Containerd, type ContainerdContainer, type ContainerdExecResult, type ContainerdImage} from 'lakutata/com/containerd'
|
|
32
|
+
|
|
33
|
+
Application
|
|
34
|
+
.run(() => ({
|
|
35
|
+
id: 'device.app',
|
|
36
|
+
name: 'Device',
|
|
37
|
+
components: {
|
|
38
|
+
containerd: {
|
|
39
|
+
class: Containerd,
|
|
40
|
+
//以下为默认值
|
|
41
|
+
socketPath: '/run/containerd/containerd.sock',
|
|
42
|
+
namespace: 'default',
|
|
43
|
+
snapshotter: 'overlayfs',
|
|
44
|
+
//containerd 以 SystemdCgroup = true 运行 runc 时设为 systemd
|
|
45
|
+
cgroupManager: 'cgroupfs',
|
|
46
|
+
//容器的日志、hosts、resolv.conf、设置,以及命名卷
|
|
47
|
+
dataRoot: '/var/lib/lakutata/containerd',
|
|
48
|
+
//组件启动容器时,超过 10 MiB 的日志文件会被轮转(默认为 0:从不轮转)
|
|
49
|
+
logMaxSize: 10 * 1024 * 1024,
|
|
50
|
+
registryAuths: {'registry.example.com': {username: 'deploy', password: 'secret'}},
|
|
51
|
+
insecureRegistries: ['localhost:5000']
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
bootstrap: ['containerd']
|
|
55
|
+
}))
|
|
56
|
+
.onLaunched(async (app: Application): Promise<void> => {
|
|
57
|
+
const containerd: Containerd = await app.getObject('containerd')
|
|
58
|
+
console.log(await containerd.version())
|
|
59
|
+
const image: ContainerdImage = await containerd.pullImage({image: 'alpine:3.20'})
|
|
60
|
+
const container: ContainerdContainer = await containerd.createContainer({image: image.name, name: 'worker', cmd: ['sleep', 'infinity']})
|
|
61
|
+
await container.start()
|
|
62
|
+
const result: ContainerdExecResult = await container.exec({cmd: 'cat /etc/os-release'})
|
|
63
|
+
console.log(result.stdout)
|
|
64
|
+
await container.remove({force: true})
|
|
65
|
+
})
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 与 Docker 组件的不同
|
|
69
|
+
|
|
70
|
+
- 中间没有守护进程:组件像 nerdctl 一样,自己拉取、解包、推送、导入和导出镜像,并生成容器的 OCI 规范。容器带有 nerdctl 的标签(`nerdctl/name` 等),CNI 网络配置也和 nerdctl 的相同(`nerdctl-<name>.conflist`),因此 nerdctl 和组件可以共用它们。
|
|
71
|
+
- 容器的输出由 containerd 的 shim 写入日志文件,重启策略由 containerd 的重启监视器执行,所以应用停止期间两者照常工作。日志没有时间戳。
|
|
72
|
+
- `createContainer()` 在选项中接收镜像,并且从不自动拉取:请先拉取。和 Docker 一样,除非 `network` 或 `networks` 另有指定,容器会加入默认网络 `bridge`(这是一个在第一次使用时创建的 CNI 网络,所以需要 CNI 插件,见“安装”一节),端口也发布在这个网络上;`network: 'host'` 让容器使用主机的网络。
|
|
73
|
+
- 没有 `loginRepository()`:凭据来自 `registryAuths` 选项,或者每次调用的 `auth`。
|
|
74
|
+
- `importImage()` 返回归档中的所有镜像,`prune()` 返回它删除的内容。
|
|
75
|
+
- 组件只在一个命名空间中工作:看不到其他命名空间的镜像、容器和卷。
|
|
76
|
+
|
|
77
|
+
### 镜像
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
import {type Containerd, type ContainerdImage, type ContainerdPruneResult} from 'lakutata/com/containerd'
|
|
81
|
+
|
|
82
|
+
declare const containerd: Containerd
|
|
83
|
+
|
|
84
|
+
//按本机平台拉取,并解包到快照器中(只解包本机能运行的平台)
|
|
85
|
+
const image: ContainerdImage = await containerd.pullImage({
|
|
86
|
+
image: 'alpine:3.20',
|
|
87
|
+
progressCallback: (progress): void => console.log(progress)
|
|
88
|
+
})
|
|
89
|
+
//docker.io/library/alpine:3.20、['linux/arm64/v8']、{env, cmd, entrypoint, workingDir, user, exposedPorts…}
|
|
90
|
+
console.log(image.name, image.platforms, image.config)
|
|
91
|
+
|
|
92
|
+
//其他平台(或 'all'),以及单次拉取的凭据
|
|
93
|
+
await containerd.pullImage({image: 'alpine:3.20', platform: ['linux/amd64', 'linux/arm/v7']})
|
|
94
|
+
await containerd.pullImage({image: 'registry.example.com/team/app:1.0', auth: {username: 'u', password: 'p'}})
|
|
95
|
+
|
|
96
|
+
//打标签和推送:推送已有的所有平台(连同索引),或者只推送指定的平台
|
|
97
|
+
const tagged: ContainerdImage = await image.tag('registry.example.com/team/alpine:3.20')
|
|
98
|
+
await tagged.push()
|
|
99
|
+
await tagged.push({reference: 'registry.example.com/team/alpine:3.20-arm64', platform: 'linux/arm64'})
|
|
100
|
+
|
|
101
|
+
//归档:带有 docker load 所需 manifest.json 的 OCI 镜像布局;也可以导入 docker save 的归档
|
|
102
|
+
await image.export({destination: '/tmp/alpine.tar'})
|
|
103
|
+
await containerd.exportImages(['alpine:3.20', 'busybox'], {destination: '/tmp/images.tar'})
|
|
104
|
+
const imported: ContainerdImage[] = await containerd.importImage({source: '/tmp/images.tar'})
|
|
105
|
+
console.log(imported.length, (await containerd.listImages()).length)
|
|
106
|
+
//指定新名称:只适用于只含一个镜像的归档
|
|
107
|
+
await containerd.importImage({source: '/tmp/alpine.tar', name: 'alpine:backup'})
|
|
108
|
+
|
|
109
|
+
await tagged.remove()
|
|
110
|
+
//已停止的容器和悬空镜像(allImages:所有没有容器使用的镜像)
|
|
111
|
+
const pruned: ContainerdPruneResult = await containerd.prune({containers: true, images: true, allImages: false})
|
|
112
|
+
console.log(pruned.images)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
镜像名称按 Docker 的规则规范化(`alpine` 即 `docker.io/library/alpine:latest`)。只要有镜像或容器在使用,containerd 就会保留镜像的内容。多平台镜像的描述信息取自其中第一个本机能运行的平台。
|
|
116
|
+
|
|
117
|
+
### 容器
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
import {type Containerd, type ContainerdContainer, type ContainerdContainerStats, type ContainerdExecResult, type ContainerdImage} from 'lakutata/com/containerd'
|
|
121
|
+
|
|
122
|
+
declare const containerd: Containerd
|
|
123
|
+
|
|
124
|
+
const container: ContainerdContainer = await containerd.createContainer({
|
|
125
|
+
image: 'alpine:3.20',
|
|
126
|
+
name: 'worker',
|
|
127
|
+
cmd: ['sh', '-c', 'while true; do date; sleep 1; done'],
|
|
128
|
+
env: {MODE: 'production'},
|
|
129
|
+
user: 'nobody',
|
|
130
|
+
labels: {app: 'worker'},
|
|
131
|
+
binds: [{hostPath: '/data/worker', containerPath: '/data'}],
|
|
132
|
+
volumes: [{volumeName: 'worker-cache', containerPath: '/cache'}],
|
|
133
|
+
tmpfs: [{containerPath: '/tmp', size: 16 * 1024 * 1024}],
|
|
134
|
+
//一个设备,以及之后插入的 USB 串口(主设备号 188)
|
|
135
|
+
devices: [{hostPath: '/dev/ttyUSB0'}],
|
|
136
|
+
deviceCgroupRules: ['c 188:* rwm'],
|
|
137
|
+
//另加同样大小的交换空间(memorySwap 默认为内存的两倍,-1 表示不限制)
|
|
138
|
+
memoryLimit: 128 * 1024 * 1024,
|
|
139
|
+
cpus: 0.5,
|
|
140
|
+
restartPolicy: 'always'
|
|
141
|
+
})
|
|
142
|
+
await container.start()
|
|
143
|
+
console.log(container.state.status, container.state.pid)
|
|
144
|
+
|
|
145
|
+
//命令为字符串时通过 /bin/sh -c 执行;输出收集在内存中
|
|
146
|
+
const result: ContainerdExecResult = await container.exec({cmd: 'cat /etc/os-release', user: 'root', timeout: 5000})
|
|
147
|
+
console.log(result.exitCode, result.stdout, result.stderr)
|
|
148
|
+
await container.exec({cmd: ['sh', '-c', 'cat > /data/input'], stdin: 'content'})
|
|
149
|
+
|
|
150
|
+
//最后 100 行;follow(默认)会在容器运行期间保持流打开
|
|
151
|
+
for await (const chunk of await container.logs({tail: 100, follow: false})) process.stdout.write(chunk)
|
|
152
|
+
|
|
153
|
+
const stats: ContainerdContainerStats = await container.stats()
|
|
154
|
+
console.log(stats.cpu.percent, stats.memory.usage, stats.pids.current)
|
|
155
|
+
|
|
156
|
+
//资源和重启策略在线修改(0 表示取消限制);修改其他设置会重新创建容器,名称不变
|
|
157
|
+
await container.update({memoryLimit: 256 * 1024 * 1024, restartPolicy: 'unless-stopped'})
|
|
158
|
+
await container.update({cpus: 0})
|
|
159
|
+
await container.update({env: {MODE: 'debug'}})
|
|
160
|
+
|
|
161
|
+
//把容器中的修改提交为镜像;把一个目录导出为 tar 归档
|
|
162
|
+
const patched: ContainerdImage = await container.commit({repoTag: 'worker:patched', changes: {cmd: ['date']}})
|
|
163
|
+
await container.exportDirectory({path: '/var/log', destination: '/tmp/logs.tar'})
|
|
164
|
+
console.log(patched.name, await container.processes())
|
|
165
|
+
|
|
166
|
+
await container.pause()
|
|
167
|
+
await container.unpause()
|
|
168
|
+
//先发送容器的停止信号,10 秒后再发送 SIGKILL
|
|
169
|
+
await container.stop({timeout: 10})
|
|
170
|
+
//容器没有运行时:立即返回它上一次的退出码
|
|
171
|
+
console.log(await container.wait())
|
|
172
|
+
await container.remove()
|
|
173
|
+
|
|
174
|
+
console.log(await containerd.listContainers({app: 'worker'}))
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
- 容器使用 containerd 的默认 seccomp 配置,并放行它的 `capabilities` 所允许的系统调用(与 nerdctl 相同);`seccompProfile: 'unconfined'` 关闭过滤,给出文件路径则使用文件中的 OCI 配置。特权容器不受限制。
|
|
178
|
+
- `devices` 在容器中创建主机的设备(单个设备,或某个目录下的所有设备,如 `/dev/snd`)。对于之后才插入的设备,绑定挂载它们所在的目录,并用 `deviceCgroupRules` 允许它们的设备号,与 Docker 的 `--device-cgroup-rule` 相同。`privileged` 容器能使用主机 `/dev` 中的所有设备(与 nerdctl 相同)。
|
|
179
|
+
- 在 cgroup v2 上,容器有自己的 cgroup 命名空间(`cgroupns: 'host'` 则共享主机的)。
|
|
180
|
+
- `privileged`、`capabilities`、`capabilitiesDrop`、`readonlyRootfs`、`pidMode: 'host'`、`ipcMode: 'host'`、`sysctls`、`extraHosts`、`dns`、`shmSize`、`pidsLimit`、`cpuSet`、`cpuShares`、`memorySwap` 和 `OOMKillDisable` 与 Docker 的同名选项作用相同。和 Docker 一样,`memorySwap` 是内存和交换空间的总和,需要同时设置 `memoryLimit`,默认是 `memoryLimit` 的两倍。
|
|
181
|
+
- `update()` 修改资源和重启策略以外的设置时,只对组件创建的容器有效(组件把它们的设置保存在 `dataRoot` 中)。设为 `0` 的资源(或 `cpuSet: []`)会立即取消限制,对正在运行的容器同样生效。
|
|
182
|
+
- 对没有运行的容器调用 `wait()` 会立即返回,结果是它上一次运行的退出码(从未启动过时为 `0`),与 `docker wait` 相同。
|
|
183
|
+
|
|
184
|
+
终端会在运行中的容器里启动一个带伪终端的进程,默认是 `/bin/sh`。与 `exec()` 一样,字符串形式的命令通过 `/bin/sh -c` 执行,数组则按原样执行。
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
import {type Containerd, type ContainerdContainer, type ContainerdContainerTTY} from 'lakutata/com/containerd'
|
|
188
|
+
|
|
189
|
+
declare const containerd: Containerd
|
|
190
|
+
|
|
191
|
+
const container: ContainerdContainer = await containerd.getContainer('worker')
|
|
192
|
+
const tty: ContainerdContainerTTY = await container.createTTY({consoleSize: {cols: 120, rows: 40}})
|
|
193
|
+
tty.onData((chunk: Buffer): void => {
|
|
194
|
+
process.stdout.write(chunk)
|
|
195
|
+
})
|
|
196
|
+
tty.write('ls\n')
|
|
197
|
+
await tty.resize({cols: 100, rows: 30})
|
|
198
|
+
//也可以使用双工流 tty.raw,把套接字通过管道接到它上面
|
|
199
|
+
await tty.close()
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### 网络
|
|
203
|
+
|
|
204
|
+
除了 `host` 和 `none`,容器加入的都是 CNI 网络:一个是 nerdctl 的默认网络 `bridge`(`nerdctl0`,`10.4.0.0/24`,在第一次使用时创建;创建容器时没有指定 `network` 或 `networks`,容器就加入这个网络),另一些是你创建的网络(bridge、macvlan 或 ipvlan,支持 IPv4 和 IPv6 子网)。每个容器的网络由它的 runc 钩子配置,所以即使应用没有运行、由 containerd 重启容器,网络也能正常工作。
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
import {type Containerd, type ContainerdContainer, type ContainerdNetworkInfo} from 'lakutata/com/containerd'
|
|
208
|
+
|
|
209
|
+
declare const containerd: Containerd
|
|
210
|
+
|
|
211
|
+
//不指定子网时:从 10.4.0.0/16 中选一个空闲子网
|
|
212
|
+
const backend: ContainerdNetworkInfo = await containerd.createNetwork({name: 'backend', subnet: '10.4.10.0/24'})
|
|
213
|
+
await containerd.createNetwork({name: 'dual', subnet: '10.4.11.0/24', ipv6Subnet: 'fd00:4:11::/64'})
|
|
214
|
+
await containerd.createNetwork({name: 'lan', driver: 'macvlan', parent: 'eth0', subnet: '192.168.1.0/24', gateway: '192.168.1.1'})
|
|
215
|
+
|
|
216
|
+
const web: ContainerdContainer = await containerd.createContainer({
|
|
217
|
+
image: 'nginx:alpine',
|
|
218
|
+
//eth0 和 eth1
|
|
219
|
+
networks: [{networkName: 'bridge'}, {networkName: backend.name, ip: '10.4.10.5'}],
|
|
220
|
+
//通过第一个网络发布
|
|
221
|
+
ports: [{port: 80, hostPorts: [8080]}]
|
|
222
|
+
})
|
|
223
|
+
await web.start()
|
|
224
|
+
console.log(web.networks, web.ports)
|
|
225
|
+
|
|
226
|
+
await web.remove({force: true})
|
|
227
|
+
//有容器使用时拒绝删除
|
|
228
|
+
await containerd.removeNetwork('backend')
|
|
229
|
+
console.log(await containerd.listNetworks())
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### 卷
|
|
233
|
+
|
|
234
|
+
命名卷是 `dataRoot` 下的目录,在容器挂载时或通过 `createVolume()` 创建。
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
import {type Containerd, type ContainerdVolumeInfo} from 'lakutata/com/containerd'
|
|
238
|
+
|
|
239
|
+
declare const containerd: Containerd
|
|
240
|
+
|
|
241
|
+
const volume: ContainerdVolumeInfo = await containerd.createVolume({name: 'data', labels: {app: 'web'}})
|
|
242
|
+
console.log(volume.mountpoint)
|
|
243
|
+
await containerd.createContainer({image: 'alpine:3.20', volumes: [{volumeName: 'data', containerPath: '/var/lib/app'}]})
|
|
244
|
+
console.log(await containerd.listVolumes())
|
|
245
|
+
//同时清理未使用的网络和卷
|
|
246
|
+
await containerd.prune({networks: true, volumes: true})
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
`removeVolume()` 拒绝删除有容器挂载的卷,即使容器已经停止。
|
|
250
|
+
|
|
251
|
+
### 用 BuildKit 构建
|
|
252
|
+
|
|
253
|
+
`buildImage()` 像 `docker buildx` 一样,通过 BuildKit 会话把构建上下文(不含 `.dockerignore` 排除的文件)和 Dockerfile 发送给 BuildKit,再把构建好的镜像导入 containerd。BuildKit 可以是本机的 `buildkitd`(通过它的 unix 套接字连接,使用 containerd 或 runc worker),也可以是远程的 BuildKit,通过 TCP 连接并使用 TLS:
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
import {readFileSync} from 'node:fs'
|
|
257
|
+
import {Application} from 'lakutata'
|
|
258
|
+
import {Containerd, type ContainerdImage} from 'lakutata/com/containerd'
|
|
259
|
+
|
|
260
|
+
Application
|
|
261
|
+
.run(() => ({
|
|
262
|
+
id: 'builder.app',
|
|
263
|
+
name: 'Builder',
|
|
264
|
+
components: {
|
|
265
|
+
containerd: {
|
|
266
|
+
class: Containerd,
|
|
267
|
+
buildkitAddress: 'tcp://builder:1234',
|
|
268
|
+
buildkitTLS: {ca: readFileSync('ca.pem'), cert: readFileSync('cert.pem'), key: readFileSync('key.pem')}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}))
|
|
272
|
+
.onLaunched(async (app: Application): Promise<void> => {
|
|
273
|
+
const containerd: Containerd = await app.getObject('containerd')
|
|
274
|
+
console.log(await containerd.buildkitVersion(), await containerd.buildkitPlatforms())
|
|
275
|
+
const image: ContainerdImage = await containerd.buildImage({
|
|
276
|
+
workdir: '/srv/app',
|
|
277
|
+
dockerfile: 'docker/App.Dockerfile',
|
|
278
|
+
repoTag: 'registry.example.com/team/app:1.0',
|
|
279
|
+
buildargs: {NODE_ENV: 'production'},
|
|
280
|
+
//默认为本机平台
|
|
281
|
+
platform: ['linux/arm64', 'linux/amd64'],
|
|
282
|
+
push: true,
|
|
283
|
+
outputCallback: (line: string): void => console.log(line)
|
|
284
|
+
})
|
|
285
|
+
console.log(image.platforms)
|
|
286
|
+
})
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
BuildKit 使用组件的镜像仓库凭据(或 `auth`)拉取基础镜像。
|
|
290
|
+
|
|
291
|
+
### 命名空间、平台与事件
|
|
292
|
+
|
|
293
|
+
每个组件只在一个命名空间中工作;要管理多个命名空间,就为每个命名空间注册一个组件(`k8s.io` 是 Kubernetes 的命名空间)。`supportedPlatforms()` 列出本机可以运行的平台:本机平台,以及在 `binfmt_misc` 中注册的模拟器对应的平台,所以注册了模拟器后,就能运行其他架构的容器。`events()` 以流的形式返回这个命名空间中 containerd 的事件。
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
import {Application} from 'lakutata'
|
|
297
|
+
import {Containerd, type ContainerdContainer, type ContainerdEvent} from 'lakutata/com/containerd'
|
|
298
|
+
|
|
299
|
+
Application
|
|
300
|
+
.run(() => ({
|
|
301
|
+
id: 'events.app',
|
|
302
|
+
name: 'Events',
|
|
303
|
+
components: {
|
|
304
|
+
containerd: {class: Containerd},
|
|
305
|
+
kubernetes: {class: Containerd, namespace: 'k8s.io'}
|
|
306
|
+
}
|
|
307
|
+
}))
|
|
308
|
+
.onLaunched(async (app: Application): Promise<void> => {
|
|
309
|
+
const containerd: Containerd = await app.getObject('containerd')
|
|
310
|
+
console.log(containerd.supportedPlatforms())
|
|
311
|
+
await containerd.pullImage({image: 'alpine:3.20', platform: 'linux/amd64'})
|
|
312
|
+
const container: ContainerdContainer = await containerd.createContainer({image: 'alpine:3.20', platform: 'linux/amd64', cmd: ['uname', '-m']})
|
|
313
|
+
await container.start()
|
|
314
|
+
const abort: AbortController = new AbortController()
|
|
315
|
+
setTimeout((): void => abort.abort(), 60000)
|
|
316
|
+
for await (const event of containerd.events(abort.signal)) {
|
|
317
|
+
//{topic: '/tasks/exit', type: 'containerd.events.TaskExit', event: {containerId, exitStatus…}, namespace, timestamp}
|
|
318
|
+
const received: ContainerdEvent = event
|
|
319
|
+
console.log(received.topic, received.event)
|
|
320
|
+
}
|
|
321
|
+
})
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
## API
|
|
325
|
+
|
|
326
|
+
| 导出 | 是什么 |
|
|
327
|
+
|---|---|
|
|
328
|
+
| `Containerd` | 组件:`version`、`supportedPlatforms`、`detectPlatforms`、`events`;镜像(`listImages`、`getImage`、`pullImage`、`buildImage`、`buildkitVersion`、`buildkitPlatforms`、`importImage`、`exportImages`);网络(`listNetworks`、`getNetwork`、`createNetwork`、`removeNetwork`);容器(`listContainers`、`getContainer`、`createContainer`);卷(`listVolumes`、`getVolume`、`createVolume`、`removeVolume`);`prune`。选项:`socketPath`、`namespace`、`snapshotter`、`runtime`、`cgroupManager`、`dataRoot`、`logMaxSize`、`cniConfigPath`、`cniPath`、`buildkitAddress`、`buildkitTLS`、`registryAuths`、`insecureRegistries` |
|
|
329
|
+
| `ContainerdImage` | 镜像:`name`、`digest`、`platforms`、`platform`、`size`、`config`……;`tag`、`push`、`export`、`remove`、`syncImageInfo` |
|
|
330
|
+
| `ContainerdContainer` | 容器:`id`、`name`、`state`、`networks`、`ports`、`volumes`……;`start`、`stop`、`restart`、`kill`、`pause`、`unpause`、`wait`、`remove`、`exec`、`logs`、`stats`、`createTTY`、`update`、`commit`、`exportDirectory`、`processes`、`image`、`syncContainerInfo` |
|
|
331
|
+
| `ContainerdContainerTTY` | 容器中的终端:`raw`、`readable`、`writable`、`onData`、`write`、`resize`、`wait`、`close` |
|
|
332
|
+
| `ImagePullOptions`、`ImagePushOptions`、`ImageBuildOptions`、`ImageImportOptions`、`ImageExportOptions` | 镜像操作的选项 |
|
|
333
|
+
| `ContainerCreateOptions`、`ContainerStopOptions`、`ContainerKillOptions`、`ContainerRemoveOptions`、`ContainerExecOptions`、`ContainerLogsOptions`、`ContainerTTYOptions`、`ContainerTTYConsoleSizeOptions`、`ContainerResourceOptions`、`ContainerCommitOptions`、`ContainerExportDirectoryOptions` | 容器操作的选项 |
|
|
334
|
+
| `NetworkCreateOptions`、`VolumeCreateOptions`、`ContainerdPruneOptions`、`ContainerdRegistryAuthOptions` | 网络、卷、`prune` 和镜像仓库凭据的选项 |
|
|
335
|
+
| `ContainerdBind`、`ContainerdVolume`、`ContainerdTmpfs`、`ContainerdDevice`、`ContainerdContainerPort`、`ContainerdRestartPolicy` | 容器选项中的各个部分 |
|
|
336
|
+
| `ContainerdContainerState`、`ContainerdContainerNetwork`、`ContainerdContainerStats`、`ContainerdExecResult`、`ContainerdImageConfig`、`ContainerdNetworkInfo`、`ContainerdVolumeInfo`、`ContainerdPruneResult`、`ContainerdEvent` | 返回的信息 |
|
|
337
|
+
| `ContainerdProgressCallback`、`ContainerdBuildOutputCallback` | 镜像传输和构建的回调 |
|
|
338
|
+
| `ContainerdConnectionException` 和其他 `Containerd*Exception` | 异常(见下文) |
|
|
339
|
+
|
|
340
|
+
它们都由 `lakutata/com/containerd` 和 `@lakutata/containerd` 导出。类型声明中的每一项都附有说明和示例。
|
|
341
|
+
|
|
342
|
+
## 错误
|
|
343
|
+
|
|
344
|
+
| 异常 | 何时抛出 |
|
|
345
|
+
|---|---|
|
|
346
|
+
| `ContainerdConnectionException`(`E_CONTAINERD_CONNECTION`) | 创建组件时 containerd 没有在 `socketPath` 上响应(containerd 没有运行,或者套接字无法访问);或者 `buildkitAddress` 是 unix 套接字,却给出了 `buildkitTLS` |
|
|
347
|
+
| `ContainerdImageNotFoundException`(`E_CONTAINERD_IMAGE_NOT_FOUND`) | 没有这个名称的镜像:`getImage()`、用尚未拉取的镜像调用 `createContainer()`、对已删除的镜像进行操作 |
|
|
348
|
+
| `ContainerdPlatformException`(`E_CONTAINERD_PLATFORM`) | 镜像没有所请求平台的清单(拉取、推送、创建容器时) |
|
|
349
|
+
| `ContainerdImagePullException`(`E_CONTAINERD_IMAGE_PULL`) | 镜像仓库无法访问、拒绝凭据或者没有这个引用;传输或解包失败 |
|
|
350
|
+
| `ContainerdImagePushException`(`E_CONTAINERD_IMAGE_PUSH`) | 镜像仓库无法访问或者拒绝推送 |
|
|
351
|
+
| `ContainerdImageImportException`(`E_CONTAINERD_IMAGE_IMPORT`) | 归档无法读取或者不含镜像;归档中有多个镜像,却给出了 `name`;解包失败 |
|
|
352
|
+
| `ContainerdImageExportException`(`E_CONTAINERD_IMAGE_EXPORT`) | 归档无法写入 |
|
|
353
|
+
| `ContainerdImageBuildException`(`E_CONTAINERD_IMAGE_BUILD`) | BuildKit 无法访问、构建失败,或者构建好的镜像无法导入 |
|
|
354
|
+
| `ContainerdContainerNotFoundException`(`E_CONTAINERD_CONTAINER_NOT_FOUND`) | 没有容器与这个 id、名称或 id 前缀匹配 |
|
|
355
|
+
| `ContainerdContainerNameConflictException`(`E_CONTAINERD_CONTAINER_NAME_CONFLICT`) | 已有其他容器使用这个名称 |
|
|
356
|
+
| `ContainerdContainerStateException`(`E_CONTAINERD_CONTAINER_STATE`) | 对没有运行的容器调用 `exec()`、`createTTY()`、`stats()`、`kill()`、`pause()`、`unpause()` 或 `processes()`;对已暂停的容器调用 `start()`;不带 `force` 对正在运行的容器调用 `remove()`;`update()` 需要重新创建一个不是由组件创建的容器 |
|
|
357
|
+
| `ContainerdPathNotFoundException`(`E_CONTAINERD_PATH_NOT_FOUND`) | `exportDirectory()` 的路径在容器中不存在 |
|
|
358
|
+
| `ContainerdNetworkNotFoundException`(`E_CONTAINERD_NETWORK_NOT_FOUND`) | 没有这个名称或 id 的网络;在不存在的网络上创建容器 |
|
|
359
|
+
| `ContainerdNetworkConflictException`(`E_CONTAINERD_NETWORK_CONFLICT`) | 网络已存在、子网与其他网络重叠、有容器在使用它、它是默认的 `bridge`,或者它不是 nerdctl 创建的 |
|
|
360
|
+
| `ContainerdVolumeNotFoundException`(`E_CONTAINERD_VOLUME_NOT_FOUND`) | 卷不存在 |
|
|
361
|
+
| `ContainerdVolumeInUseException`(`E_CONTAINERD_VOLUME_IN_USE`) | 有容器挂载了这个卷 |
|
|
362
|
+
| `InvalidValueException`(来自 `lakutata`) | 创建组件时选项无效;在线 `update()` 修改 `memorySwap` 时,容器没有内存限制,或者新值小于内存限制 |
|
|
363
|
+
| `InvalidMethodAcceptException`(来自 `lakutata`) | 调用的参数不符合选项的要求(容器名称含空格、缺少 `image`、设置了 `memorySwap` 却没有 `memoryLimit`) |
|
|
364
|
+
| `GRPCError`(来自 `@lakutata/grpc`) | `wait()` 被中止(对象被销毁);与 containerd 的连接断开 |
|
|
365
|
+
| `Error` | 容器的设置无法应用(有端口却没有 CNI 网络、没有命令、没有 CNI 插件、镜像中没有指定的用户);容器前缀匹配到多个容器 |
|
|
366
|
+
|
|
367
|
+
## 另见
|
|
368
|
+
|
|
369
|
+
- 网站上的指南:[Containerd 集成](https://lakutata.com/zh/guide/ContainerdIntegration)。
|
|
370
|
+
- `lakutata` 包中的 `doc/zh/ContainerdIntegration.md`:组件的指南。
|
|
371
|
+
- `@lakutata/docker`(`lakutata/com/docker`):通过 Docker 守护进程完成同样的操作。
|
|
372
|
+
- `@lakutata/grpc`:组件及其构建所用的 gRPC 客户端。
|
|
373
|
+
- `@lakutata/core`(`lakutata`):组件、`Application` 和依赖注入。
|
|
374
|
+
- `npm run test:engines`(在这个包中运行)会在特权 Docker 容器中针对 containerd 1.6、1.7 和 2.4 运行引擎测试(`test-env/run.sh 2.4` 只测一个版本,`--build` 重新构建它的镜像),同时使用本机和远程的 BuildKit;见 `test-env/README.md`。
|
package/package.json
CHANGED
|
@@ -1,7 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lakutata/containerd",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "The containerd component of the lakutata framework: images (pull, push, import, export, of any platform), containers and their processes, over the API of containerd, for the systems which run containerd without Docker.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"lakutata",
|
|
7
|
+
"containerd",
|
|
8
|
+
"containers",
|
|
9
|
+
"images",
|
|
10
|
+
"buildkit",
|
|
11
|
+
"oci",
|
|
12
|
+
"typescript"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://lakutata.com/packages/containerd",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/lakutata/lakutata/issues",
|
|
17
|
+
"email": "admin@lakutata.com"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/lakutata/lakutata.git",
|
|
22
|
+
"directory": "packages/containerd"
|
|
23
|
+
},
|
|
24
|
+
"author": "alex.ma",
|
|
25
|
+
"license": "MIT",
|
|
5
26
|
"type": "module",
|
|
6
27
|
"main": "./dist/cjs/exports/Containerd.js",
|
|
7
28
|
"types": "./dist/cjs/exports/Containerd.d.ts",
|
|
@@ -31,16 +52,16 @@
|
|
|
31
52
|
"test:engines": "sh test-env/run.sh"
|
|
32
53
|
},
|
|
33
54
|
"dependencies": {
|
|
34
|
-
"@lakutata/grpc": "3.
|
|
55
|
+
"@lakutata/grpc": "3.1.0",
|
|
35
56
|
"tar-stream": "3.2.2"
|
|
36
57
|
},
|
|
37
58
|
"peerDependencies": {
|
|
38
|
-
"@lakutata/core": "3.
|
|
39
|
-
"@lakutata/logger": "3.
|
|
40
|
-
"@lakutata/utils": "3.
|
|
59
|
+
"@lakutata/core": "3.1.0",
|
|
60
|
+
"@lakutata/logger": "3.1.0",
|
|
61
|
+
"@lakutata/utils": "3.1.0"
|
|
41
62
|
},
|
|
42
63
|
"engines": {
|
|
43
|
-
"node": ">=
|
|
64
|
+
"node": "^22.13.0 || >=24.11.0"
|
|
44
65
|
},
|
|
45
66
|
"files": [
|
|
46
67
|
"dist",
|