@lakutata/containerd 3.0.0 → 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.
Files changed (3) hide show
  1. package/README.md +88 -79
  2. package/README.zh-CN.md +374 -0
  3. package/package.json +27 -6
package/README.md CHANGED
@@ -1,13 +1,16 @@
1
1
  # @lakutata/containerd
2
2
 
3
- The containerd component of the lakutata framework: it manages the images, containers, CNI networks and named volumes
4
- of a [containerd](https://containerd.io) namespace by talking to containerd's gRPC socket directly, without a Docker
5
- daemon and without nerdctl (the gRPC client is written in TypeScript: no native addon). It targets the Linux machines
6
- running containerd alone, such as embedded devices, with an API following the Docker component's: images pulled with
7
- registry credentials, pushed, tagged, imported, exported and pruned; containers created with ports, volumes, devices,
8
- seccomp and resource limits, then started, stopped, executed in, followed, measured, updated, committed and copied
9
- from; builds sent to BuildKit. The containers it creates are nerdctl's, so `nerdctl ps` lists them. The umbrella
10
- package `lakutata` re-exports it as `lakutata/com/containerd`.
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 dependency. The machine needs:
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 or later; the application runs as root or as a member of the group owning containerd's socket |
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` of the application and get it with `getObject()` or `@Inject`. It connects
33
- when it is created: without containerd on `socketPath`, the application fails to start with
34
- `ContainerdConnectionException`. Its namespace is created when it does not exist.
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 it (0: never)
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
- - No daemon in between: the component itself pulls, unpacks, pushes, imports and exports the images, and generates the
78
- containers' OCI specifications, as nerdctl does. The containers carry nerdctl's labels (`nerdctl/name`…), and the
79
- CNI network configurations are nerdctl's (`nerdctl-<name>.conflist`): nerdctl and the component share them.
80
- - A container's output is written to a log file by containerd's shim, and its restart policy is applied by containerd's
81
- restart monitor: both keep working while the application is stopped. The logs have no timestamps.
82
- - `createContainer()` takes the image in its options, and never pulls it: pull it first. As with Docker, the containers
83
- join the default network `bridge` by default (a CNI network created when first used: the CNI plugins are needed, see
84
- the installation), where their ports are published; `network: 'host'` gives them the host's network.
85
- - There is no `loginRepository()`: the credentials are the `registryAuths` option, or the `auth` of each call.
86
- - `importImage()` returns every image of the archive, and `prune()` returns what it removed.
87
- - A namespace is the unit: the images, containers and volumes of the other namespaces are not seen.
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 runs are unpacked)
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'), the credentials of one pull
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
- //Another name: only for an archive of one image
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
- The names are normalized as Docker does (`alpine` is `docker.io/library/alpine:latest`). containerd keeps an image's
128
- content while an image or a container uses it. A multi-platform image is described for the first of its platforms
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
- //And as much swap (memorySwap: twice the memory by default, -1 unlimited)
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: live (0 removes a limit); any other setting: the container created again, its name kept
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
- //A container which is not running: its last exit code at once
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
- - The containers get containerd's default seccomp profile, with the system calls their `capabilities` allow (as
192
- nerdctl does); `seccompProfile: 'unconfined'` disables it, and a file path gives an OCI profile. The privileged
193
- containers are unconfined.
194
- - `devices` creates the host's devices in the container (a device, or every device of a directory such as `/dev/snd`).
195
- For the devices plugged later, bind their directory and allow their numbers with `deviceCgroupRules`, as Docker's
196
- `--device-cgroup-rule` does. A `privileged` container gets every device of the host's `/dev` (as nerdctl gives
197
- them).
198
- - The containers have their own cgroup namespace on cgroup v2 (`cgroupns: 'host'` shares the host's).
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` are Docker's
201
- options of the same names. As with Docker, `memorySwap` is the memory and the swap together, needs `memoryLimit`,
202
- and is twice `memoryLimit` by default.
203
- - `update()` with other settings than the resources and the restart policy only works for the containers the component
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()` of a container which is not running resolves at once, as `docker wait` does: the exit code of its last run,
207
- `0` when it was never started.
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; a string command runs
210
- through `/bin/sh -c` as `exec()` runs it, an array as it is.
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`, the containers join CNI networks: nerdctl's default `bridge` (`nerdctl0`, `10.4.0.0/24`,
231
- created when first asked for; the network of the containers created without `network` nor `networks`) and the networks created, bridges, macvlans or ipvlans, with IPv4 and
232
- IPv6 subnets. Each container's networks are set up by its runc hooks, so they also work when containerd restarts it
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
- //A free subnet of 10.4.0.0/16 without subnet
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 one.
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 its session, as `docker buildx` does, then imports the image built into containerd. BuildKit is a local
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 one). `supportedPlatforms()` lists the platforms the machine runs: its own and the emulators registered in
326
- `binfmt_misc`, so that a container of another architecture runs once its emulator is registered. `events()` streams
327
- containerd's events of the namespace.
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 of the networks, the volumes, `prune` and the registry credentials |
369
- | `ContainerdBind`, `ContainerdVolume`, `ContainerdTmpfs`, `ContainerdDevice`, `ContainerdContainerPort`, `ContainerdRestartPolicy` | The parts of the container options |
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 of the transfers and of the builds |
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, no access to the socket), or `buildkitTLS` is given with a unix socket `buildkitAddress` |
382
- | `ContainerdImageNotFoundException` (`E_CONTAINERD_IMAGE_NOT_FOUND`) | No image has the name: `getImage()`, `createContainer()` of an image not pulled, an operation on an image removed |
383
- | `ContainerdPlatformException` (`E_CONTAINERD_PLATFORM`) | The image has no manifest for the platform asked (pull, push, container creation) |
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 has no image; a `name` is given with an archive of several images; the unpacking fails |
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 built cannot be imported |
389
- | `ContainerdContainerNotFoundException` (`E_CONTAINERD_CONTAINER_NOT_FOUND`) | No container has the identifier, name or prefix |
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()`, `processes()` on a container not running; `start()` on a paused one; `remove()` on a running one without `force`; an `update()` recreating a container the component did not create |
392
- | `ContainerdPathNotFoundException` (`E_CONTAINERD_PATH_NOT_FOUND`) | `exportDirectory()` of a path missing from the container |
393
- | `ContainerdNetworkNotFoundException` (`E_CONTAINERD_NETWORK_NOT_FOUND`) | No network has the name or identifier; a container created on a missing network |
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 a `memorySwap` without a memory limit, or below it |
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 matching several containers |
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
- - `doc/en/ContainerdIntegration.md` of the package `lakutata` (Chinese in `doc/zh`): the guide of the component.
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`): the components, `Application` and the dependency injection.
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`.
@@ -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.0.0",
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.0.0",
55
+ "@lakutata/grpc": "3.1.0",
35
56
  "tar-stream": "3.2.2"
36
57
  },
37
58
  "peerDependencies": {
38
- "@lakutata/core": "3.0.0",
39
- "@lakutata/logger": "3.0.0",
40
- "@lakutata/utils": "3.0.0"
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": ">=22"
64
+ "node": "^22.13.0 || >=24.11.0"
44
65
  },
45
66
  "files": [
46
67
  "dist",